Skip to content
0

cluster

Node.js 进程集群可用于运行多个 Node.js 实例,这些实例可以在其应用程序线程之间分配工作负载。当不需要进程隔离时,请改用worker_threads 模块,它允许在单个Node.js实例中运行多个应用程序线程。

共享服务器端口的子进程

cluster 模块允许轻松创建所有共享服务器端口的子进程。

import cluster from 'node:cluster';
import http from 'node:http';
import { availableParallelism } from 'node:os';

const numCPUs = availableParallelism(); // Available in node18

if (cluster.isPrimary) {
  console.log(`Primary ${process.pid} is running`);

  // Fork workers.
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  cluster.on('exit', (worker, code, signal) => {
    console.log(`worker ${worker.process.pid} died`);
  });
} else {
  // Workers can share any TCP connection
  // In this case it is an HTTP server
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end(`Response from ${process.pid}\n`);
  }).listen(8000);

  console.log(`Worker ${process.pid} started`);
}

我们启动 server,并且一次性用 curl 发送多个请求:

$ node server.js
Primary 42084 is running
Worker 42086 started
Worker 42090 started
Worker 42087 started
Worker 42088 started
Worker 42092 started
Worker 42097 started
Worker 42091 started
Worker 42089 started
Worker 42093 started
Worker 42096 started
Worker 42094 started
Worker 42095 started
$ for i in {1..5}; do curl http://127.0.0.1:8000; done
Response from 42086
Response from 42090
Response from 42087
Response from 42088
Response from 42092

可以看到是不同的子进程响应的请求

如何实现的

在上面的例子中,可以看到有多个进程监听了同一端口,通常来说,多个进程监听同个端口,系统会报错

那为什么我们的例子没问题呢?

首先,我们要知道在 NodeJs 中,是 net 模块提供的网络通信的能力。而 http 模块是建立在 net 模块之上,提供了更高层次的抽象,专门用于处理 HTTP 请求和响应,它是基于 net 的封装

在 net 模块的 listen 方法中进行了特殊处理,对 master 进程和 worker 进程有不同的逻辑执行判断:

  • master - 创建 server 实例,正常监听端口
  • worker - 也创建 server 实例,通过 IPC 向主进程的 server 通信。主进程将请求转发给 worker 进程的 server 实例

具体源码实现在:https://github.com/nodejs/node/blob/ea88a3e1f2ba38531806fbd6fb5c668fa1cf0104/lib/net.js#L1916-L1934

判断主/子进程

判断从而执行不同的分支,这也是上面代码执行模式的原理

cluster.isMaster // Deprecated ❌

cluster.isPrimary // => True if the process is a primary.
cluster.isWorker //=> True if the process is not a primary.

worker

关于 fork 出来的 worker 一些特性

id

if (cluster.isPrimary) {
    const worker = cluster.fork();
    console.log(worker.id) //=> 1
    // 创建出来的 worker 会被自动收集到 cluster.workers 中
    console.log(cluster.workers[worker.id] === worker) // => true
} else {
    console.log(cluster.worker.id) //=> 1
}

exit 事件

if (cluster.isPrimary) {
  const worker = cluster.fork();
  worker.on('exit', (code, signal) => {
    if (signal) {
      console.log(`worker was killed by signal: ${signal}`);
    } else if (code !== 0) {
      console.log(`worker exited with error code: ${code}`);
    } else {
      console.log('worker success!');
    }
  });
} else {
    // process.exit(0) //=> worker success!
    // process.exit(1) //=> worker exited with error code: 1
}

message 事件

import cluster from 'node:cluster';

if (cluster.isPrimary) {
    const worker = cluster.fork()
    worker.on('message', function(data) {
        if(data.cmd === 'NODE_NotifyRequest') {
            console.log(data)
        }
    });
} else {
    process.send({ cmd: 'NODE_NotifyRequest' })
}

TIP

NODE_ 作为前缀的 cmd,该消息会被视为内部消息,不会通过 message 事件抛出。所以推荐这样的命名规范

父子通信

import cluster from 'node:cluster';

if (cluster.isPrimary) {
    const worker = cluster.fork();
    setTimeout(() => { // 需要一点延迟,等 worker 监听再 send
        worker.send('hello');
    }, 1000)
} else if (cluster.isWorker) {
    process.on('message', (msg) => {
        console.log(msg)
    });
}

Released under the MIT License.