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

随之放大的还有:

除非经过明确容量设计,通常只选择一层负责 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 前退出

健康检查应区分:

不要让健康接口每次执行昂贵的全库查询,也不要在初始化未完成时提前报告就绪。

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 与生产启动

交互终端里 nodepython 能运行,不代表 PM2 环境也有相同 PATH:

排查时记录 process.execPathprocess.versionprocess.cwd() 和脱敏后的关键 PATH 信息。

状态必须外置

无论 PM2、Cluster 还是 Kubernetes,多实例都意味着进程内状态不可靠:

Session/共享缓存 → Redis 或数据库
任务状态         → 数据库或队列
上传和结果文件   → 共享存储/对象存储或明确路由
全局限流         → Redis/网关
定时任务         → 独立调度器或分布式协调

Cluster 和 PM2 都不能替代后台任务队列。队列负责持久化、确认、重试、延迟任务和跨机器消费。

跨平台注意事项

常见错误

练习

  1. 为当前 Express 项目写出两套部署草图:PM2 Cluster Mode;PM2 fork + 应用内 Cluster。
  2. PM2 两实例、应用内四 Worker、每 Worker 连接池 15,计算总连接上限并判断风险。
  3. 设计 /health/live/health/ready 的职责,不需要接入真实数据库。
  4. 模拟滚动重载,列出新旧实例同时存在时会倍增的资源。

验收清单