作者:互联网 时间: 2026-07-01 09:30:52
Node 的单进程模型很好理解,也很好踩坑

你在一台 8 核机器上启动一个 Express 服务,如果没有额外处理,真正跑业务 JS 的就是一个进程。CPU 一个核心打满了,旁边几个核心还在看戏。线上流量一上来,表现就很怪:机器总体 CPU 看着不高,接口 p99 却开始抖
这篇讲 Node.js 多进程和 Cluster。环境口径:Node.js 24、Express 5.2.1。先用 Node 官方 cluster 模块写一个能跑的版本,再说它的边界,以及为什么生产里很多团队最后会交给 PM2、systemd、Docker 或 Kubernetes 管进程
Node 官方文档对 cluster 的描述很直白:它可以创建一组子进程,这些子进程共享服务器端口
这里的关键词是进程
Cluster 不是线程池,也不是把一个请求拆给多个核心一起算。它的做法更像是:主进程负责拉起 worker,多个 worker 都监听同一个端口,请求来了之后分发给其中一个 worker 处理
这带来两个直接好处
多核 CPU 能用起来
某个 worker 崩了,主进程可以拉一个新的
代价也很明确:进程之间内存不共享。你放在内存里的登录态、计数器、缓存,每个 worker 都有自己的一份。只要上了多进程,就不要再把进程内存当成全局真相
先装 Express
mkdir node-cluster-democd node-cluster-demonpm init -ynpm 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.availableParallelism())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 http://localhost:${ port}`)})}
启动:
node server.js
多请求几次:
curl http://localhost:3000/healthcurl http://localhost:3000/healthcurl http://localhost:3000/health
你会看到返回的 pid 可能不同,说明请求落到了不同 worker 上
这里用 os.availableParallelism() 而不是老习惯里的 os.cpus().length。Node 官方在 os.cpus() 文档里也提醒过,不应该用它来计算应用可用并行度,推荐用 os.availableParallelism()
上面的代码能跑,但离生产还差几步。至少要处理优雅退出
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 = 0app.post('/count', (req, res) => { counter = 1res.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 不是性能银弹。它只是让你把请求分散到多个进程。某个接口本身写得很重,多进程能缓解,但不能治本
上了 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 正在泄漏
如果你只是想理解原理,Node 内置 cluster 很适合
如果你要生产部署,我会按环境选
传统服务器,可以用 PM2 的 cluster mode 或 systemd 管理多个进程。PM2 省心,日志、重启、进程列表都有现成命令。systemd 更贴近系统层,适合不想引入额外 Node 进程管理工具的团队
Docker / Kubernetes 环境,我更倾向于一个容器一个 Node 进程,然后用多个副本扩容。进程重启、健康检查、滚动发布、日志采集都交给平台。你也可以在容器内再开 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.availableParallelism() 与 os.cpus() 的使用建议,采集于 2026-06-29 Node.js process 官方文档:SIGTERM、进程事件与退出处理,采集于 2026-06-29 npm 元数据:[email protected],采集于 2026-06-29