人人都会AI编程

多核利用:cluster / PM2 集群模式

更新时间:2026-07-11

Node.js 主线程是单线程的,所以一个 Node.js 进程默认只能利用一个 CPU 核心。在如今多核服务器已成标配的环境下,如果只启动一个进程,剩下的核心都处于闲置状态,这显然是一种浪费。更关键的是,单进程一旦崩溃,整个服务就中断了。为了充分利用硬件资源并提升可用性,我们需要让多个 Node.js 进程同时工作,也就是本节要介绍的 cluster 模块和 PM2 集群模式。

为什么需要多进程?

答案可以从三个方面理解:

  1. 榨干 CPU:8 核服务器只跑一个 Node.js 进程,相当于 7 个核在围观。启用多进程后,每个进程占一个核心,并发处理能力几乎可以线性增长(受限于 I/O 和内存)。
  2. 容错与高可用:单进程难免会遇到未捕获的异常或内存泄漏等问题,一旦崩溃,服务就完全中断。多进程模式下,一个进程挂掉可以由主进程自动重启,保证服务整体可用。
  3. 绕开单线程限制:即便 I/O 再异步,如果业务代码中包含 JSON 解析超大文件、复杂模板渲染等 CPU 密集操作,还是会阻塞事件循环。多进程可以将这些任务分散到不同的核心上执行,避免相互影响。

cluster 模块:原生的多进程方案

Node.js 从 v0.8 开始内置了 cluster 模块,它使用 child_process.fork() 来创建多个工作进程,并通过进程间通信(IPC)共享同一个服务器端口。这在操作系统层面表现为多个进程监听同一个端口,而负载分配由主进程和内核的调度机制完成。

主从架构与端口共享

cluster 采用经典的主从模式:

  • 主进程(Master):不处理业务请求,只负责管理子进程的启动、重启和负载分发。
  • 工作进程(Worker):实际处理 HTTP 请求,每个 Worker 是一个独立的 Node.js 实例,拥有自己的内存和事件循环。

当调用 cluster.fork() 时,主进程会创建一个新的 Worker,Worker 内部可以同样创建 http.Server 并监听端口。但有趣的是,多个 Worker 监听同一个端口并不会触发操作系统的“地址已被占用”错误,因为 Node.js 在内部将监听端口的操作交给了主进程,主进程负责接受新连接,然后以轮转(round-robin,除 Windows 外默认)方式分发给各个 Worker。

基础用法示例

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

if (cluster.isMaster) {
  console.log(`主进程 ${process.pid} 正在运行`);

  // 创建与 CPU 数量相等的 Worker
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  // 当 Worker 退出时自动重启
  cluster.on('exit', (worker, code, signal) => {
    console.log(`工作进程 ${worker.process.pid} 已退出,正在重启...`);
    cluster.fork();
  });
} else {
  // Worker 进程,创建 HTTP 服务
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end(`处理请求的进程 PID: ${process.pid}\n`);
  }).listen(8000);

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

运行这段代码后,服务器会启动多个进程,通过浏览器访问 http://localhost:8000,返回的 PID 会随机变化,体现了负载分发。

cluster 的调度策略

在非 Windows 平台上,cluster 默认采用轮询(round-robin)的分发算法,主进程将 Socket 句柄逐个分配给 Worker,简单公平。也可以设置为让操作系统在内核层直接分配(cluster.SCHED_NONE),这种模式下由内核的 TCP 负载均衡特性决定,一般不需要调整。

进程守护与零停机重启

cluster.on('exit') 可以监听到 Worker 的异常退出并重新 fork(),这是一种简单的进程守护。但生产环境还需要考虑零停机重启(滚动更新)。一种常见方案是:主进程收到重启信号后,逐个创建新的 Worker,等新 Worker 准备就绪后再关闭旧 Worker,整个过程不中断服务。cluster 模块本身未提供封装好的平滑重启 API,很多团队会自己写信号处理逻辑,或直接使用 PM2。

PM2 集群模式:开箱即用的进程管家

手写 cluster 虽然灵活,但进程管理、日志收集、负载监控、热更新等功能都需要自己从零实现。PM2 是一个专门为 Node.js 设计的进程管理工具,它在 cluster 的基础上提供了丰富的能力,可以大幅简化生产环境部署。

快速启动集群模式

安装 PM2 后,一行命令就能启动多个进程:

pm2 start app.js -i max

其中 -i max 表示根据服务器的 CPU 核心数自动启动相应数量的进程。也可以手动指定进程数,如 -i 4

PM2 会在后台启动一个守护进程(pm2 daemon),由它来管理应用程序进程。原理上,PM2 也使用了 cluster 模块,但提供了更完善的生命周期管理:启动时自动创建 Worker,Worker 崩溃后自动重启,内存超过阈值时可自动重启,甚至支持设置固定的重启时间(比如每 24 小时定时重启释放碎片)。

常用管理命令

