nodejs多进程架构

来源:互联网 发布:淘宝天猫内部优惠券 编辑:程序博客网 时间:2024/06/06 02:47

熟悉node的应该都知道cluster模块,下述提及的问题,cluster模块都已经解决。在我们的项目中直接使用cluster即可。详细的cluster用法请参照官方文档。本文章从原理方面解释了node多进程架构以及cluster部分原理,

nodejs是单线程,不能充分利用多核cpu资源,因此要启动多进程,每个进程利用一个CPU,实现多核CPU利用。

一. 共有如下三种方案:

方案1.开启多个进程,每个进程绑定不同的端口,主进程对外接受所有的网络请求,再将这些请求分别代理到不同的端口的进程上,通过代理可以避免端口不能重复监听的问题,甚至可以再代理进程上做适当的负载均衡,由于进程每接收到一个连接,将会用掉一个文件描述符,因此代理方案中客户端连接到代理进程,代理进程连接到工作进程的过程需要用掉两个文件描述符,操作系统的文件描述符是有限的,代理方案浪费掉一倍数量的文件描述符的做法影响了系统的扩展能力。

方案2.作为一种改进,父进程创建socket,并且bind、listen后,通过fork创建多个子进程,通过send方法给每个子进程传递这个socket,子进程调用accpet开始监听等待网络连接。demo如下

// master.jsvar fork =require('child_process').fork;var cpus =require('os').cpus();var server =require('net').createServer()server.listen(1337);for(vari=0;i<cpus.length;i++){  var worker = fork(./worker.js);  worker.send('server', server);}

// worker.jsvar http =require('http')var server =http.createServer(function(req,res){  res.writeHead(200, {'Content-Type':'text/plain'});  res.end('handled by child, pid is ' +process.pid +'\n')})process.on('message',function(m,tcp){  if(m ==='server') {    tcp.on('connection',function(socket){      server.emit('connection',socket);    })  }})

这个时候有多个进程同时等待网络的连接事件,当这个事件发生时,这些进程被同时唤醒,就会产生“惊群问题”。我们知道进程被唤醒,需要进行内核重新调度,这样每个进程同时去响应这一个事件,而最终只有一个进程能处理事件成功,其他的进程在处理该事件失败后重新休眠或其他,浪费性能。

而且这时采用的是操作系统的抢占式策略,谁抢到谁服务,一般而言这是公平的,各个进程可以根据自己的繁忙度来进行抢占,但对于node来说,需要分清他的繁忙度是由CPU,I/O两部分构成的,影响抢占的是CPU的繁忙度,对于不同的业务可能存在I/O繁忙,而CPU较为空闲的情况,这可能造成某个进程抢到较多请求,形成负载不均衡的情况。

方案3.为了解决负载均衡以及消除惊群效应,改进是在master调用accpet开始监听等待网络连接,master来控制请求的给予。将获得的连接均衡的传递给子进程。

改进后的代码

// master.jsvar fork =require('child_process').fork;var cpus =require('os').cpus();var server =require('net').createServer()var workers = []server.listen(1337);server.on('connection',function(socket){  var one_worker =workers.pop();//取出一个worker  one_worker.send('server',socket);  workers.unshift(one_worker);//再放回取出的worker})for(vari=0;i<cpus.length;i++){  var worker = fork(./worker.js);  workers.push(worker);}

// worker.jsvar http =require('http')var server =http.createServer(function(req,res){  res.writeHead(200, {'Content-Type':'text/plain'});  res.end('handled by child, pid is ' +process.pid +'\n')})process.on('message', function(socket){  if(m === 'server') {    server.emit('connection', socket)  }})

但负责接收socket的master需要重新分配发送socket ,而且仅有一个进程去accept连接,效率会降低

node官方的cluster模块就是这么实现的,实质是采用了round-robin轮叫调度算法。

二. 集群稳定之路

1.自动重启:

我们在主进程上要加入一些子进程管理的机制,比如在一个子进程挂掉后,要重新启动一个子进程来继续服务

假设子进程中有未捕获异常发生;

// worker.jsprocess.on('uncaughtException',function(err){  console.error(err);  //停止接收新的连接  worker.close(function(){  //所有已有连接断开后,退出进程    process.exit(1)  })  //如果存在长连接,断开可能需要较久的时间,要强制退出,  setTimeout(function(){    process.exit(1)  }, 5000);})
主进程中监听每个子进程的exit事件
// master.jsvar other_work = {};var createWorker = function() {  var worker = fork('./worker.js')  // 退出时启动新的进程  worker.on('exit',function(){    console.log('worker ' +worker.pid +' exited.');    delete other_work[worker.pid]    createWorker();  })  other_work[worker.pid] = worker;  console.log('create worker pid: ' +worker.pid)}

