在 Node多进程架构的原理 中,我们已经讲解了多进程的原理,本文就其易用的应用模块 cluster 展开介绍。
单个 Node.js 实例运行在单个线程中。 为了充分利用多核系统,有时需要启用一组 Node.js 进程去处理负载任务。
在 Node v0.8 版本之前,实现多进程架构必须通过 child_process
来实现,要创建单机 Node 集群,由于有这么多细节需要处理,对普通工程师而言是一件相对较难的工作。
Node v0.8 引入了 cluster模块 ,用以解决多核 CPU 利用率的问题,同时也提供了较完善的 API ,用以处理进程的健壮性问题。
cluster模块 可以创建共享服务器端口的子进程。
// 推荐写法,主、子进程分离
// server.js
const cluster = require('cluster');
const numCPUs = require('os').cpus().length;
cluster.setupMaster({
exec: "worker.js" // 此参数为工作进程的文件路径
});
var cpus = require('os').cpus();
for (var i = 0; i < numCPUs; i++) {
// 创建多个工作进程之后, 程序就会相应启动n次工作进程
// 这里指定工作进程为 worker.js 文件,所以其执行n次
cluster.fork();
}
// worker.js
// 工作进程可以共享任何 TCP 连接。
// 在本例子中,共享的是 HTTP 服务器。
http.createServer((req, res) => {
res.writeHead(200);
res.end('你好世界\n');
}).listen(8000);
console.log(`工作进程 ${process.pid} 已启动`);
执行 node server.js
将会得到与前文创建子进程集群效果相同。就官方的文档而言,它更喜欢如下的形式作为示例:
// 官方示例,个人不推荐
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;
// 执行时判断当前运行的进程为主进程还是工作进程,主进程就创建多个工作进程,工作进程就创建http server应用
if (cluster.isMaster) {
console.log(`主进程 ${process.pid} 正在运行`);
// 衍生(fork)工作进程。
for (let i = 0; i < numCPUs; i++) {
// 创建多个工作进程之后, 程序就会相应启动n次工作进程
// 这里工作进程与主进程为同一文件,所以其执行n + 1次此文件
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`工作进程 ${worker.process.pid} 已退出`);
});
} else {
// 工作进程可以共享任何 TCP 连接。
// 在本例子中,共享的是 HTTP 服务器。
http.createServer((req, res) => {
res.writeHead(200);
res.end('你好世界\n');
}).listen(8000);
console.log(`工作进程 ${process.pid} 已启动`);
}
运行代码,则工作进程会共享 8000 端口:
$ node server.js
主进程 3596 正在运行
工作进程 4324 已启动
工作进程 4520 已启动
工作进程 6056 已启动
工作进程 5644 已启动
在进程中判断是主进程还是工作进程,主要取决于环境变量中是否有 NODE_UNIQUE_ID
,如下所示:
cluster.isWorker = ('NODE_UNIQUE_ID' in process.env);
cluster.isMaster = (cluster.isWorker === false);
但是官方示例中,忽而判断 cluster.isMaster、忽而判断 cluster.isWorker,对于代码的可读性十分差。我建议用 cluster.setupMaster() 这个API,将主进程和工作进程从代码上完全剥离,如同 send() 方法看起来直接将服务器从主进程发送到子进程那样神奇,剥离代码之后,甚至都感觉不到主进程中有任何服务器相关代码。
通过 cluster.setupMaster()
创建子进程而不是使用 cluster.fork()
,程序结构不再凌乱,逻辑分明,代码的可读性和可维护性较好。
Cluster 工作原理
事实上,cluster 模块就是 child_process
和 net
模块的组合应用。
cluster 启动时,如同我们在 Node多进程架构的原理 中的代码一样,它会在内部启动 TCP 服务器,在 cluster.fork() 子进程时,将这个 TCP 服务器端的 socket 的文件描述符发送给 工作进程。
如果进程是通过 cluster.fork() 复制出来的,那么它的环境变量里就存在 NODE_UNIQUE_ID ,如果工作进程中存在 listen() 侦听网络端口的调用,它将拿到该文件描述符,通过 SO_REUSEADDR 端口重用,从而实现多个子进程共享端口。对于普通方法启动的进程,则不存在文件描述符传递共享等事情。
在 cluster 内部隐式创建 TCP 服务器的方式对使用者来说十分透明,但也正是这种方式使得它无法如直接使用 child_process
那样灵活。在 cluster 模块应用中,一个主进程只能管理一组工作进程,如下所示:
对于自行通过 child_process
来操作时,则可以更灵活地控制工作进程,甚至控制多组工作进程。其原因在于自行通过 child_process
操作子进程时,可以隐式地创建多个 TCP 服务器,使得子进程可以共享多个的服务器端socket,如下所示:
Cluster 事件
对于健壮性处理,cluster模块也暴露了相当多的事件。
- fork: 复制一个工作进程后,触发该事件。
- online: 复制好一个工作进程后,工作进程主动发送一条online消息给主进程,主进程收到消息后,触发该事件。
- listening: 工作进程中调用listen()(共享了服务器端Socket)后,发送一条listening消息给主进程,主进程收到消息后,触发该事件。
- disconnect: 主进程和工作进程之间IPC通道断开后,触发该事件。
- exit: 有工作进程退出时,触发该事件。
- setup: cluster.setupMaster()执行后,触发该事件。
这些事件大多跟 child_process
模块的事件相关,在进程间消息传递的基础上完成的封装。这些事件对于增强应用的健壮性已经足够了。
更多详细信息请参考:nodejs.cn/api/cluster…
参考
- 深入浅出NodeJs by 朴灵