nodejs事件循环与多进程(五)——cluster多进程模型 & worker进程使用fork()函数,实现与master进程间通信 & 惊群之发生多线程/多进程等待同一个socket事件
cluster
node进行多进程的模块
属性和方法
- isMaster 属性,返回该进程是不是主进程
- isWorker 属性,返回该进程是不是工作进程
- fork() 方法,只能通过主进程调用,衍生出一个新的 worker 进程,返回一个 worker 对象。和process.child的区别,不用创建一个新的child.js
- setupMaster([settings]) 方法,用于修改 fork() 默认行为,一旦调用,将会按照cluster.settings进行设置。
- settings 属性,用于配置,参数 exec: worker文件路径;args: 传递给 worker 的参数;execArgv: 传递给 Node.js 可执行文件的参数列表
事件
- fork 事件,当新的工作进程被 fork 时触发,可以用来记录工作进程活动
- listening 事件,当一个工作进程调用 listen() 后触发,事件处理器两个参数 worker:工作进程对象
- message事件, 比较特殊需要去在单独的worker上监听。
- online 事件,复制好一个工作进程后,工作进程主动发送一条 online 消息给主进程,主进程收到消息后触发,回调参数 worker 对象
- disconnect 事件,主进程和工作进程之间 IPC 通道断开后触发
- exit 事件,有工作进程退出时触发,回调参数 worker 对象、code 退出码、signal 进程被 kill 时的信号
- setup 事件,cluster.setupMaster() 执行后触发
文档地址:
https://nodejs.org/api/child_process.html 多看文档!
cluster多进程模型
每个worker进程通过使用child_process.fork()函数,基于IPC(Inter-Process Communication,进程间通信),实现与master进程间通信。
那我们直接用child_process.fork()自己实现不就行了,干嘛需要cluster呢?
这样的方式仅仅实现了多进程。多进程运行还涉及父子进程通信,子进程管理,以及负载均衡等问题,这些特性cluster帮你实现了。
实例
文件1
cluster\child.js
console.log('我是子进程')
文件2
cluster\main.js
var cluster = require('cluster');
var cpuNums = require('os').cpus().length;
var http = require('http');
if (cluster.isMaster) {
for (var i = 0;i < cpuNums; i++) {
cluster.fork('./child.js')
}
// 当新的工作进程被fork时,cluster模块将触发'fork'事件。 可以被用来记录工作进程活动,产生一个自定义的timeout。
// cluster.on('fork', (worker) => {
// console.log('主进程fork了一个worker pid为', worker.process.pid);
// })
// 当一个工作进程调用listen()后,工作进程上的server会触发'listening' 事件,同时主进程上的 cluster 也会被触发'listening'事件。
//事件处理器使用两个参数来执行,其中worker包含了工作进程对象, address 包含了以下连接属性: address、 port 和 addressType。当工作进程同时监听多个地址时,这些参数非常有用。
// cluster.on('listening', (worker) => {
// console.log('主进程fork了一个worker进行http服务 pid为', worker.process.pid);
// })
// 当cluster主进程接收任意工作进程发送的消息后被触发
// cluster.on('message', (data) => {
// console.log('收到data', data)
// })
// var log = cluster;
// Object.keys(cluster.workers).forEach((id) => {
// cluster.workers[id].on('message', function(data) {
// console.log(data)
// })
// });
cluster.on('disconnect', (worker)=> {
console.log('有工作进程退出了', worker.process.pid )
cluster.fork(); // 保证你永远有8个worker
});
} else {
// http.createServer((req, res) => {
// try{
// res.end(aasdasd); // 报错,整个线程挂掉,不能提供服务
// } catch(err) {
// process.disconnect(); // 断开连接, 断开和master的连接,守护进程其实就是重启
// }
// }).listen(8001, () => {
// console.log('server is listening: ' + 8001);
// });
// console.log('xxx')
process.send(process.pid);
// process.disconnect();
}
打开终端,执行命令
node .\main.js
main文件的console内容都能打印
最初的多进程模型
最初的 Node.js 多进程模型就是这样实现的,master 进程创建 socket,绑定到某个地址以及端口后,自身不调用 listen 来监听连接以及 accept 连接,而是将该 socket 的 fd 传递到 fork 出来的 worker 进程,worker 接收到 fd 后再调用 listen,accept 新的连接。但实际一个新到来的连接最终只能被某一个 worker 进程 accpet 再做处理,至于是哪个 worker 能够 accept 到,开发者完全无法预知以及干预。这势必就导致了当一个新连接到来时,多个 worker 进程会产生竞争,最终由胜出的 worker 获取连接。
相信到这里大家也应该知道这种多进程模型比较明显的问题了
- 多个进程之间会竞争 accpet 一个连接,产生惊群现象,效率比较低。
- 由于无法控制一个新的连接由哪个进程来处理,必然导致各 worker 进程之间的负载非常不均衡。
这其实就是著名的”惊群”现象。
简单说来,多线程/多进程等待同一个 socket 事件,当这个事件发生时,这些线程/进程被同时唤醒,就是惊群。可以想见,效率很低下,许多进程被内核重新调度唤醒,同时去响应这一个事件,当然只有一个进程能处理事件成功,其他的进程在处理该事件失败后重新休眠(也有其他选择)。这种性能浪费现象就是惊群。
惊群通常发生在 server 上,当父进程绑定一个端口监听 socket,然后 fork 出多个子进程,子进程们开始循环处理(比如 accept)这个 socket。每当用户发起一个 TCP 连接时,多个子进程同时被唤醒,然后其中一个子进程 accept 新连接成功,余者皆失败,重新休眠。
http.Server继承了net.Server, http客户端与http服务端的通信均依赖于socket(net.Socket)。
cluster\master.js
const net = require('net');
const fork = require('child_process').fork;
var handle = net._createServerHandle('0.0.0.0', 3000);
for(var i=0;i<4;i++) {
console.log('11111111111111111111111111')
fork('./worker').send({
}, handle);
}
cluster\worker.js
const net = require('net');
process.on('message', function(m, handle) {
//master接收客户端的请求,worker去响应
start(handle);
});
var buf = 'hello nodejs';
var res = ['HTTP/1.1 200 OK','content-length:'+buf.length].join('\r\n')+'\r\n\r\n'+buf;
var data = {
};
function start(server) {
// 响应逻辑,重点关注惊群的效果、计数
server.listen();
server.onconnection = function(err,handle) {
var pid = process.pid;
if (!data[pid]) {
data[pid] = 0;
}
data[pid] ++; //每次服务 +1
console.log('got a connection on worker, pid = %d', process.pid, data[pid]);
var socket = new net.Socket({
handle: handle
});
socket.readable = socket.writable = true;
socket.end(res);
}
}
打开终端,执行命令
node .\master.js
worker.js文件的console内容都能打印