上述代码中存在的问题是要等到已有的所有连接断开后进程才退出,在极端的情况下,所有工作进程都停止接收新链接,全处在等待退出状态。但在等到进程完全退出才重启的过程中,所有新来的请求可能存在没有工作进程为新用户服务的场景,这会丢掉大部分请求。

为此需要改进,在子进程停止接收新链接时,主进程就要fork新的子进程继续服务。为此在工作进程得知要退出时,向主进程主动发送一个自杀信号,然后才停止接收新连接。主进程在收到自杀信号后立即创建新的工作进程。

worker.js代码改动:

// worker.jsprocess.on('uncaughtException',function(err){  console.error(err);  process.send({act: 'suicide'})//自杀信号  worker.close(function(){    process.exit(1)  })  //如果存在长连接,断开可能需要较久的时间,要强制退出,  setTimeout(function(){    process.exit(1)  }, 5000);})
主进程将重启工作进程的任务,从exit事件的处理函数中转移到message事件的处理函数中
// master.jsvar other_work = {};var createWorker = function() {  var worker = fork('./worker.js')  worker.on('message', function(){     if(message.act === 'suicide'){      createWorker();    }   })  worker.on('exit',function(){    console.log('worker ' +worker.pid +' exited.');    delete other_work[worker.pid]  })  other_work[worker.pid] =worker;  console.log('create worker pid: ' +worker.pid)}

2.限量重启

工作进程不能无限制的被重启,如果启动的过程中就发生了错误或者启动后接到连接就收到错误,会导致工作进程被频繁重启。所以要加以限制,比如在单位时间内规定只能重启

多少次,超过限制就触发giveup事件,告知放弃重启工作进程这个重要事件。

我们引入一个队列来做标记,在每次重启工作进程之间打点判断重启是否过于频繁。在master.js加入如下代码

//重启次数var limit =10;//时间单位var during =60000;var restart = [];var isTooFrequently =function() {  //纪录重启时间  var time =Date.now()  var length =restart.push(time);  if (length >limit) {    //取出最后10个纪录    restart = restart.slice(limit * -1)  }  return restart.length >=limit &&restart[restart.length -1] -restart[0] <during;}
在createWorker方法最开始部分加入判断

// 检查是否太过频繁if (isTooFrequently()) {  //触发giveup事件后,不再重启  process.emit('giveup', length, duiring);  return;}

giveup事件是比uncaughtException更严重的异常事件,giveup事件表示集群中没有任何进程服务了,十分危险。为了健壮性考虑,我们应在giveup事件中添加重要日志,并让监控系统监视到这个严重错误,进而报警等

3.disconnect事件

disconnect事件表示父子进程用于通信的channel关闭了,此时父子进程之间失去了联系,自然是无法传递客户端接收到的连接了。失去联系不表示会退出,worker进程有可能仍然在运行,但此时已经无力接收请求了。所以当master进程收到某个worker disconnect的事件时,先需要kill掉worker,然后再fork一个worker。

// 在createWorker中添加如下代码worker.on('disconnect', function(){  worker.kill();  console.log('worker' + worker.pid + 'killed')  createWorker();})

三. 实际服务器模型



nginx就是图中的反向代理服务器,拥有诸多优势,可以做负载均衡和静态资源服务器。后面的两台机器就是我们的nodejs应用服务器集群。

nginx 的负载均衡是用在多机器环境下的,单机的负载均衡还是要靠cluster 这类模块来做。

nginx与node应用服务器的对比:nginx是一个高性能的反向代理服务器,要大量并且快速的转发请求,所以不能采用上面第三种方法,原因是仅有一个进程去accept,然后通过消息队列等同步方式使其他子进程处理这些新建的连接,效率会低一些。nginx采用第二种方法,那就依然可能会产生负载不完全均衡和惊群问题。nginx是怎么解决的呢:nginx中使用mutex互斥锁解决这个问题,具体措施有使用全局互斥锁,每个子进程在epoll_wait()之前先去申请锁,申请到则继续处理,获取不到则等待,并设置了一个负载均衡的算法(当某一个子进程的任务量达到总设置量的7/8时,则不会再尝试去申请锁)来均衡各个进程的任务量。具体的nginx如何解决惊群,看这篇文章: http://blog.csdn.net/russell_tao/article/details/7204260

那么,node应用服务器为什么可以采用方案三呢,我的理解是:node作为具体的应该服务器负责实际处理用户的请求,处理可能包含数据库等操作,不是必须快速的接收大量请求,而且转发到某具体的node单台服务器上的请求较之nginx也少了很多。

参考文献:

<深入浅出nodejs>

http://blog.csdn.net/russell_tao/article/details/7204260

https://yq.aliyun.com/articles/3068

https://segmentfault.com/a/1190000004621734