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 ─┘

最小 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.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
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 崩溃 → 记录原因 → 判断是否计划退出
                       ├─ 是:不重启
                       └─ 否:退避后重启,超过阈值报警

跨平台注意事项

常见错误

练习

  1. 启动两个 Worker,让接口返回 PID,连续访问并观察请求分布。
  2. 在一个 Worker 中主动抛出异常,记录 disconnectexit 的顺序。
  3. 实现“10 秒内最多重启 3 次”的简单保护策略。
  4. 分别尝试 SCHED_RR 与默认策略,解释为什么请求数不一定完全相等。

验收清单