nodejs cluster模块初探

时间:2022-03-24 23:16:54

大家都知道nodejs是一个单进程单线程的服务器引擎,不管有多么的强大硬件,只能利用到单个CPU进行计算。所以,为了使用多核cpu来提高性能 就有了cluster,让node可以利用多核CPU实现并行。

随着nodejs的发展,让nodejs上生产环境,就必须是支持多进程多核处理!在V0.6.0版本,Nodejs内置了cluster的特性。自此,Nodejs终于可以作为一个独立的应用开发解决方案,映入大家眼帘了。

这个包不用安装,node默认自带。

引入cluster

 const cluster = require('cluster');

  

如果你有多进程使用的经验,就一定知道一般来说很多多进程的模型都是 一个主进程,然后fork一堆工作进程。我们这里也不例外。 下面看一个简单的例子。

const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
for (var i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`worker ${worker.process.pid} died`);
});
} else {
http.createServer((req, res) => {
res.writeHead(200);
res.end('hello world\n');
}).listen(8000);
}

  

这个demo首先引入了 cluster 然后判断是否是主进程,如果是的话 进入主进程 创建 cpu个数 个子进程。然后监听子进程exit

子进程if判断未通过,所以进入else 执行创建http代码

这样就可以简单的实现了一个单机多进程的httpd服务器。

#### 初窥
nodejs把进程封装的很全面,所以以前有c基础的同学再看node就会感觉非常简单。大家都知道在c里面 我们是通过fork的返回值判断到底是主进程还是子进程 那么在node里面 isMaster 到底是何方神圣呢。

其实node会在子进程里面添加一个环境变量

module.exports = ('NODE_UNIQUE_ID' in process.env) ?
require('internal/cluster/child') :
require('internal/cluster/master');

  

只需要判断当前进程有没有环境变量“NODE_UNIQUE_ID”就可知道当前进程是否是主进程;而变量“NODE_UNIQUE_ID”则是在主进程fork子进程时传递进去的参数,因此采用cluster.fork创建的子进程是一定包含“NODE_UNIQUE_ID”的。

消息通信

nodejs为我们屏蔽了底层的多进程信息通信,提供了一个 `send` 和 `on` 事件来传递消息,不过只能子进程和父进程传递,如果我们要2个子进程传递的话必须通过主进程做中转。

const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
for (var i = 0; i < numCPUs; i++) {
var worker = cluster.fork();
(function (i) {
workers[i].on('message', function(message) {
if (message.cmd == 'start') {
console.log(JSON.stringify(message));
}
});
workers[i].send({cmd:'callback',msg:"收到收到"});
})(i);
}
cluster.on('exit', (worker, code, signal) => {
console.log(`worker ${worker.process.pid} died`);
});
} else {
http.createServer((req, res) => {
res.writeHead(200);
res.end('hello world\n');
process.send({cmd: 'start',msg:"我已准备好!"});
}).listen(8000);
}

  

上面代码我们通过 子进程 `process.send` 发送给父进程,然后父进程接收到消息后 通过`send`回传的一个简单demo。

疑惑,多个子进程可以监听一个端口吗?

 

大家看到上面的几个例子,都是多个子进程监听同一个8000端口,那么有同学会有疑问,多个socket是否可以同时监听一个端口呢.

其实 cluster 实现同时多个进程监听一个端口,有两种方法。

1、 RoundRobin 默认*

主进程每fork一个子进程,都会调用handoff函数,进入该子进程的处理循环中。一旦主进程没有缓存的客户端请求时(this.handles为空),便会将当前子进程加入free空闲队列,等待主进程的下一步调度。这就是cluster模式的RoundRobin调度策略,每个子进程的处理逻辑都是一个闭环,直到主进程缓存的客户端请求处理完毕时,该子进程的处理闭环才被打开。在这个模式中,其实子进程 listen是个假的,并不会真正的监听端口,而是主进程来监听端口,如果有请求过来,就会调用其中的一个子进程来处理。

nodejs cluster模块初探

2、shared socket (windows默认)

shared socket策略(后文简称SS策略)采用SS策略调度算法,子进程的服务器工作逻辑完全不同于上文中所讲的那样,子进程创建的TCP服务器会在底层侦听端口并处理响应,这是如何实现的呢?SS策略的核心在于IPC传输句柄的文件描述符,并且在C++层设置端口的**SO_REUSEADDR(端口复用)** 选项,最后根据传输的文件描述符还原出handle(net.TCP),处理请求。这正是shared socket名称由来,共享文件描述符。

既然SS策略传递的是master进程的服务端socket的文件描述符,子进程侦听该描述符,那么由谁来调度哪个子进程处理请求呢?这就是由操作系统内核来进行调度。可是内核调度往往出现意想不到的效果,在linux下导致请求往往集中在某几个子进程中处理。这从内核的调度策略也可以推算一二,内核的进程调度离不开上下文切换,上下文切换的代价很高,不仅需要保存当前进程的代码、数据和堆栈等用户空间数据,还需要保存各种寄存器,如PC,ESP,最后还需要恢复被调度进程的上下文状态,仍然包括代码、数据和各种寄存器,因此代价非常大。而linux内核在调度这些子进程时往往倾向于唤醒最近被阻塞的子进程,上下文切换的代价相对较小。而且内核的调度策略往往受到当前系统的运行任务数量和资源使用情况,对专注于业务开发的http服务器影响较大,因此会造成某些子进程的负载严重不均衡的状况。那么为什么cluster模块默认会在windows机器中采用SS策略调度子进程呢?原因是node在windows平台采用的IOCP来最大化性能,它使得传递连接的句柄到其他进程的成本很高,因此采用默认的依靠操作系统调度的SS策略。

今天就大概写到这里,其实我对cluster了解的也不算很多,这篇文章就当是个抛砖引玉吧。欢迎大家一起来共同学习。

cluster 常用对象

cluster的各种属性和函数
cluster.setttings:配置集群参数对象
cluster.isMaster:判断是不是master节点
cluster.isWorker:判断是不是worker节点
Event: 'fork': 监听创建worker进程事件
Event: 'online': 监听worker创建成功事件
Event: 'listening': 监听worker向master状态事件
Event: 'disconnect': 监听worker断线事件
Event: 'exit': 监听worker退出事件
Event: 'setup': 监听setupMaster事件
cluster.setupMaster([settings]) : 设置集群参数
cluster.fork([env]): 创建worker进程
cluster.disconnect([callback]): 关闭worket进程
cluster.worker: 获得当前的worker对象
cluster.workers: 获得集群中所有存活的worker对象 worker对象
worker的各种属性和函数:可以通过cluster.workers, cluster.worket获得。
worker.id: 进程ID号
worker.process: ChildProcess对象
worker.suicide: 在disconnect()后,判断worker是否自杀
worker.send(message, [sendHandle]): master给worker发送消息。注:worker给发master发送消息要用process.send(message)
worker.kill([signal='SIGTERM']): 杀死指定的worker,别名destory()
worker.disconnect(): 断开worker连接,让worker自杀
Event: 'message': 监听master和worker的message事件
Event: 'online': 监听指定的worker创建成功事件
Event: 'listening': 监听master向worker状态事件
Event: 'disconnect': 监听worker断线事件
Event: 'exit': 监听worker退出事件

  

参考文档
 Nodejs cluster模块深入探究
https://cnodejs.org/topic/596ffb9b3f0ab31540ed4b91
官方文档
https://nodejs.org/api/cluster.html