pm2 list                      # 查看所有进程状态
pm2 logs                      # 实时查看日志
pm2 restart app               # 重启应用
pm2 reload app                # 零停机重载
pm2 stop app                  # 停止应用
pm2 delete app                # 从 PM2 中移除应用
pm2 monit                     # 实时监控面板
pm2 save                      # 保存当前进程列表,重启自动恢复
pm2 startup                   # 配置开机自启

零停机重载(graceful reload)

PM2 的 reload 命令是生产部署的核心优势。当代码更新后,无需停机,它先启动新的 Worker,等待新进程就绪后,再逐步结束旧进程的请求处理,最后终止旧进程。整个过程请求不中断,用户无感知。

要使用该功能,应用程序需要监听 SIGTERMSIGINT 信号,并在收到信号后优雅关闭 HTTP 服务器(不再接受新连接,等待现有请求处理完毕)。Express/Koa 等框架通常可以这样做:

process.on('SIGTERM', () => {
  server.close(() => {
    process.exit(0);
  });
});

PM2 发送 SIGINT 信号给旧进程,配合上述代码,就能实现细腻的滚动更新。

配置文件管理

对于复杂的应用,PM2 支持 JSON 或 JS 配置文件(如 pm2.config.js),将所有配置固化下来:

module.exports = {
  apps: [{
    name: 'my-api',
    script: './server.js',
    instances: 'max',
    exec_mode: 'cluster',
    env: {
      NODE_ENV: 'production'
    },
    max_memory_restart: '512M',   // 内存超限自动重启
    log_date_format: 'YYYY-MM-DD HH:mm:ss',
    error_file: './logs/err.log',
    out_file: './logs/out.log',
  }]
};

然后用 pm2 start pm2.config.js 即可启动。

cluster vs PM2 如何选择?

| 维度 | cluster 原生模块 | PM2 |
|------------|--------------------------------------------|-----------------------------------------|
| 上手成本 | 需要手写 master/worker 管理逻辑 | 一行命令,开箱即用 |
| 功能完整性 | 仅提供 fork 和基本的负载分发 | 日志、监控、热重载、开机自启、环境管理 |
| 生产就绪度 | 需要自行处理进程守护、日志、平滑重启等 | 久经考验,大量生产环境验证 |
| 灵活性 | 极高,可以定制任意逻辑 | 较高,但需遵循 PM2 的线程模型 |
| 额外依赖 | 无 | 需要在服务器上安装 PM2 |

一般来说,小型项目或对定制要求极高的团队可以选择原生 cluster,而绝大多数生产环境推荐直接使用 PM2,因为它将最佳实践固化为工具,省去了大量重复工作。此外,如果应用已经运行在 Docker 或 Kubernetes 中,容器编排平台本身也能提供多副本和负载均衡能力,这时 cluster 可能会被容器层的水平扩展替代,但仍可使用 PM2 管理单容器内的进程守护。

多进程下的常见“坑”与应对

1. 内存消耗翻倍

启动多个进程,意味着整体内存消耗会成倍增加。如果一个进程占用 80MB,8 个进程就是 640MB。这对小内存服务器压力不小,需要根据实际情况调整实例数量,并不一定非要等于 CPU 核心数。

2. 进程间状态隔离

每个 Worker 拥有独立的内存和变量,全局状态无法直接共享。比如一个计数器全局变量,不同请求可能落在不同 Worker 上,导致计数不一致。解决方案是把共享状态外移到 Redis 或数据库中,或者使用专门的多进程共享模块(如 IPC 通道传递)。

3. WebSocket 与粘性会话

WebSocket 连接建立后,通常需要保持连接状态并继续在同一个进程内通信。cluster 的默认轮询分发可能导致 WebSocket 升级请求被分到一个 Worker,而后续数据包被分到另一个 Worker,造成连接断开。解决方法有两种:一是让每个 Worker 监听不同端口,由前端负载均衡器(如 Nginx)做 IP Hash 分发;二是使用 Redis 适配器(Socket.IO 已支持),通过中心化 Pub/Sub 同步消息,这样无论连接在哪个 Worker 都能正常通信。

4. 定时任务重复执行

如果代码中存在 setInterval 或 cron 任务,多进程环境下这些定时器会在每个 Worker 中都执行一次,导致重复处理。解决方案通常是让主进程负责调度,或使用外部分布式任务调度器,避免 Worker 直接运行定时任务。

小结

无论是原生的 cluster 还是成熟的 PM2,多进程模型弥补了 Node.js 单线程的短板,让服务能充分榨干多核性能,同时提供了容错能力和零停机部署的基础。对于线上 Node.js 应用而言,不使用多进程反而是不正常的。选择 PM2 可以快速进入生产就绪状态,而了解 cluster 的底层原理有助于在遇到疑难时从容应对。结合前面章节介绍的负载均衡和反向代理,你完全有能力构建一个稳定、高效、可扩展的 Node.js 服务集群。