将 Node.js 应用部署到生产环境只是第一步,保证它能够持续稳定、高性能地运行才是运维的核心目标。与开发阶段不同,线上问题往往没有交互式调试环境,也无法随意重启服务,因此需要提前建立一套可靠的监控与排错体系。本节从日志、性能指标、错误告警、以及常见瓶颈(内存泄漏、CPU 飙高)的排查方法四个方面,介绍如何在生产环境中维护 Node.js 服务。
19.4.1 日志体系:可读、可查、不丢失
日志是排查问题的最基本依靠。生产环境的日志需要满足几个要求:分级明确、格式统一、输出高效、集中存储。
日志分级与输出格式
使用结构化日志库,例如 pino(性能极高)或 winston,按 error、warn、info、debug 等级别记录日志。在 Node.js 中推荐使用 JSON 格式输出,便于后续日志系统索引与检索。例如:
const pino = require('pino');
const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
// 记录请求日志
logger.info({ method: req.method, url: req.url, status: res.statusCode }, 'request completed');
// 记录错误,附带堆栈信息
logger.error({ err, requestId }, 'database connection failed');
pino 自带极低序列化开销,当与 pino-pretty 搭配用于本地开发时会输出可读格式,但在生产环境应保留原始 JSON 并交给集中式日志平台(如 ELK、Splunk、Loki)去解析。
日志切割与多目标输出
生产环境中单一日志文件会快速膨胀,可通过 pino 的传输模块或系统工具(如 logrotate)进行切割。另外,关键错误日志除了写入文件外还应直接输出到 stdout/stderr,以便容器编排平台(Kubernetes、Docker)统一抓取。
集中式日志的实践
对于微服务或多节点部署,必须将所有服务日志汇聚到集中式系统。常见的开源方案有 ELK(Elasticsearch + Logstash + Kibana)或 Grafana Loki。每个 Node.js 实例只需将日志以 JSON 格式发送到 stdout,再由 Filebeat 或 Fluentd 转发至中心存储。在 Kibana 或 Grafana 中,可以按 requestId、userId 等字段快速串联一次请求的完整链路日志。
19.4.2 性能监控:洞悉系统脉搏
日志告诉我们“发生了什么”,监控指标则告诉我们“正在发生什么”。Node.js 线上运行需要关注至少以下四类指标:
进程级别指标
- CPU 使用率(user、system 占比)
- 内存占用(RSS、heapUsed、heapTotal、external)
- 事件循环延迟(lag):测量任务被调度的延迟,延迟过高会导致请求响应变慢
- 活跃句柄数与文件描述符数(防止句柄泄漏)
- 请求吞吐量(RPS)与响应时间(P50、P95、P99)
采集方式
- PM2 内嵌监控:
pm2 monit可实时查看每个进程的 CPU/内存/事件循环延迟,并可通过pm2 server:monit启动 Web 监控面板。适合小规模部署。 - 应用性能管理(APM)工具:如 Elastic APM、Datadog、New Relic、Sentry Performance 等提供 Node.js 客户端库,通过
require引入即可自动收集数据库查询、外部 HTTP 请求、中间件耗时等细节,并生成服务拓扑与慢端点分析。 - 开源自建方案:使用
prom-client(Prometheus 客户端)暴露/metrics端点,再用 Prometheus 抓取,最终通过 Grafana 制作大盘。这是目前云原生环境下的主流方案。
一个暴露基础指标的示例:
const client = require('prom-client');
const collectDefaultMetrics = client.collectDefaultMetrics;
collectDefaultMetrics({ prefix: 'myapp_' });
// 自定义指标:请求计数器
const httpRequestsTotal = new client.Counter({
name: 'http_requests_total',
help: 'Total number of HTTP requests',
labelNames: ['method', 'route', 'status'],
});
在 Grafana 中可基于这些指标可视化 QPS、错误率、内存趋势等,并设置阈值告警。
事件循环延迟监控
事件循环延迟是 Node.js 健康度的一个重要预警指标。可使用 toobusy-js 或自行监测 setImmediate 的时间差来判断是否过载:
let lastTime = Date.now();
setInterval(() => {
const now = Date.now();
const loopDelay = now - lastTime - 1000; // 如果该回调延迟超过 1 秒,则差值>0
lastTime = now;
if (loopDelay > 100) {
logger.warn({ loopDelay }, 'event loop delayed');
}
}, 1000);
更精确的方式是使用 perf_hooks.monitorEventLoopDelay(),可以得到事件循环延迟的直方图统计。
19.4.3 错误告警:第一时间响应
在生产环境中,未被捕獲的异常会导致进程退出;而逻辑错误即使不崩溃也可能造成业务异常。需要建立多层级告警通道。
全局异常捕获与进程守护
代码中应使用 process.on('uncaughtException', handler) 和 process.on('unhandledRejection', handler) 记录致命错误日志后再优雅退出,让 PM2 或 Kubernetes 自动重启。但是不应在此类 handler 中尝试恢复应用状态,因为进程可能已经不稳定。
错误收集与发送告警
推荐使用 Sentry 或 Fundebug 等错误跟踪服务。它们提供了 Node.js SDK,可捕获并聚合错误,附带请求上下文、堆栈和用户环境信息,并支持邮件、Slack、钉钉等即时通知。集成方式很简单:
const Sentry = require('@sentry/node');
Sentry.init({ dsn: 'your-dsn' });
对于业务自定义错误,可手动调用 Sentry.captureException(error)。同时可以将 Sentry 与日志系统打通,用 Sentry 事件 ID 关联详细日志。
告警分级与降噪
设置合理的告警阈值,避免“狼来了”。例如:接口报错率连续 5 分钟 >5% 时触发警告;内存使用率持续增长超过 80% 时告警。利用监控系统的告警规则配置(如 Prometheus Alertmanager)可以细粒度管理告警路由与静默。
19.4.4 常见性能问题排查
当监控指标显示异常时(如内存持续上涨、CPU 使用率突然飙高),需要针对性地排查根因。
内存泄漏排查
内存泄漏在 Node.js 中通常表现为 RSS 稳步增长,最终导致 OOM 崩溃。常见原因有:闭包中意外持有大对象引用、事件监听器未移除、定时器未清除、流未正确消费等。
定位步骤:
- 生成 heap snapshot:在检测到内存升高时,通过
v8.writeHeapSnapshot()或使用heapdump模块生成堆快照。可以在应用内添加一个受保护的、仅内网可访问的触发接口。 - 对比快照:使用 Chrome DevTools 的 Memory 面板,加载两个时间点的快照,对比两者之间哪些对象数量和大小激增,找出可疑的构造函数或字符串。
- 结合应用上下文:一旦通过快照定位到哪类对象(如数据库连接、缓存对象)在累积,便可回溯代码中相应对象的生命周期管理。
对于线上难以生成快照的情况,可以使用 clinic 工具的 clinic doctor,通过采样生成分析报告,它通常能指出内存泄漏的嫌疑区域。
CPU 占用过高排查
高 CPU 常意味着事件循环被密集计算阻塞,导致服务响应变慢甚至超时。可能的原因:巨大的同步循环、复杂正则表达式、JSON 序列化/反序列化大对象、缺少限流的加密操作等。
排查工具与流程:
- 使用
--prof生成 V8 日志:启动应用时加入node --prof app.js,会生成isolate-.log文件,再通过node --prof-process isolate-.log转换成人类可读的分析报告,查看最耗时的 C++/JS 函数调用栈。 - 使用火焰图:推荐使用
0x工具,它自动采样 CPU profile 并生成火焰图。
npm install -g 0x
0x -o app.js
火焰图中宽度最大的“平顶”就是 CPU 热点路径,可直接定位到具体的代码函数。
- 使用 clinic flame:
clinic flame -- node app.js启动后模拟流量,它会生成一份火焰图报告,直观展示事件循环中被耗时的同步操作。 - 使用 Chrome DevTools:可以在本地复现场景,用
node --inspect app.js启动,然后通过 Chrome 的chrome://inspect连接,使用 Profiler 开始录制 CPU profile,操作线上同样的逻辑,然后分析函数耗时。
在 Node.js 中,一个常见的 CPU 杀手是同步阻塞的大规模计算。例如,一次返回数万条数据的数据库查询后用 JSON.stringify 同步转换,占用 CPU 数百毫秒。这类问题可以通过流式处理、分页、或转移到 Worker 线程来解决。
19.4.5 建立排错文化
监控与排错最终离不开团队的响应流程。建议:
- 将监控大盘作为团队每日第一眼:关键服务指标应投射到办公室大屏或团队 IM 频道,形成习惯。
- 每次故障后产出复盘报告:记录报警过程、定位步骤、根因、修复措施,并归档到内部知识库,避免重复踩坑。
- 定期进行故障演练:在预发环境人为注入内存泄漏或 CPU 高负载,演练团队的发现、响应、恢复流程。
线上监控与排错不是一次性配置,而是一个随着业务迭代持续演进的体系。借助 Node.js 生态丰富的监控 SDK 和诊断工具,我们完全可以将生产环境的黑盒逐渐透明化,在问题影响用户之前就发现并解决它。