• 2026年7月16日
  • 5 分钟阅读
  • SystemDo

Nginx 与 PM2 性能如何优化?指标、瓶颈定位与容量测试

本文从指标采集、压测方法、瓶颈定位到参数调整与容量基线,系统讲解如何优化Nginx与PM2组合下的Node.js应用性能,为企业决策提供可操作的工程实践。

Nginx 与 PM2 性能如何优化?指标、瓶颈定位与容量测试
SystemDo
SystemDo

软件定制开发团队

"真正有价值的技术内容,应该能帮助客户更快判断方向、预算和落地路径。"

为什么性能优化不能只靠直觉

在生产环境中,Nginx 与 PM2 的组合是 Node.js 应用最常见的部署方案。很多团队在遇到响应变慢或请求失败时,第一反应是增加实例数或升级服务器配置。这种做法有时能缓解症状,但更多时候只是掩盖了真正的瓶颈。

性能优化的前提是量化。没有指标,就没有优化方向。没有压测,就无法验证优化效果。没有容量基线,就无法判断系统何时会过载。本文围绕这四点展开:指标采集、压测方法、瓶颈定位和参数调整,最终给出容量基线的制定思路。

这六个步骤可以形成一个闭环:采集基线指标 → 设计压测场景 → 执行压测并监控 → 定位瓶颈 → 调整参数 → 验证并更新基线。每次迭代都基于数据,而非猜测。

关键指标:从操作系统到应用层

优化之前,先要明确该采集哪些指标。我把它们分为三个层次。

1. 操作系统层

CPU 使用率、内存占用、磁盘 I/O 和网络带宽是最基础的指标。对于 Nginx 和 PM2 场景,需要重点关注:

  • **CPU 上下文切换**:高上下文切换意味着内核在频繁切换进程或线程,常见于 PM2 集群模式下实例数超过 CPU 核心数的情况。
  • **平均负载(Load Average)**:如果负载值持续超过 CPU 核心数,说明系统处于过载状态,但不一定是 CPU 瓶颈——也可能是 I/O 等待。
  • **网络连接状态**:`ss -s` 可以查看当前系统的 TCP 连接数,`TIME_WAIT` 和 `CLOSE_WAIT` 数量异常时,通常与 Nginx 的 keepalive 配置有关。

2. Nginx 层

Nginx 自身的指标主要通过 `stub_status` 模块获取。开启后访问 `/nginx_status` 可以拿到:

  • Active connections:当前活跃连接数。
  • Accepts / Handled / Requests:累计的接受连接、处理连接和总请求数。如果 Accepts 与 Handled 差距较大,说明有连接被丢弃,可能达到 worker_connections 上限。
  • Reading / Writing / Waiting:Reading 表示正在读取请求头,Writing 表示正在发送响应,Waiting 表示空闲 keepalive 连接。

更详细的数据可以通过 Prometheus + nginx_exporter 采集,包括请求延迟分布、上游响应时间等。

3. PM2 与 Node.js 层

PM2 本身提供 `pm2 monit` 命令,可以实时查看每个进程的 CPU 和内存使用。但更关键的是 Node.js 应用内部的指标:

  • **事件循环延迟**:使用 `process.hrtime()` 或 `perf_hooks` 模块可以测量事件循环的阻塞时间。超过 50ms 说明有同步操作阻塞了主线程。
  • **垃圾回收频率与耗时**:通过 `--trace-gc` 启动应用,或使用 `v8` 模块的 `getHeapStatistics()` 监控堆内存变化。频繁的 GC 暂停会导致请求延迟抖动。
  • **请求处理时间**:在应用层记录每个请求的处理时间,包括路由处理、数据库查询和外部 API 调用。这是判断瓶颈在应用内部还是外部依赖的关键。

这些指标需要统一采集到监控系统(如 Prometheus + Grafana 或 Datadog),才能在做压测时实时关联分析。

压测工具与场景设计

压测不是随便跑个 ab 或 wrk 就行了。场景设计决定了压测结果的可信度。

工具选择

  • **wrk**:适合测试高并发下的吞吐量,但只能发送预定义的请求,无法模拟用户行为。
  • **k6**:脚本化压测工具,支持模拟登录、会话保持、不同路径的请求比例。更适合接近真实场景的压测。
  • **Locust**:基于 Python,支持分布式压测,适合需要模拟大量并发用户的场景。

