单机单进程扛不住流量时,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 打通零停机发布。
延伸阅读
- Node.js 异步与并发 — 事件循环与并发模型
- Node.js 消息队列实践 — 多进程间共享状态与解耦
- Node.js 可观测性 — 多进程链路追踪与指标
- Node.js 设计模式 — 多进程架构与模式
- Node.js 性能调优 — 瓶颈定位与压测
- Node.js Docker 与 Kubernetes — 多进程在容器化环境的使用
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。