16. Cluster 工作原理
本章解决的问题
Cluster 让一个 Primary 进程管理多个 Node.js Worker 进程,使这些 Worker 能共同提供网络服务。它解决的是“单台机器上运行多个 Node.js 服务进程”的问题,不会自动共享内存状态,也不等于任务队列。
Primary 与 Worker
Primary PID 1000
├─ Worker 1 PID 1001 ─┐
├─ Worker 2 PID 1002 ─┼─ 共同服务 :8080
└─ Worker 3 PID 1003 ─┘
- Primary:创建、监控、重启和停止 Worker,通常不处理 HTTP 业务。
- Worker:重新执行应用入口,创建 Server、处理请求。
- 每个 Worker:独立 PID、V8 堆、Event Loop、模块缓存和连接池。
最小 TypeScript 示例
import * as cluster from "node:cluster";
import http from "node:http";
import os from "node:os";
const port = 8080;
if (cluster.isPrimary) {
const workerCount = Math.min(2, os.availableParallelism());
console.log(`Primary PID=${process.pid}`);
for (let index = 0; index < workerCount; index += 1) {
cluster.fork();
}
cluster.on("exit", (worker, code, signal) => {
console.error("Worker 退出", {
workerId: worker.id,
pid: worker.process.pid,
code,
signal,
});
});
} else {
const server = http.createServer((_request, response) => {
response.end(`worker=${cluster.worker?.id}, pid=${process.pid}\n`);
});
server.on("error", (error) => {
console.error("监听失败:", error.message);
process.exitCode = 1;
});
server.listen(port, () => {
console.log(`Worker ${cluster.worker?.id} 正在监听 ${port}`);
});
}
TypeScript 的 import 会按当前 tsconfig.json 转为 CommonJS。用 ts-node server.ts 学习时,各 Worker 需要继承能运行 TypeScript 的启动配置;生产部署更推荐编译后运行 JavaScript。
同一个端口如何被多个 Worker 使用
Worker 调用 server.listen(8080) 时,Cluster 会协调监听资源。常见默认策略由 Primary 接收连接后按策略分发给 Worker;另一种策略让操作系统参与连接分配。这里共享的是网络监听能力,不是任意端口、普通变量或业务对象。
cluster.schedulingPolicy = cluster.SCHED_RR;
调度策略常量:
cluster.SCHED_RR:Primary 以轮转方式分发连接,Windows 除外的常见默认策略;cluster.SCHED_NONE:交由操作系统调度,可能导致连接分布不均。
应在 cluster.fork() 前设置策略。平台和 Node.js 版本可能影响默认值,不要把“每个 Worker 请求数绝对相同”当作保证;Keep-Alive、WebSocket 和长连接会进一步影响分布。
高频 API
| API/属性 | 角色 | 用途 |
|---|---|---|
cluster.isPrimary |
两者 | 判断当前是否 Primary |
cluster.isWorker |
两者 | 判断当前是否 Worker |
cluster.fork(env?) |
Primary | 创建 Worker |
cluster.workers |
Primary | 获取 Worker 映射 |
cluster.worker |
Worker | 获取当前 Worker |
cluster.setupPrimary() |
Primary | 设置 Worker 执行文件和启动参数 |
cluster.disconnect() |
Primary | 断开所有 Worker,等待连接关闭 |
worker.send() |
Primary | 向指定 Worker 发送 IPC 消息 |
worker.disconnect() |
Primary | 断开 Worker 的 IPC/Server |
worker.kill() |
Primary | 向 Worker 发送终止信号 |
高频事件和时间线
Primary 可观察:
fork → online → listening → message ... → disconnect → exit
fork:已经创建 Worker 对象;online:Worker 已开始运行 Node.js,不代表 HTTP 已监听;listening:Worker 的 Server 已监听;message:收到 Worker IPC 消息;disconnect:IPC 通道断开;exit:Worker 进程退出。
cluster.on("listening", (worker, address) => {
console.log("Worker 已就绪", worker.id, address);
});
cluster.on("message", (worker, message: unknown) => {
console.log("来自 Worker 的消息", worker.id, message);
});
即使消息来自自己的 Worker,也应按 unknown 做运行时校验。
Worker 崩溃与重启
最简单的 exit 回调里立即 cluster.fork(),可能产生每秒重启数百次的崩溃循环。生产实现至少要:
- 区分计划退出和异常退出;
- 记录最近重启时间与次数;
- 使用退避策略;
- 达到阈值后停止重启并报警;
- 先修复导致所有 Worker 必然崩溃的配置问题。
Worker 崩溃 → 记录原因 → 判断是否计划退出
├─ 是:不重启
└─ 否:退避后重启,超过阈值报警
跨平台注意事项
- 默认调度策略在 Windows 和其他平台可能不同。
SIGTERM、SIGKILL等 POSIX Signal 在 Windows 上不能照搬。- 多进程日志顺序不保证严格按时间排列,应带 PID、Worker ID 和时间戳。
- Cluster 是单机能力,多台主机仍需负载均衡器和外部共享存储。
常见错误
- 在 Primary 和 Worker 分支外无条件启动 HTTP Server。
- 认为 Worker 会共享 Primary 的内存缓存。
- 使用已废弃的
cluster.isMaster、setupMaster();Node.js 22+ 应使用isPrimary、setupPrimary()。 - 在
online事件就宣布服务已经能接收请求。 - Worker 一退出立即无限重启。
- Worker 数量机械设置为所有逻辑 CPU,忽略内存、连接池和同机其他服务。
练习
- 启动两个 Worker,让接口返回 PID,连续访问并观察请求分布。
- 在一个 Worker 中主动抛出异常,记录
disconnect、exit的顺序。 - 实现“10 秒内最多重启 3 次”的简单保护策略。
- 分别尝试
SCHED_RR与默认策略,解释为什么请求数不一定完全相等。
验收清单
- [ ] 能解释 Primary 与 Worker 的职责。
- [ ] 知道多个 Worker 共享的是监听能力,不是内存变量。
- [ ] 能区分
online、listening、disconnect和exit。 - [ ] 能使用 Node.js 22+ 的
isPrimary和setupPrimary()名称。 - [ ] Worker 重启有退避、阈值和计划退出判断。
- [ ] 日志包含 PID 和 Worker ID。