对于 Nginx + PM2 的组合,我建议优先用 k6。因为它可以精确控制请求间隔、并发数和持续时间,还能输出详细的响应时间分布。

压测场景设计

至少设计三种场景:

1. **基准场景**:低并发(如 10 个虚拟用户),持续 5 分钟。目的是获取系统在无压力下的基线响应时间(P50、P95、P99)和错误率。
2. **负载场景**:逐步增加并发用户数(从 50 到 500),每个阶梯持续 2 分钟。目的是找到系统的拐点——即响应时间开始急剧上升或错误率超过 1% 的并发数。
3. **压力场景**:在负载场景找到的拐点附近,持续运行 15 分钟。目的是验证系统在持续压力下是否稳定,内存是否会持续增长(内存泄漏),GC 频率是否正常。

每个场景都要同时采集上述三个层次的指标,并记录时间戳,方便后续对齐分析。

瓶颈定位:从现象到根因

压测过程中出现性能问题,不要急着改配置。先定位瓶颈在哪里。

现象一:Nginx 返回 502 或 504

502 通常说明 Nginx 无法连接上游(PM2 进程),504 说明上游响应超时。

检查步骤:

  • 先看 PM2 进程数:`pm2 list`。如果进程全部挂掉,检查应用是否有未捕获的异常导致进程退出。PM2 的 `--max-memory-restart` 参数如果设置过低,也会导致进程被频繁重启。
  • 如果进程存活,用 `ss -tlnp | grep :3000` 检查 PM2 监听的端口是否正常。如果 PM2 使用 socket 文件,检查文件权限。
  • 再看 Nginx 的 `proxy_pass` 配置。如果上游使用 IP:Port 形式,确保 IP 和端口正确;如果使用 Unix socket,确保路径一致。
  • 最后看 Nginx 的 `proxy_read_timeout` 和 `proxy_connect_timeout`。如果应用处理请求时间较长(比如超过 60 秒),而超时时间设置过短,就会提前断开连接。

现象二:响应时间正常,但吞吐量上不去

这种情况通常出现在 Nginx 层。

  • 检查 `worker_connections` 是否足够。计算公式:最大并发连接数 = worker_processes * worker_connections。如果压测并发数接近这个值,Nginx 会开始丢弃连接。
  • 检查 `worker_processes` 是否设置为 `auto`。Nginx 会自动匹配 CPU 核心数,但如果服务器上还运行了其他服务(如数据库),手动调整为 CPU 核心数减 1 可能更合理。
  • 检查 keepalive 配置。如果客户端与 Nginx 之间没有开启 keepalive,每次请求都会新建 TCP 连接,消耗大量资源。在 `http` 块中设置 `keepalive_requests 1000; keepalive_timeout 65;`。

现象三:CPU 使用率不高,但响应时间抖动

这往往意味着瓶颈在应用内部,且与 I/O 或锁有关。

  • 检查 Node.js 事件循环延迟。在压测过程中,用 `clinic doctor` 或 `0x` 工具生成火焰图,可以直观看到哪些函数占用了大量时间。
  • 检查数据库查询。如果应用使用了 ORM,容易产生 N+1 查询问题。在压测时打开数据库的慢查询日志,看是否有大量重复查询。
  • 检查是否有同步文件操作。Node.js 的 `fs.readFileSync` 会阻塞事件循环,在并发场景下应该替换为异步版本。

现象四:内存持续增长

如果压测过程中 PM2 进程的内存占用不断上升,且压测结束后没有回落,说明存在内存泄漏。

排查方法:

  • 使用 `node --inspect` 启动应用,在 Chrome DevTools 中抓取堆快照,对比压测前后的对象数量变化。
  • 常见泄漏点:全局变量、闭包引用、未清理的定时器、事件监听器未移除。
  • 设置 PM2 的 `--max-memory-restart` 作为兜底措施,但不要依赖它。内存泄漏必须修复,而不是靠重启掩盖。

Nginx 参数调优:基于压测结果做微调

