node 的 cluster模块

200 阅读4分钟

cluster翻译可以为 集群。这也是node的翻译。

我们都知道js是单线程的语言,单个 Node.js 实例也运行在单个线程中。

为了充分利用多核系统,有时需要启用一组 Node.js 进程去处理负载任务(共享一个端口)。

const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length; //获取cpu的数量,每颗核心开一个node进程,运行同一份代码。

if (cluster.isMaster) { // 一组进程往往需要一个master主进程引导
  console.log(`主进程 ${process.pid} 正在运行`);
  
  // 衍生工作进程。
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  cluster.on('exit', (worker, code, signal) => { // 设置一个监听器,在node进程退出的时候触发
    console.log(`工作进程 ${worker.process.pid} 已退出`);
  });
} else {
  // 工作进程可以共享任何 TCP 连接。
  // 在本例子中,共享的是 HTTP 服务器。
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end('你好世界\n');
  }).listen(8000);

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

以下是运行结果

$ node server.js
主进程 3596 正在运行
工作进程 4324 已启动
工作进程 4520 已启动
工作进程 6056 已启动
工作进程 5644 已启动

上面也看到了,其实他是fork出来的一个子进程,所以可以使用 IPC 和父进程通信,从而使各进程交替处理连接服务。

cluster 模块支持两种分发连接的方法。

  1. 第一种方法(也是除 Windows 外所有平台的默认方法)是循环法,由主进程负责监听端口,接收新连接后再将连接循环分发给工作进程,在分发中使用了一些内置技巧防止工作进程任务过载。(以上的例子便是)

  2. 第二种方法是,主进程创建监听 socket 后发送给感兴趣的工作进程,由工作进程负责直接接收连接。

理论上第二种方法应该是效率最佳的。 但在实际情况下,由于操作系统调度机制的随机,会使分发变得不稳定。 可能会出现八个进程中有两个分担了 70% 的负载。其他的空闲出来。

因为 server.listen() 将大部分工作交给主进程完成,因此导致普通 Node.js 进程与 cluster 工作进程差异的情况有三种:

  1. server.listen({fd: 7}) 因为消息会被传给主进程,所以父进程中的文件描述符 7 将会被监听并将句柄传给工作进程,而不是监听文件描述符 7 指向的工作进程。
  2. server.listen(handle) 显式地监听句柄,会导致工作进程直接使用该句柄,而不是和主进程通信(如例子)。
  3. server.listen(0) 正常情况下,这种调用会导致 server 在随机端口上监听。 但在 cluster 模式中,所有工作进程每次调用 listen(0) 时会收到相同的“随机”端口。 实质上,这种端口只在第一次分配时随机,之后就变得可预料。 如果要使用独立端口的话,应该根据工作进程的 ID 来生成端口号。
  • Worker 类

对象包含了关于工作进程的所有的公共的信息和方法。

在主进程中,可以使用 cluster.workers 来获取它。

在工作进程中,可以使用 cluster.worker 来获取它。

  • 'disconnect' 事件

类似于 cluster.on('disconnect'),但他不是全局的,而是特定于单独某个工作进程。

cluster.fork().on('disconnect', () => {
  // 工作进程已断开连接。
});
  • 'exit' 事件

类似于 cluster.on('exit') 事件,但他不是全局的,而是特定于单独某个工作进程。

// code是正常退出的代码,signal是不正常退出的信号
cluster.fork().on('exit', (code, signal) => {
 if (signal) {
   console.log(`工作进程已被信号 ${signal} 杀死`);
 } else if (code !== 0) {
   console.log(`工作进程退出,退出码: ${code}`);
 } else {
   console.log('工作进程成功退出');
 }
});
  • 'listening' 事件

类似于 cluster.on('listening') 事件,但他不是全局的,而是特定于单独某个工作进程。

cluster.fork().on('listening', (address) => {
  // 工作进程正在监听。
});
  • 'message' 事件

类似于 cluster.on('message') 事件,但特定于此工作进程。

在工作进程内,也可以使用 process.on('message')。

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

if (cluster.isMaster) { // 主线程计数

  let numReqs = 0; // 计数
  setInterval(() => { // 第二个循环再打印,给子进程执行时间
    console.log(`请求的数量 = ${numReqs}`);
  }, 1000);

  // 对请求计数的函数。
  function messageHandler(msg) {
    if (msg.cmd && msg.cmd === 'notifyRequest') {
      numReqs += 1;
    }
  }

  // 启动 worker 并监听包含 notifyRequest 的消息。
  const numCPUs = require('os').cpus().length;
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  for (const id in cluster.workers) {
    cluster.workers[id].on('message', messageHandler); // 监听子进程的消息
  }

} else {

  // 工作进程有一个 http 服务器。
  http.Server((req, res) => {
    res.writeHead(200);
    res.end('你好世界\n');

    // 通知主进程接收到了请求。
    process.send({ cmd: 'notifyRequest' }); // 子进程会走到这个逻辑发送消息。
  }).listen(8000);
}

未完待续。。。