20. PM2、Nginx、Cluster 与部署选择
本章解决的问题
Nginx、PM2 和 Node.js Cluster 都能出现在同一张架构图里,但职责不同。最需要避免的是 PM2 已开多个实例,应用内部又 cluster.fork() 多个 Worker,最终产生不可控的双重扩容。
三者职责
客户端
↓
Nginx:TLS、反向代理、静态文件、入口限流
↓
PM2:启动、监控、重启、日志、滚动重载 Node.js 实例
↓
Node.js 应用:HTTP 业务、优雅关闭
应用内 Cluster 是另一种多进程管理方案:一个 Primary 管理多个 Worker。它与 PM2 Cluster Mode 的目标部分重叠。
不要双重 Cluster
PM2 instances = 4
每个应用又 cluster.fork() 4 次
理论 Worker = 4 × 4 = 16
随之放大的还有:
- 数据库与 Redis 连接;
- 内存占用;
- 定时任务和队列消费者;
- 日志量;
- 停机与重启复杂度;
- CPU 上下文切换。
除非经过明确容量设计,通常只选择一层负责 Node.js 多实例。
三种常见部署方案
方案 A:PM2 Cluster Mode
PM2 管理 N 个应用实例
应用代码保持单进程,不使用 cluster.fork()
适合大多数传统服务器上的 Express 项目。应用更简单,PM2 负责实例生命周期和滚动重载。
// ecosystem.config.js
module.exports = {
apps: [
{
name: "learning-api",
script: "dist/server.js",
exec_mode: "cluster",
instances: 2,
kill_timeout: 10000,
listen_timeout: 8000,
env: {
NODE_ENV: "production",
PORT: "8080",
},
},
],
};
当前项目学习时使用 ts-node server.ts,生产部署仍建议先 tsc 编译并让 PM2 启动 dist/server.js,以减少启动差异和每实例编译开销。
方案 B:应用内 Cluster
PM2 fork mode 管理一个 Primary
Primary 自己管理 N 个 Cluster Worker
适合学习 Cluster,或业务确实需要定制 Worker 分配、IPC 和重启策略。此时 PM2 的 instances 通常设为 1,避免再扩一层。
方案 C:容器/编排平台多副本
一个容器一个 Node.js 进程
Kubernetes/平台横向扩容多个 Pod
这是常见的云原生选择。进程模型简单,故障与资源边界清楚。是否在每个容器内再开 Cluster,要根据容器 CPU 配额、内存和运维策略评估,不能机械套用宿主机 CPU 数。
选择表
| 环境/需求 | 推荐起点 |
|---|---|
| 单机学习 Cluster 原理 | 应用内 Cluster |
| 单台传统服务器部署 Express | PM2 Cluster Mode,应用单进程 |
| Kubernetes/容器平台 | 一个容器一个进程,多副本扩容 |
| 需要可靠后台任务 | 单独队列 Worker,不靠 Cluster |
| 需要 TLS、域名、反向代理 | Nginx 或平台负载均衡器 |
Nginx 不管理 Node.js 生命周期
Nginx 可以把请求转发给 Node.js,并处理 TLS、超时、请求体大小等入口问题,但它不会负责重启崩溃的 Node.js 进程。PM2、systemd、Docker 或 Kubernetes 才负责进程生命周期。
upstream learning_api {
server 127.0.0.1:8080;
}
server {
listen 80;
location / {
proxy_pass http://learning_api;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
Express 若根据代理头判断客户端 IP 或 HTTPS,需要谨慎设置 trust proxy,只信任真实代理链,避免客户端伪造转发头。
优雅重载与健康检查
滚动重载期新旧实例会短暂共存,资源预算必须考虑峰值。一个可靠流程是:
启动新实例
→ 初始化必要资源
→ Server listening
→ readiness 通过
→ 停止向旧实例发送新请求
→ 旧实例等待活动请求
→ 关闭 DB/Redis/子进程
→ 在 kill_timeout 前退出
健康检查应区分:
- Liveness:进程是否需要重启;
- Readiness:当前实例是否可以接收流量。
不要让健康接口每次执行昂贵的全库查询,也不要在初始化未完成时提前报告就绪。
Signal 与 PM2
PM2 停止或重载应用时会触发停机流程,并在超时后强制结束。应用要监听部署环境实际发送的 Signal,停止接受新请求,清理资源,并让进程自然退出。
let stopping = false;
async function shutdown(): Promise<void> {
if (stopping) return;
stopping = true;
await stopAcceptingRequests();
await closeExternalResources();
}
process.once("SIGINT", () => void shutdown());
process.once("SIGTERM", () => void shutdown());
kill_timeout 必须大于应用正常清理需要的时间,同时应用内部应有更短的强制收尾期限。不要在 Signal 回调第一行调用 process.exit()。
NVM、PATH 与生产启动
交互终端里 node、python 能运行,不代表 PM2 环境也有相同 PATH:
- PM2 守护进程可能在旧 Node.js 版本下启动;
- NVM 切换版本后需要确认 PM2 使用的解释器;
- systemd/PM2 环境变量通常少于登录 Shell;
- 子进程执行第三方工具时应记录解析后的版本和启动错误。
排查时记录 process.execPath、process.version、process.cwd() 和脱敏后的关键 PATH 信息。
状态必须外置
无论 PM2、Cluster 还是 Kubernetes,多实例都意味着进程内状态不可靠:
Session/共享缓存 → Redis 或数据库
任务状态 → 数据库或队列
上传和结果文件 → 共享存储/对象存储或明确路由
全局限流 → Redis/网关
定时任务 → 独立调度器或分布式协调
Cluster 和 PM2 都不能替代后台任务队列。队列负责持久化、确认、重试、延迟任务和跨机器消费。
跨平台注意事项
- Nginx 与 PM2 主要用于 Linux 生产环境;Windows 开发环境的 Signal 和守护方式不同。
- Windows 下 PM2 行为和开机启动方案应单独验证,不能照搬 systemd 教程。
- 容器中应让 Node.js 正确收到 Signal,关注 PID 1 和入口脚本是否转发 Signal。
- PowerShell、Bash 下环境变量写法不同,部署配置应显式管理。
常见错误
- PM2 Cluster Mode 与应用内 Cluster 同时按 CPU 数扩容。
- PM2 直接运行 TypeScript,却没有固定 TypeScript 运行器和版本。
- Worker/实例增加后不重算数据库连接池。
- 把 Nginx 502 当成唯一根因;它通常只是上游不可用的表现。
- 重载期间新旧实例共存,却按稳态实例数计算资源。
- 依赖进程内 Session、缓存、限流或任务状态。
kill_timeout很短,导致日志、请求和子进程被截断。
练习
- 为当前 Express 项目写出两套部署草图:PM2 Cluster Mode;PM2 fork + 应用内 Cluster。
- PM2 两实例、应用内四 Worker、每 Worker 连接池 15,计算总连接上限并判断风险。
- 设计
/health/live和/health/ready的职责,不需要接入真实数据库。 - 模拟滚动重载,列出新旧实例同时存在时会倍增的资源。
验收清单
- [ ] 能说明 Nginx、PM2 和 Cluster 各自职责。
- [ ] 默认只选择一层 Node.js 多实例管理。
- [ ] 生产环境优先运行编译后的 JavaScript。
- [ ] 滚动重载资源预算包含新旧实例共存。
- [ ] Signal、readiness 和
kill_timeout形成完整停机流程。 - [ ] 多实例共享状态已经外置。
- [ ] 后台任务使用队列或独立 Worker,而不是依赖 Cluster。