背景

刚接触Node.js那会儿,总听说它是单线程的,这句话可把我坑惨了——有次线上流量暴涨,我的服务直接卡成PPT。后来才明白,单线程不是枷锁,解锁多核性能的关键在于集群模式。今天我们就手把手拆解这个能让你服务器吞吐量翻倍的神器。


一、为什么你的Nodejs应用需要集群模式?

上周帮朋友优化一个电商系统,单核QPS只有1200,启用集群后直接冲到8600。这背后的原理很简单:现代服务器至少8核CPU,但Nodejs默认只用一个核。就像让八个工人只做一个流水线,纯属资源浪费。

我们做个压力测试对比(使用wrk工具):

wrk -t12 -c400 -d30s http://localhost:3000
Requests/sec: 1242.34

# 启用8核集群模式
wrk -t12 -c400 -d30s http://localhost:3000 
Requests/sec: 8568.71

数据不会说谎——吞吐量提升近7倍。但这只是开始,后面还有更猛的操作。


二、三分钟搭建你的第一个集群服务

直接上干货,先看基础代码模板(技术栈:Nodejs 18.x原生cluster模块):

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 {
  // 工人进程处理具体业务
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end('Hello Cluster!');
  }).listen(3000);

  console.log(`工人进程 ${process.pid} 就绪`);
}

这段代码实现了:

  1. 自动检测CPU核心数量
  2. Master进程管理worker生命周期
  3. Worker崩溃自动重启机制
  4. 各worker共享3000端口(底层由操作系统负载均衡)

三、生产环境必须知道的五个实战技巧

3.1 负载均衡算法调优

默认的轮询策略不适合长连接场景,我们可以改用能感知worker负载的算法:

const cluster = require('cluster');

if (cluster.isMaster) {
  // 创建负载统计Map
  const workerLoads = new Map();
  
  cluster.on('message', (worker, message) => {
    if (message.type === 'loadReport') {
      workerLoads.set(worker.id, message.load);
    }
  });

  // 自定义调度策略
  cluster.on('request', (worker, message, handle) => {
    // 找出当前最空闲的worker
    const leastLoadedWorker = [...workerLoads.entries()]
      .sort((a, b) => a[1] - b[1])[0];
    
    leastLoadedWorker.send('handle', handle);
  });
}

这种实现可以让请求优先分配给负载最低的worker,适合文件上传等重量级请求场景。

3.2 进程异常监听策略

基础版的崩溃重启存在雪崩风险,得加上重启频率限制:

const restartRecords = new Map();

cluster.on('exit', (worker) => {
  const now = Date.now();
  const lastRestart = restartRecords.get(worker.id) || 0;

  // 30秒内重启超过3次则停止
  if (now - lastRestart < 30000) {
    worker.restartCount = (worker.restartCount || 0) + 1;
    if (worker.restartCount >= 3) {
      console.error('工人频繁崩溃,停止重启');
      return;
    }
  }

  restartRecords.set(worker.id, now);
  cluster.fork();
});

四、别踩这些坑!集群模式的隐藏陷阱

4.1 会话存储要特殊处理

别用内存存session!不同worker内存不共享,推荐Redis方案:

const session = require('express-session');
const RedisStore = require('connect-redis')(session);

app.use(session({
  store: new RedisStore({ host: '127.0.0.1', port: 6379 }),
  secret: 'mySecret'
}));

4.2 热更新要这样玩

直接重启会有服务中断,试试零停机部署:

// 主进程收到SIGUSR2信号时触发热更新
process.on('SIGUSR2', () => {
  const workers = Object.values(cluster.workers);
  
  function restartWorker(i) {
    if (i >= workers.length) return;
    const worker = workers[i];
    
    // 先启新后停旧
    const newWorker = cluster.fork();
    newWorker.on('listening', () => {
      worker.kill('SIGTERM');
      restartWorker(i + 1);
    });
  }
  
  restartWorker(0);
});

五、什么样的项目适合用集群?

经过三年实战,我总结出这些场景最受益:

  • 用户量超过10万的中大型Web应用
  • 需要处理大量并发I/O(如即时通讯)
  • 需要弹性缩放的云原生应用

但遇到CPU密集型任务(如视频转码),集群就力不从心了,这时候该用Worker Thread。