人人都会AI编程

10.3 集群模式:cluster 模块

更新时间:2026-07-11

Node.js 的单线程事件循环模型天然适合 I/O 密集型任务,但是它默认只会运行在一个 CPU 核心上。在一台 8 核甚至更多核的服务器上,单个 Node.js 进程最多只能利用一个核心的计算能力,其余核心处于闲置状态。对于流量较大的线上服务,这显然是一种浪费。为此,Node.js 提供了 cluster 模块,允许我们轻松创建共享同一端口的多个子进程,从而充分利用多核 CPU。

10.3.1 集群模式的核心思想:主从多进程

cluster 模块会创建一个主进程(Master)和若干个工作进程(Worker)。主进程负责管理 Worker 的生命周期,但并不直接处理业务请求;工作进程实际接受网络连接、执行业务逻辑并返回响应。每个 Worker 都是一个独立的 Node.js 进程,拥有自己的 V8 实例、内存空间和事件循环。

这种方式本质上是通过进程复制来实现横向扩展。对于大部分 I/O 密集型应用,将 Worker 数量设置为 CPU 核心数是一个经验性的起点,它能确保事件循环不会被单核处理能力所限。

10.3.2 多进程如何共享同一个端口

在传统的多进程服务器模型中,如果需要多个进程监听同一个 TCP 端口,通常会比较棘手:端口只能被一个进程独占。开发者往往需要引入反向代理(如 Nginx)来分发流量。但 Node.js 的 cluster 模块巧妙地解决了一种特殊场景:多个 Worker 进程可以监听同一个端口,而不会产生端口冲突

实现原理可以简述如下:

  • 在主进程中调用 cluster.fork() 时,Node.js 内部会创建一个服务器句柄(socket),并将该句柄通过 IPC 通道传递给子进程。
  • 当 Worker 启动并调用 http.createServer().listen() 时,如果发现这是通过 cluster 启动的 Worker,其实并没有真的在 Worker 进程内部创建新的文件描述符去绑定端口,而是直接使用从主进程传递下来的那个共享句柄。
  • 多个 Worker 都可以对这个共享句柄 accept 新的连接。操作系统内核负责将进入的连接分发给当前正在等待 accept 的某个 Worker。

最终效果就是:虽然看起来每个 Worker 都在监听同一个端口,但底层只有一个 socket 被真正绑定到端口上,所有 Worker 都共享这个 socket。这既保证了多进程处理能力,又避免了复杂的 IPC 转发逻辑,同时也保持了编程模型的一致性——每个 Worker 的代码就像单进程服务一样编写。

值得注意的是,这个共享机制仅在 Worker 监听的是同一个端口且使用 cluster 模块时才自动生效。如果 Worker 直接绑定不同的端口,或者使用了外部反向代理,则与 cluster 的内置端口共享无关。

10.3.3 负载调度策略

对于接入的连接如何分发给 Worker,这是 cluster 负载均衡的核心。Node.js 提供了两种可配置的调度策略,通过环境变量 NODE_CLUSTER_SCHED_POLICYcluster.schedulingPolicy 设置:

  1. 轮询调度(Round-Robin)(默认,除 Windows 外)

主进程负责 accept 所有的连接,然后以轮询的方式依次分配给每个 Worker。每个连接只会被一个 Worker 拿到,能够比较均匀地分布负载。这也是大多数情况下的推荐策略,因为它避免了某些内核调度可能导致的负载不均(如“惊群”现象)。

  1. 操作系统调度(none)

不启用 Node.js 层面的负载均衡,直接让所有 Worker 都在同一个 socket 上同时 accept。这种方法依赖操作系统的调度机制(如 Linux 3.9+ 的 SO_REUSEPORT 或更早版本的“惊群”但内核可能会避免)。在 Linux 上,这种方式可能因为内核的调度行为导致个别 Worker 承担过多连接,通常建议配合 SO_REUSEPORT 或将调度策略显式设为默认的 round-robin。

在绝大多数生产环境中,使用默认的 round-robin 能获得较为稳定的负载效果。仅在需要更底层控制的特殊场景下(例如对内核行为的精确依赖),才考虑切换为 OS 调度。

10.3.4 进程守护与自动重启

线上服务难免会遇到 Worker 进程意外退出——可能是未捕获的异常,也可能是内存耗尽导致被杀。cluster 模块提供了基础的进程守护能力:主进程可以监听 Worker 的 exit 事件,当某个 Worker 挂掉时,立即 fork 一个新的 Worker 替补。

一个典型的主从守护代码如下:

