Node.js 多进程与 PM2 进程管理实战

完整讲解 Node.js 多进程架构:cluster 模块与主从模型、round-robin 负载均衡、PM2 进程守护与热重载、集群模式扩展、优雅重启与零停机发布,以及多进程时代的日志与运维实践。

单机单进程扛不住流量时,Node.js 的第一个台阶就是多进程:cluster 模块把事件循环复制 N 份共享同一端口,PM2 再把「起停、守护、重启、日志」变成一条命令。本文从 V8 单线程模型讲起,覆盖 cluster 主从架构、负载均衡、PM2 全套进程管理,最后给出优雅重启与零停机发布的落地方案。

1. 单线程模型与多进程价值

1.1 为什么单进程会卡

Node.js 的主线程只有一个事件循环。同步阻塞、CPU 密集计算都会把事件循环占死,导致所有请求排队:

// 这段 JSON 解析会阻塞 3 秒,期间所有请求全部挂起
const data = JSON.parse(readBigFileSync('/tmp/huge.json'));

更隐蔽的是正则灾难回溯、加密哈希、图像缩放这类「吃 CPU」的操作。它们在单线程下是灾难,在多进程下可以被并行消化。

1.2 多进程的收益

收益说明
吞吐翻倍每个进程独立事件循环,互不阻塞
CPU 核并行每进程占一核,N 核机器跑 N 份
故障隔离单进程崩溃由守护拉起,服务整体存活
部署演进集群 + 重启,天然贴近 K8s 多副本模型

一句话:Node 单线程的软肋是 CPU 密集与阻塞;多进程 = 多个独立事件循环并行消化请求,是单机扩容的第一级。IO 密集收益巨大,CPU 密集需 worker_threads,内存状态型要配合共享存储。


2. cluster 模块与主从架构

2.1 主进程与工作进程

cluster 的模式是主从(master-worker):主进程负责 fork 与调度,工作进程各自跑业务代码并共享监听同一端口:

import cluster from 'node:cluster';
import http from 'node:http';
import { cpus } from 'node:os';

if (cluster.isPrimary) {
  const count = cpus().length; // 主进程:按 CPU 核数 fork
  console.log(`主进程 ${process.pid} 启动,fork ${count} 个 worker`);
  for (let i = 0; i < count; i++) cluster.fork();
  cluster.on('exit', (worker) => {
    console.log(`worker ${worker.process.pid} 退出`);
    cluster.fork(); // 崩溃自动拉起新 worker
  });
} else {
  // 工作进程:业务代码,各进程共享同一端口
  http.createServer((req, res) => {
    res.end(`worker ${process.pid} 处理 ${req.url}`);
  }).listen(3000);
}

2.2 工作进程间通信

工作进程之间不能直接通信,必须经过主进程 IPC:

// worker 内发消息给主进程
process.send({ type: 'report', pid: process.pid });

// 主进程广播给所有 worker
for (const id in cluster.workers) {
  cluster.workers[id].send({ type: 'shutdown-now' });
}

2.3 共享端口原理

主进程先创建 socket 监听 3000
每个 worker fork 时继承该 socket 的文件描述符
内核把连接分发给不同 worker

一句话:cluster = 主进程负责 fork/调度,N 个 worker 共享同一端口各自独立跑;崩溃自动拉起,是进程守护的原生雏形。


3. 负载均衡策略

3.1 默认 round-robin

非 Windows 平台 cluster 默认用 round-robin 轮询分发连接:

连接 A → worker 1
连接 B → worker 2
连接 C → worker 3
连接 D → worker 1(循环)

3.2 切换调度策略

cluster.schedulingPolicy = cluster.SCHED_RR 在 fork 前指定轮询,SCHED_NONE 则交给操作系统分发。

策略优点缺点
SCHED_RR负载均衡精确主进程成为调度中心,长连接损耗
SCHED_NONE内核分发,性能好各 worker 负载可能不均

3.3 会话粘滞问题

WebSocket、登录态等有状态场景,轮询会把同一用户打到不同 worker。需要粘滞会话(sticky session):按 IP 或 cookie 哈希定向分发。

hash(clientId) % workerCount → 固定的 worker

一句话:默认 round-robin 公平分发;无状态 API 用它,WebSocket/登录态要上粘滞会话按哈希定向。


4. PM2 基础与生态配置

4.1 安装与启动

npm i -g pm2            # 安装
pm2 start app.js        # 启动单进程
pm2 start app.js -i 4   # 启动 4 个进程(集群模式)
pm2 status              # 查看状态
pm2 stop app && pm2 delete app   # 停止并从 PM2 移除

4.2 ecosystem.config.js 声明式配置

// ecosystem.config.js
module.exports = {
  apps: [{
    name: 'web-api',
    script: './dist/index.js',
    instances: 'max',            // 自动按 CPU 核数
    exec_mode: 'cluster',        // cluster 模式(非 fork)
    max_memory_restart: '512M',  // 内存超限自动重启
    autorestart: true,           // 崩溃自动重启
    env: { NODE_ENV: 'production', PORT: 3000 },
  }],
};
pm2 start ecosystem.config.js

4.3 常用运维命令

pm2 logs web-api        # 实时日志(可加 --lines 200)
pm2 monit              # 终端监控面板
pm2 reload web-api     # 零停机重载(仅集群模式)
pm2 restart web-api    # 有停机的重启
pm2 save               # 保存进程列表,开机自启
pm2 startup            # 生成开机自启脚本

一句话:PM2 = 一行起 N 个进程 + 崩溃自动拉起 + 内存超限重启 + 统一日志;用 ecosystem.config.js 把配置版本化进仓库。


5. 进程守护与热重载

5.1 守护机制

