人人都会AI编程

20.4 集群与负载均衡

更新时间:2026-07-11

Node.js 主线程在单个核心上运行这一事实,意味着如果只启动一个进程,即便服务器有 32 个 CPU 核心,你的应用也只能用满其中 1 个。当流量增长到一定程度,单个 Node.js 进程的吞吐量会触及天花板,响应延迟开始上升,服务稳定性也随之下降。解决这个问题的方向非常明确:启动多个进程,将请求分摊到不同核心上并行处理。 这就是集群与负载均衡要达到的目标。

Node.js 提供了两套主流方案:原生的 cluster 模块和 PM2 的集群模式。二者底层原理相同,都基于主进程(master)监听端口、将连接分发给多个工作进程(worker)的模式。而在生产环境中,通常还会在 Node.js 集群前面加一层 Nginx 反向代理,形成更健壮的多层负载均衡架构。

20.4.1 Cluster 模块:多进程共享端口的内部负载均衡

cluster 是 Node.js 内置模块,能简单快速地创建共享同一端口的多个进程。它的工作原理可以概括为以下几点:

  1. 主进程调用 cluster.fork() 创建多个子进程(worker),数量通常等于 CPU 核心数。
  2. 主进程并不直接处理请求,而是监听端口,将进入的连接通过轮询(round-robin) 算法分发给不同的 worker。
  3. 每个 worker 独立运行自己的事件循环,互不影响,崩溃也不会波及其他 worker(前提是做好进程守护)。

Node.js 内部针对不同的操作系统会使用不同的分发策略:在 Linux 上默认启用 round-robin,而在 Windows 上则是由 worker 直接竞争接受连接。生产环境通常要求显式设置为 round-robin,以保证请求在各 worker 间均匀分布。

下面是一个使用 cluster 模块的典型示例,它创建一个与 CPU 核心数相等的多进程 HTTP 服务:

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

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

  // 为每个 CPU 核心 fork 一个 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 {
  // 工作进程内部可以共享任何 TCP 连接
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end(`处理请求的进程 PID: ${process.pid}\n`);
  }).listen(8000);

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

运行这个脚本,所有工作进程都监听同一个 8000 端口,主进程负责将请求均匀派发。你可以通过多次访问 http://localhost:8000 观察到每次返回的 PID 不同,证明负载分摊到了不同的 worker 上。

cluster 的优点在于零依赖、轻量可控,适合对进程数量、重启策略有精确要求的项目。缺点是需要自己处理进程管理细节(如优雅退出、平滑重启、日志聚合),工程量大时容易出错。

20.4.2 PM2 集群模式:生产级的进程守护与负载均衡

PM2 是目前 Node.js 生态中最常用的进程管理工具,它的 cluster 模式本质上是对内置 cluster 模块的高级封装,并增加了大量运维能力:

  • 自动检测 CPU 核心数pm2 start app.js -i max 会根据服务器核心数自动启动相应数量的进程。
  • 进程守护与自动重启:worker 进程崩溃或被 OOM Killer 杀死后,PM2 会自动拉起来,保证服务可用。
  • 零停机重载(Graceful Reload):更新代码后,pm2 reload 会逐个重启 worker,始终保持有进程在线,不会中断服务。
  • 内置负载均衡:在 cluster 模式下,PM2 充当主进程,将请求分发到各 worker。
  • 内存监控与自动重启:可以配置 --max-memory-restart,当某个 worker 内存超过阈值时自动重启。

一个典型的 PM2 集群启动命令如下:

# 以最大 CPU 核心数启动集群
pm2 start app.js -i max --name my-api

# 或指定进程数量
pm2 start app.js -i 4

与之配合的 ecosystem.config.js 配置文件可以固化这些参数:

module.exports = {
  apps: [{
    name: 'my-api',
    script: 'app.js',
    instances: 'max',          // 或数字,如 4
    exec_mode: 'cluster',      // 集群模式
    max_memory_restart: '500M',// 内存超限自动重启
    env: {
      NODE_ENV: 'production'
    }
  }]
};

之后运行 pm2 start ecosystem.config.js 即可按照配置启动。

PM2 的负载均衡依赖于 Node.js 的 cluster 模块,因此同样使用轮询算法将连接分发给 worker。在生产环境中,PM2 还常被用作守护进程管理多个不同的服务,通过 pm2 list 查看状态,pm2 logs 汇聚所有 worker 日志,极大降低了运维成本。

20.4.3 Nginx 反向代理:多节点负载均衡

