人人都会AI编程

10.3 集群模式:cluster 模块

更新时间:2026-07-10

前面两节分别介绍了 process 对象和 child_process 模块,我们已经可以单进程地管理环境变量、创建子进程并发执行任务。但在实际生产环境中,一台服务器往往有多个 CPU 核心,而 Node.js 的 JavaScript 主线程只能使用其中一个核心。假如你的服务器有 8 核心,但只有一个 Node 进程在运作,那么剩下 7 个核心基本是“围观”状态,无法有效发挥硬件能力。

cluster 模块就是为了解决这个问题而设计的。它允许多个 Node.js 进程共享同一个服务器端口,并通过主从模式统一管理它们,从而在不引入 Nginx 等外部组件的情况下,快速实现多进程负载均衡。

10.3.1 为什么需要 cluster:多核利用的本质

Node.js 的单线程模型擅长高并发 I/O,但 CPU 运算能力却受限于一个核心。当并发足够大或者业务逻辑稍重时,单进程的吞吐量总会碰到天花板。cluster 模块的思路非常直接:在同一个机器上启动多个工作进程,每个工作进程独立运行一份应用代码,并共享相同的监听端口。 这样,多核 CPU 就能被多个工作进程平均分担,整体处理能力几乎线性增长,同时保持开发模型不变。

10.3.2 基本用法:从单进程到多进程

使用 cluster 模块非常直观。主进程(通常称为 Master)负责初始化系统资源并创建若干工作进程(Worker),工作进程则执行真正的服务代码。主进程本身不处理业务请求,只承担管理和调度职责。

const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
  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 {
  // 工作进程可以共享任何 TCP 连接,比如 HTTP 服务器
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end('Hello World\n');
  }).listen(3000);

  console.log(`工作进程 ${process.pid} 已启动`);
}

通过 cluster.isMaster 判断当前进程是主进程还是工作进程。主进程内调用 cluster.fork() 会使用 child_process.fork() 为当前文件创建一个子进程,该子进程执行时 cluster.isMasterfalse,从而进入 else 分支走到真实的服务器逻辑。

现象:启动这个脚本后,系统会创建一个主进程和 N 个工作进程,但它们都监听 3000 端口。操作系统并不会报端口冲突,因为 cluster 模块在底层通过特殊的文件描述符传递机制,让所有工作进程共享同一个端口句柄。

10.3.3 负载均衡:连接请求如何分发

当多个工作进程监听同一端口时,外部请求是如何被分配的?Node.js 提供了两种主要调度策略,默认使用 轮询(Round-Robin)

Round-Robin 负载均衡(默认)

在这种模式下,主进程负责监听端口,并将接收到的连接按照顺序依次分发给工作进程。主进程与工作进程之间使用 IPC 通道传递文件描述符,每个连接轮流转给下一个工作进程。

客户端 → 服务端口(主进程监听) → IPC分发 → Worker 1
                                    → Worker 2
                                    → Worker 3
  • 优点:分发逻辑简单、公平,能避免某几个 Worker 堆积大量连接而其他 Worker 空闲的情况。
  • 缺点:多了一次 IPC 传递的开销,但在绝大多数场景下可以忽略不计。

由操作系统直接分发(不设置 Round-Robin)

如果设置环境变量 NODE_CLUSTER_SCHED_POLICYnone,或者在某些非 Linux 平台上默认行为不同,则每个工作进程会自行监听端口。此时,操作系统内核负责调度哪个进程接收到新的连接(通常也是某种轮询或哈希策略)。

这种模式下,分发效率略高,因为少了主进程中转,但可能出现“惊群”现象(多个进程被同时唤醒,但只有一个成功获取连接),在新版 Linux 内核中已通过 accept4SO_REUSEPORT 优化。一般情况下,保留默认的 Round-Robin 即可。

10.3.4 主从模式与进程守护

Cluster 的核心是 主从架构:主进程作为 Master,所有工作进程为 Worker。主进程不处理业务,专职管理 Worker 的创建、调度、销毁和重启。这种模式带来的直接好处是 进程守护:当某个工作进程因为未捕获异常或内存耗尽而意外退出时,主进程能够侦测到 exit 事件并立即创建一个新进程补上,保证服务不被中断。

cluster.on('exit', (worker, code, signal) => {
  console.log(`工作进程 ${worker.process.pid} 退出,code=${code}, signal=${signal}`);
  // 通常建议添加一个条件判断,避免因为内存泄漏等原因
  // 导致反复重启消耗资源。同时可记录日志、发送告警。
  cluster.fork();
});

除了被动守护,主进程还可以主动给 Worker 发送安抚信号:

  • 如果业务需要平滑重启(零停机更新),可以由主进程依次向工作进程发送 SIGTERM 信号,让它们优雅关闭(停止接收新连接,处理完当前请求再退出),然后 fork 新版本的 Worker。
  • 如果某个 Worker 长时间无响应,主进程可以设置超时,调用 worker.kill() 强制重启。

10.3.5 生产级选择:cluster 还是 PM2?

cluster 模块虽然强大,但在真正的生产环境中,我们通常直接使用 PM2(Process Manager)来代替手工管理 cluster。PM2 内部基于 cluster 模块并在此基础上封装了更丰富的功能:

  • 命令行启动多进程pm2 start app.js -i max,自动根据 CPU 核心数创建进程。
  • 图形化管理与监控:提供仪表盘查看 CPU、内存、请求量。
  • 日志管理:自动收集每个进程的 stdout/stderr 输出,支持日志轮转。
  • 零停机重载pm2 reload 逐一重启工作进程,保持服务可用。
  • 开机自启、异常自动重启:可用 pm2 startup 生成系统服务脚本。
  • 进程守护增强:除了监控 Node 进程退出,还可以设置内存上限自动重启(--max-memory-restart 500M)。

使用 PM2 时,你不需要手动写 cluster 代码,只需将应用编写为单进程模式(监听端口)即可,PM2 会自动包裹成 cluster 模式。这极大简化了多进程部署的复杂度。但在一些轻量级容器环境或需要深度自定义进程行为的场景下,直接使用 cluster 模块依然有价值。

10.3.6 多进程下的状态同步陷阱

最后,必须提醒一个真实的陷阱:多进程意味着内存不共享。在单进程模式下,你可以随意将用户 session 或缓存在内存中存放;但一旦切换到 cluster 多进程,同一个用户的两次请求可能落在不同的 Worker 上,导致 session 失效或数据不一致。

解决方法通常是将共享状态外迁:

  • Session 存放到 Redis 或 Memcached,所有进程通过同样的 key 读取。
  • 分布式缓存 如 Redis,确保数据一致性。
  • 数据库 本身就是天然共享的持久层。
  • Sticky Session:通过 IP 哈希或 Cookie 指定,将同一用户的请求永远路由到同一个 Worker,但这不是银弹,且可能破坏负载均衡的均匀性。

理解这一限制,是平稳地从单进程走向多进程部署的关键一步。


cluster 模块和 PM2 共同构成了 Node.js 在生产环境中的多核利用方案。下一节我们将继续深入 worker_threads 模块,探讨如何在同一进程内使用多线程处理 CPU 密集型任务,以及它与多进程模型的取舍。