参数调整必须基于压测数据,而不是照搬网上的配置。以下是几个关键参数的调整思路。

worker_processes 与 worker_connections

这两个参数决定了 Nginx 能处理的最大并发连接数。但并非越大越好。

  • worker_processes 建议设置为 CPU 核心数或核心数减 1。如果设置为 `auto`,Nginx 会为每个 CPU 核心启动一个 worker 进程。在虚拟化环境中,如果宿主机超分严重,`auto` 可能检测到错误的核数,此时手动指定更可靠。
  • worker_connections 默认 1024,对于大多数中小型应用足够。但如果你预期并发连接数超过 5000,可以调高到 4096 或 8192。注意:每个连接会占用内存,调高后需要监控内存使用。

一个常见误区:为了处理高并发,把 worker_processes 设置得比 CPU 核心数多很多。这会导致上下文切换开销增加,反而降低吞吐量。

proxy_buffering 与 proxy_buffer_size

默认情况下 Nginx 会缓冲上游响应。如果应用返回的响应体较大(比如超过 4KB),缓冲可以减少上游等待时间,但会增加 Nginx 内存占用。

  • 如果应用返回的是流式数据(如 SSE、大文件下载),应该关闭缓冲:`proxy_buffering off;`。
  • 如果应用返回的是 JSON 或 HTML 页面,建议开启缓冲,并设置合理的 `proxy_buffer_size`(默认 4KB)和 `proxy_buffers`(数量和大小)。

压测时可以通过观察 Nginx 的 Writing 指标来判断缓冲是否合理。如果 Writing 持续很高,说明 Nginx 正在大量写入响应,可能需要调整缓冲参数。

keepalive 与 upstream 配置

在 Nginx 与 PM2 之间,默认每次请求都会新建 TCP 连接。开启 keepalive 可以复用连接,减少握手开销。

配置示例:

```
upstream node_app {
server 127.0.0.1:3000;
keepalive 64;
}

server {
location / {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://node_app;
}
}
```

keepalive 值设为 64 意味着每个 Nginx worker 会保持最多 64 个空闲连接。如果 PM2 的实例数为 4,那么总空闲连接数为 4 * 64 = 256。这个值需要根据压测中的连接复用率来调整——如果复用率低于 80%,可以适当调高。

PM2 参数调优:集群模式与资源限制

PM2 的 cluster mode 可以让应用利用多核 CPU,但配置不当反而会降低性能。

实例数的选择

很多人习惯把实例数设置为 CPU 核心数,认为这样能充分利用资源。实际上,对于 I/O 密集型的 Node.js 应用,实例数可以适当超过 CPU 核心数。因为 Node.js 的事件循环在处理 I/O 操作时会释放 CPU,让其他实例有机会执行。

但有一个前提:应用不能有大量同步计算。如果应用是 CPU 密集型(如图像处理、加密运算),实例数超过 CPU 核心数反而会导致频繁的上下文切换。

最佳做法:从 CPU 核心数开始,逐步增加实例数,在压测中观察吞吐量和响应时间的变化。当实例数达到某个值后,吞吐量不再增加甚至下降,那就是最佳值。

内存限制与重启策略

PM2 的 `--max-memory-restart` 参数可以设置进程的最大内存使用量。这个值应该基于压测中的内存基线来设定。

例如,压测中单个实例的稳定内存使用是 200MB,那么 `--max-memory-restart` 可以设为 300MB 或 400MB,留出缓冲空间。设置过低会导致进程被频繁重启,影响服务稳定性。

`--max-restarts` 参数控制重启次数上限,防止进程反复崩溃时无限重启。建议设为 10 次/分钟,避免短时间内大量重启导致服务完全不可用。

日志与监控配置

PM2 默认会输出所有日志到文件,高并发场景下日志 I/O 会成为性能瓶颈。

建议:

  • 将日志级别设置为 `error` 或 `warn`,避免输出 debug 日志。
  • 使用 `--merge-logs` 选项合并日志文件,减少文件句柄占用。
  • 考虑使用 PM2 的 `--log-type json` 输出结构化日志,方便接入日志收集系统(如 ELK 或 Loki)。