当服务规模进一步扩大,单台服务器上再多进程也有物理上限,此时需要横跨多台机器部署多个 Node.js 实例,并由一个外部的反向代理来统一接收请求,再分发给后端的实例池。Nginx 是目前最成熟、性能最优秀的选择之一。

典型架构如下:

 用户请求
    ↓
 Nginx (入口,反向代理)
    ↓
 后端 Node.js 集群 (多台服务器,每台运行多个 PM2/cluster 进程)

Nginx 提供了丰富的负载均衡算法:

  • 轮询(round-robin):默认方式,请求按顺序分配给后端服务器。
  • 最少连接(least_conn):优先分发给当前活跃连接最少的服务器,适合长连接场景。
  • IP 哈希(ip_hash):根据客户端 IP 的哈希值固定分配给某个后端,解决会话粘滞问题(若后端无状态则可关闭)。
  • 权重(weight):手动为不同性能的服务器分配不同的请求比例。

下面是一个典型的 Nginx 反向代理配置:

upstream node_backend {
    # 轮询算法,可添加 weight 调整权重
    server 192.168.1.10:8000 weight=5;
    server 192.168.1.11:8000 weight=5;
    server 192.168.1.12:8000 backup;   # 备用节点
}

server {
    listen 80;
    server_name api.example.com;

    location / {
        proxy_pass http://node_backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;

        # 设置超时,防止慢请求拖垮连接
        proxy_connect_timeout 5s;
        proxy_read_timeout 30s;
    }
}

这段配置定义了一个上游组 node_backend,包含两台主服务器和一台备用服务器。通过 proxy_pass 将请求转发过去,同时使用 proxy_set_header 将真实客户端 IP 等信息传递给 Node.js(这在日志和鉴权中非常重要)。当后端某个实例宕机,Nginx 会自动将请求转发到其他健康实例,并在该实例恢复后重新加入池中。

20.4.4 完整的集群负载均衡方案选择

在实际项目中,我们通常会组合使用多层负载均衡:

  • 单机多核利用:通过 cluster 或 PM2 集群模式在每台服务器内部启动多个进程,充分利用 CPU。
  • 多机水平扩展:使用 Nginx(或 HAProxy)在多个服务器之间分发请求,实现横向扩容和高可用。
  • 会话处理:如果应用是有状态的(如使用内存存储 Session),需要使用 Redis 等外部缓存统一管理 Session,或开启 Nginx 的 ip_hash 实现粘性会话。但更推荐将服务设计为无状态,方便弹性伸缩。
  • 健康检查与故障转移:Nginx 的原生健康检查(max_failsfail_timeout)能自动跳过不健康的后端,PM2 负责单机进程的自动重启,双重保障降低服务中断概率。

20.4.5 实践中的注意事项

尽管集群和负载均衡能线性提升吞吐量,但在实施过程中有几个容易忽视的细节:

  1. 共享状态问题:如果你的应用在内存中缓存了数据(如简单的 Map 对象),多进程会导致数据不一致。解决方法是将共享状态外置到 Redis、数据库或消息队列中,不要依赖进程内存。
  2. WebSocket 粘性会话:Nginx 反向代理 WebSocket 时,需要确保客户端与后端建立的连接始终路由到同一台服务器。可以在 Nginx 中配置 ip_hash 或使用更高级的负载均衡方案(如 HAProxy 的粘性表)。
  3. 数据库连接池放大:每个 worker 进程都会建立自己的数据库连接池,如果 4 个 worker 每个配置了 max: 20,总共会向数据库打开 80 个连接,可能导致数据库压力过大。应适当调低单个进程的连接池大小,或使用连接复用中间件(如 PgBouncer)。
  4. 文件描述符限制:高并发下 Linux 默认的 1024 个文件描述符限制可能不够,需要在系统中调高(如 ulimit -n 65535)。
  5. 滚动日志与日志聚合:多进程自行写文件会产生日志分割混淆或写冲突,PM2 内置了日志管理,但大规模多机部署建议使用集中式日志方案(如 ELK、Loki)。
  6. 优雅退出:重启或缩容时,应让 worker 先关闭监听、处理完已接收的请求,再退出进程。PM2 的 graceful shutdown 机制和 Nginx 的 worker_shutdown_timeout 可以帮助实现这一点。

通过合理使用集群和负载均衡,Node.js 服务可以从单机的几千并发,轻松扩展到支撑海量用户的高可用集群。这既是性能优化的关键一环,也是从开发走向生产运维的必经之路。