const cluster = require('cluster');
const os = require('os');

if (cluster.isMaster) {
  const numCPUs = os.cpus().length;
  console.log(`主进程 ${process.pid} 启动`);

  // 根据 CPU 核心数创建工作进程
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  // 监听工作进程退出,自动重启
  cluster.on('exit', (worker, code, signal) => {
    console.log(`工作进程 ${worker.process.pid} 异常退出,正在重启...`);
    cluster.fork();
  });
} else {
  // 工作进程实际运行的代码
  require('./app'); // 假设 app.js 启动 HTTP 服务
  console.log(`工作进程 ${process.pid} 已启动`);
}

注意事项: 若 Worker 反复崩溃(比如代码一启动就抛错),上面的简单示例会导致无限重启,从而不断消耗系统资源。生产环境应当结合计数器,如果短时间内重启次数过多,则立即终止并通知运维,或使用更成熟的进程管理器(如 PM2)来处理。

10.3.5 进程间通信

主进程和 Worker 之间可以通过 IPC 通道发送消息。每一个 Worker 对象都提供了一个 send() 方法,而 Worker 进程本身也可以通过 process.send() 向主进程回传数据。这可以用于传递共享状态、健康检查命令、优雅关闭指令等。

// 主进程中
worker.send({ type: 'shutdown' });

// Worker 进程中
process.on('message', (msg) => {
  if (msg.type === 'shutdown') {
    server.close(() => process.exit(0));
  }
});

因为每个 Worker 的内存空间是独立的,不能直接共享 JavaScript 对象。如果需要共享数据(如缓存、配置),必须借助外部存储(Redis、数据库)或者通过 IPC 通知机制。

10.3.6 优雅关闭与零停机重启

在滚动更新或服务发布时,直接杀死进程会导致正在处理的请求中断。利用 cluster 可以实现优雅关闭:

  1. 主进程收到系统信号(如 SIGTERM)后,向所有 Worker 发送关闭指令。
  2. Worker 接到指令后,调用 server.close() 拒接新连接,但继续处理已有请求,待所有活跃请求结束后再退出。
  3. 退出后,主进程可以启动新的 Worker(例如加载新版本代码的 Worker),逐渐完成平滑升级。

这种零停机部署可以通过手动编写逻辑实现,但更常见的是使用 PM2 的 gracefulReload 或 Kubernetes 的滚动更新策略,它们内部都封装了类似的优雅关闭机制。

10.3.7 开发与生产中的实际考量

虽然 cluster 模块提供了直接的多进程能力,但在实际项目中,绝大部分团队会选择 PM2 这类进程管理工具来替代手写 cluster 代码。原因是 PM2 不仅封装了 cluster 的创建与守护,还提供了日志管理、监控、负载均衡模式选择、零停机重载、集群模式启动(pm2 start app.js -i max)等开箱即用的功能。用 PM2 启动集群模式只需要一行命令,复杂度远低于手写。

尽管如此,理解 cluster 的底层原理有助于在调试性能瓶颈、排查网络问题、或需要定制化的进程管理逻辑时做出正确的判断。同时,在一些轻量级场景或需要在代码中集成进程守护逻辑的场合,直接使用 cluster 仍然是可行的选择。

10.3.8 常见误区与限制

  • 内存无法共享:每个 Worker 拥有独立的堆空间,全局变量不会在 Worker 间同步。需要使用 Redis 等外部缓存。
  • 状态服务需要注意一致性:如果服务是有状态的(例如 WebSocket 会话保持在某个 Worker 中),当该 Worker 崩溃后,原有连接将丢失。优化方案是使用 Redis 做 Session 存储,或通过消息队列实现跨进程推送。
  • CPU 密集任务仍会阻塞 Worker:Cluster 只是复制了多个相同的事件循环,并不能避免单个 Worker 内的 CPU 密集计算阻塞。对于纯计算任务,应结合 worker_threads
  • 文件描述符限制:大量 Worker 和连接会消耗文件描述符,需要合理设置系统参数。

10.3.9 小结

cluster 模块是 Node.js 原生实现多核利用的核心方案。它通过主从多进程、共享端口、自动进程守护和简单的负载均衡策略,让单线程的 Node.js 应用能够轻松扩展到多核服务器上。尽管生产环境中常由 PM2 或容器编排工具代劳,但 cluster 的原理是深入理解 Node.js 高性能部署的必修课。

在下一节,我们将探讨如何使用 worker_threads 在单进程内部开辟真正的多线程能力,以应对 CPU 密集任务的挑战,并比较其与 cluster 多进程模型的适用场景。