PM2 本身就是独立守护进程,业务进程死了它立刻拉起:

PM2 Daemon(守护)
 ├─ worker 1(业务)  崩溃 → 立即 fork 新 worker
 ├─ worker 2(业务)
 └─ worker 3(业务)
// 配置节流:1 分钟内最多重启 10 次,避免疯狂重启
module.exports = {
  apps: [{
    name: 'web-api',
    script: './dist/index.js',
    instances: 2,
    max_restarts: 10,          // 1 分钟内最大重启次数
    restart_delay: 3000,       // 重启间隔 3 秒
    exp_backoff_restart_delay: 200, // 指数退避重启
  }],
};

5.2 watch 热重载

module.exports = {
  apps: [{
    name: 'web-api',
    script: './src/index.js',
    watch: ['src'],            // 监听目录变化自动重启
    ignore_watch: ['node_modules', 'dist'],
    watch_delay: 1000,         // 防抖 1 秒
  }],
};

生产环境慎用 watch:文件写入一半触发重启会导致服务中途掉线,尽量依赖发布平台触发 reload。

一句话:守护 = 自动拉起 + 重启节流(max_restarts + exp_backoff),热重载 = watch 目录变化自动重启,开发可用、生产要交给 reload。


6. 集群模式与优雅重启

6.1 零停机重载

pm2 reload 是集群模式的杀手锏:逐 worker 重启,新 worker 就绪再杀旧 worker:

pm2 reload web-api

worker1 停止旧进程、启动新进程、健康后,worker2/worker3 依次滚动,整个过程中始终有 worker 在服务,客户端无感。

6.2 优雅退出与 kill_timeout

要让 reload 真正做到零停机,业务侧要主动释放而不是被动被杀:

// 业务代码里监听退出信号,先排空再退出
process.on('SIGINT', () => {
  server.close(() => process.exit(0)); // 停止接收新连接,排空在途
});
// PM2 侧配合
module.exports = {
  apps: [{
    name: 'web-api',
    script: './dist/index.js',
    instances: 2,
    kill_timeout: 5000,       // 给 worker 5 秒处理退出
    listen_timeout: 3000,     // 等新 worker 监听就绪
    wait_ready: true,         // 业务主动发 ready 信号
  }],
};

业务侧就绪后再发信号,避免「进程已监听但依赖未就绪」的流量黑洞:

process.send('ready'); // 依赖初始化完成后通知 PM2

6.3 优雅发布流程

git pull → npm ci → 构建 dist
pm2 reload web-api          # 逐实例滚动,零停机
pm2 save                    # 保存最新列表
curl 健康检查端点            # 验证全绿

一句话:零停机 = pm2 reload 逐 worker 滚动 + 业务监听 SIGINT 主动排空 + wait_ready 就绪信号,三者缺一不可。


7. 日志管理与运维实践

7.1 日志分流

PM2 默认把 stdout 与 stderr 分开落盘,避免互相污染:

module.exports = {
  apps: [{
    name: 'web-api',
    script: './dist/index.js',
    out_file: '/var/log/web-api/out.log',    // stdout
    error_file: '/var/log/web-api/err.log',  // stderr
    log_file: '',                            // 禁用合并日志
    merge_logs: true,                        // 多实例日志合并
    time: true,                              // 每行加时间戳
  }],
};

7.2 日志轮转

生产日志必须轮转,否则磁盘被撑爆:

pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 50M
pm2 set pm2-logrotate:retain 7       # 保留 7 份
pm2 set pm2-logrotate:compress true
pm2 set pm2-logrotate:rotateInterval '0 0 * * *'   # 每天零点

7.3 进程健康度监控

pm2 monit   # 交互式面板:CPU、内存、重启次数
指标关注点报警阈值
内存是否缓慢爬升(泄漏信号)超过 max_memory_restart
重启次数是否频繁崩溃1 分钟内 > 5
CPU是否异常满载持续 > 90%

一句话:多进程日志 = out/err 分流 + 时间戳 + 轮转压缩,健康度看 pm2 monit 的三个指标:内存、重启次数、CPU。


8. 踩坑清单

坑现象对策
fork 模式跑 cluster 代码主进程逻辑被执行 N 遍isPrimary 判断
进程内维护内存状态用户数据在不同 worker 间漂移状态放 Redis,见消息队列
调度策略理解错worker 负载不均无状态用 SCHED_RR
WebSocket 被轮询打散连接频繁断开粘滞会话按 IP/cookie 哈希
watch 热重载触发重启生产中服务掉线生产禁用 watch 用 reload
业务不监听退出信号reload 时在途请求被强杀监听 SIGINT + server.close
日志不轮转磁盘被日志撑爆pm2-logrotate
疯狂崩溃重启CPU 100% 空转max_restarts + exp_backoff
端口被占用EADDRINUSE确认旧实例已 delete

9. 总结

环节要点
单线程一个事件循环,CPU 密集会卡住全部请求
cluster主从架构,worker 共享端口,崩溃自动拉起
调度round-robin 默认;有状态用粘滞会话
PM2 启动-i max + ecosystem 声明式配置
守护自动拉起 + max_restarts 节流
热重载watch 开发用,reload 生产用
零停机reload 逐 worker + SIGINT 排空 + wait_ready
日志out/err 分流 + 轮转 + monit 看健康度

一句话记住:多进程是 Node 单机的扩容底座,PM2 是把「起停、守护、滚动重启、日志」全部标准化的引擎——先用 cluster 想清楚进程模型,再让 PM2 接管生命周期,最后用 reload 打通零停机发布。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「nodejs」更多文章

  1. Node.js 优雅停机与健康检查实战
  2. Node.js 内存泄漏诊断实战
  3. BullMQ 后台任务队列实战