Node.js多进程和Cluster集群部署,别让一个进程扛完整台机器
Node.js 的单进程模型在理解上非常直观,但在生产环境中踩坑的姿势也相当丰富。

想象一下:你在一台 8 核机器上运行一个 Express 服务,没有做任何特殊处理。真正干活的 JavaScript 引擎只有一个进程。CPU 一号核心已经累垮了,而旁边的几个核心却悠闲得像在围观。线上流量一旦上来,表象就很诡异:机器整体 CPU 看起来并不高,但接口的 p99 延迟已经开始剧烈抖动。
这篇文章专门讲解 Node.js 的多进程方案和 Cluster 集群部署。环境采用 Node.js 24 与 Express 5.2.1。我们先使用 Node 官方 cluster 模块搭建一个可运行的示例,然后讨论它的边界在哪里,以及为什么生产环境中很多团队最终选择使用 PM2、systemd、Docker 或 Kubernetes 来管理进程。
Cluster 解决的核心问题
Node 官方文档对 cluster 的描述非常直白:它能够创建一组子进程,并且这些子进程可以共享同一个服务器端口。
这里的关键词是——进程。
Cluster 不是线程池,也不是把一个请求拆给多个核心并行计算。它的工作方式更像这样:主进程负责启动 worker,多个 worker 都监听同一个端口,请求进入后,由主进程分发给其中一个 worker 去处理。
这样做直接带来两个好处:
- 多核 CPU 终于能被充分利用起来。
- 某个 worker 崩溃后,主进程可以立刻启动一个新的。
代价也很现实:进程之间内存不共享。你存放在内存里的登录态、计数器、缓存,每个 worker 都各自有一份。一旦上了多进程,就千万别再将进程内存当作全局真理。
一个最小可运行的 Cluster 服务
首先安装 Express:
mkdir node-cluster-demo
cd node-cluster-demo
npm init -y
npm install express
server.js:
const cluster = require('node:cluster')
const os = require('node:os')
const process = require('node:process')
const express = require('express')
const port = Number(process.env.PORT || 3000)
const workerCount = Number(process.env.WORKERS || os.a vailableParallelism())
if (cluster.isPrimary) {
console.log(`primary ${ process.pid} is running`)
console.log(`starting ${ workerCount} workers`)
for (let i = 0; i < workerCount; i ) {
cluster.fork()
}
cluster.on('exit', (worker, code, signal) => {
console.error(`worker ${ worker.process.pid} died`, {code, signal })
cluster.fork()
})
} else {
const app = express()
app.get('/health', (req, res) => {
res.json({ ok: true, pid: process.pid,})
})
app.get('/cpu', (req, res) => {
const startedAt = Date.now()
while (Date.now() - startedAt < 80) {
Math.sqrt(Math.random())
}
res.json({ ok: true, pid: process.pid,})
})
app.listen(port, () => {
console.log(`worker ${ process.pid} listening on https://localhost:${ port}`)
})
}
启动服务:
node server.js
多请求几次:
curl https://localhost:3000/health
curl https://localhost:3000/health
curl https://localhost:3000/health
你会看到返回的 pid 可能不同——说明请求被分配到了不同的 worker 进程上。
这里特意用了 os.a vailableParallelism(),而不是以前习惯的 os.cpus().length。Node 官方在 os.cpus() 文档里也曾提醒过,不应拿它来计算应用可用并行度,推荐直接使用 os.a vailableParallelism()。
生产代码不能只会 fork
上面的代码能运行,但离生产环境还有好几步。至少需要处理优雅退出:
function createApp() {
const app = express()
app.get('/health', (req, res) => {
res.json({ok: true, pid: process.pid })
})
return app
}
if (cluster.isPrimary) {
for (let i = 0; i < workerCount; i ) {
cluster.fork()
}
cluster.on('exit', (worker, code, signal) => {
if (worker.exitedAfterDisconnect) { return }
console.error(`worker ${ worker.process.pid} crashed`, {code, signal })
cluster.fork()
})
} else {
const app = createApp()
const server = app.listen(port)
process.on('SIGTERM', () => {
server.close(() => {
process.exit(0)
})
setTimeout(() => {
process.exit(1)
}, 10_000).unref()
})
}
server.close() 会停止接收新连接,等已有的连接处理完成后再退出。外面再加一个 10 秒的兜底,是为了防止某些长连接或异常请求导致进程永远退不掉。
主进程里也别无脑重启所有退出的 worker。worker.exitedAfterDisconnect 这个属性可以帮助你区分“主动要求它退出”和“它意外崩溃”。这在滚动重启时尤其有用。
状态别放在进程内存里
这是 Cluster 最容易踩坑的地方。
假设你写了一个简单计数器:
let counter = 0
app.post('/count', (req, res) => {
counter = 1
res.json({counter, pid: process.pid })
})
单进程时它看起来没问题。多进程后,每个 worker 都有自己的 counter。请求打到 worker A,counter 是 10;下一个请求打到 worker B,counter 可能是 3。你以为自己写了全局计数器,实际上写了 N 个局部计数器。
登录态也一样。不要把 session 存在进程内存里,然后指望 Cluster 替你同步。应该放到 Redis、数据库,或者使用无状态 token。进程内缓存也要接受一个现实:每个 worker 都会各自缓存一份,命中率和内存占用都会受到影响。
曾经见过一个后台系统,上了 Cluster 之后验证码偶发校验失败。原因很朴实:验证码存在内存 Map 里,生成请求落到 worker 1,校验请求落到 worker 3,自然找不到了。
负载均衡不是魔法
Node cluster 支持两种分发方式。官方文档提到,除 Windows 外,默认是 round-robin,由主进程接收连接再分发给 worker。另一种方式是主进程创建监听 socket 后交给 worker,由 worker 自己 accept。
日常业务一般不用手动修改 cluster.schedulingPolicy。真正需要关心的是:worker 之间的负载可能仍然不均匀。
原因有很多:
- 请求耗时不同,慢请求会长期占用某个 worker
- 长连接(比如 WebSocket)会让连接长时间停留在某个 worker 上
- CPU 密集逻辑会阻塞单个 worker 的 event loop
所以 Cluster 并不是性能银弹。它只是让你把请求分散到多个进程。如果某个接口本身写得很重,多进程能缓解,但不能从根本上解决问题。
多进程下日志和监控要带 pid
上了 Cluster 之后,日志里必须带上 pid 或 worker id:
logger.info('request finished', {
pid: process.pid,
workerId: cluster.worker?.id,
method: req.method,
path: req.originalUrl,
status: res.statusCode,
})
否则你看到一段错误日志,很难判断是不是某一个 worker 持续出问题。比如 worker 4 内存一直上涨,最后反复重启。总览日志里只看到“进程重启了”,却看不到是哪一个,定位起来非常困难。
监控也一样。除了进程整体指标,我建议给每个 worker 都打上:
- RSS、heapUsed
- event loop delay
- 请求数和错误数
- 重启次数
多进程系统最怕平均值。平均 CPU 不高,不代表每个 worker 都健康;平均内存正常,也可能其中一个 worker 正在泄漏。
Cluster 和 PM2、容器怎么取舍
如果只是想理解原理,Node 内置 cluster 完全够用。
如果是生产部署,我的建议是按环境选择:
- 传统服务器:可以用 PM2 的 cluster mode 或 systemd 管理多个进程。PM2 省心,日志、重启、进程列表都有现成命令。systemd 更贴近系统层,适合不想引入额外 Node 进程管理工具的团队。
- Docker / Kubernetes 环境:我更倾向于一个容器只跑一个 Node 进程,然后通过多个副本来扩容。进程重启、健康检查、滚动发布、日志采集都交给平台。也可以在容器内再开 Cluster,但这样会让资源限制、优雅退出、监控粒度变得复杂。不是不能做,只是要有明确的理由。
- 本地开发和小型内网服务:直接用 cluster 也够。但别把它当成完整的进程平台——它不负责日志采集、发布编排、健康检查、限流熔断,也不会替你处理共享状态。
什么场景不适合只靠 Cluster
下面几种情况,纯 Cluster 往往搞不定:
- CPU 重任务:Cluster 能把请求分到多个进程,但单个请求里的 CPU 计算还是会堵住对应 worker。重计算更适合 Worker Threads、任务队列,或者拆成独立服务。
- 大量 WebSocket 长连接:连接会固定在某个 worker 上,负载均衡和会话管理需要额外设计。多实例部署时还要考虑消息广播和房间状态。
- 强依赖本地内存状态的应用:比如用本地 Map 存 session、验证码、临时任务状态。先把状态外置,再谈多进程。
- 超短任务但日志极重的服务:进程多了,日志竞争和 IO 压力也会上来。这时要先控制日志量。
收尾
Cluster 的价值很朴素:让 Node 服务用上多核,并且给 worker 崩溃后的恢复留一个入口。
但它也会逼你面对几个工程事实:
- 进程内存不是共享状态
- 日志和监控必须能区分 worker
- 优雅退出要自己处理
- 多进程只能缓解单进程瓶颈,不能修好糟糕的同步代码
我的建议是:先用 cluster 把多进程模型跑明白,再根据部署环境决定交给谁管理。传统机器上用 PM2 或 systemd,容器环境交给 Kubernetes 或编排平台。无论选择哪条路,都别让一个 Node 进程孤零零扛完整台机器。
参考来源
- Node.js cluster 官方文档:cluster 工作模型、
cluster.isPrimary、cluster.fork()、exit事件、调度策略,采集于 2026-06-29 - Node.js os 官方文档:
os.a vailableParallelism()与os.cpus()的使用建议,采集于 2026-06-29 - Node.js process 官方文档:
SIGTERM、进程事件与退出处理,采集于 2026-06-29 - npm 元数据:
express@5.2.1,采集于 2026-06-29