在 SystemDo 的项目中,我们曾遇到一个案例:某电商应用在双十一压测时,PM2 的日志文件写入速度达到每秒 50MB,导致磁盘 I/O 成为瓶颈。将日志级别从 `info` 改为 `error` 后,吞吐量提升了 30%。

容量基线的制定与维护

容量基线是性能优化的最终产出。它回答了两个问题:系统当前能承受多少并发?什么时候需要扩容?

基线内容

一个完整的容量基线至少包含以下信息:

  • **最大并发用户数**:在响应时间 P95 不超过 500ms、错误率不超过 1% 的前提下,系统能支持的最大并发数。
  • **吞吐量上限**:每秒请求数(RPS)的上限,以及对应的 CPU、内存、网络带宽使用率。
  • **资源使用拐点**:当并发数达到多少时,CPU 使用率超过 80%,或内存使用超过 70%。这些是扩容的预警指标。
  • **PM2 实例数**:在达到吞吐量上限时,PM2 的实例数和每个实例的资源消耗。

如何制定基线

基线不是一次压测就能确定的。需要经过多轮压测,每次调整一个参数,记录结果。最终取一个保守值作为生产环境的容量上限。

例如,压测结果显示在 500 并发下 P95 响应时间为 450ms,RPS 为 2000。那么生产环境的容量上限可以设为 400 并发,留出 20% 的缓冲空间。

基线还需要考虑业务高峰期。如果业务有季节性波动(如电商促销),需要在基线基础上增加弹性扩容策略。

基线的更新

容量基线不是静态的。每次应用代码变更、依赖升级或服务器配置调整后,都应该重新压测并更新基线。

建议:

  • 每次上线前,在预发布环境执行一次基准压测,与线上基线对比。如果 P95 响应时间上升超过 10%,需要排查原因。
  • 至少每季度执行一次完整压测,验证基线是否仍然有效。
  • 将基线数据纳入监控系统,当实际流量接近基线值时自动触发扩容告警。

常见风险与避坑指南

风险一:压测环境与生产环境不一致

压测结果只有在与生产环境配置一致时才有参考价值。常见差异包括:服务器规格不同、网络带宽不同、数据库数据量不同。建议在预发布环境或生产环境的低峰期进行压测,并确保数据库数据量与生产环境相近。

风险二:忽略冷启动问题

PM2 的 cluster mode 在启动时会同时创建所有实例,如果应用初始化时间较长(如加载模型、建立数据库连接池),会导致启动期间请求超时。建议使用 PM2 的 `--wait-ready` 选项,让应用在初始化完成后才接受请求。

风险三:过度优化

很多团队花大量时间优化 Nginx 的缓冲区大小或 PM2 的日志级别,而忽略了应用本身的性能问题。在压测中,如果应用层的请求处理时间占整体响应时间的 80% 以上,优化 Nginx 和 PM2 的效果有限。此时应该优先优化应用代码。

风险四:依赖默认配置

Nginx 和 PM2 的默认配置适用于开发环境,不适合生产。例如 Nginx 的 `sendfile` 默认开启,但如果应用返回的是动态内容,`sendfile` 几乎没有帮助。PM2 的 `--instances max` 在某些服务器上会导致实例数过多,反而降低性能。每个参数都应该根据压测结果来调整。

总结:性能优化是一个持续过程

性能优化不是一次性的工作。指标采集、压测、瓶颈定位、参数调整和容量基线制定,构成了一个持续改进的循环。每次代码变更或部署环境变化,都可能打破原有的性能平衡。

对于企业来说,建立标准化的压测流程和容量基线管理机制,比追求某一次的极致优化更有价值。当系统出现性能问题时,团队能够快速定位瓶颈并做出调整,而不是靠重启或扩容来临时应付。

在实际项目中,我们遇到过很多团队在压测阶段表现良好,上线后却出现性能问题。原因往往是压测场景与真实流量模式不符。因此,除了常规的负载场景,还应该设计突发流量场景(如瞬间并发翻倍)和慢请求场景(如某个接口响应时间异常),来验证系统在极端情况下的表现。

最终,性能优化的目标不是跑出最高的 RPS,而是保证系统在预期负载下稳定运行,并且在负载超出预期时能够优雅降级,而不是直接崩溃。