好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

高并发下的Node.js与负载均衡

高并发下的Node.js与负载均衡

新兴的 Node.js 已经吸引了很多开发人员的眼光,它提供给我们一个快速构建高性能的网络应用的平台。我也开始逐步投入node.js的怀抱,在学习和使用的过程中,遇到了一些问题,也有一些经验,我觉得有必要写出来,作为总结,也用作分享。

众所周知,node.js基于 v8 引擎,所以它本身并不支持多线程(有多线程的 Module 哦),那么为了充分利用server的Multi-core,就必须使用多进程的方式。那么进程之间如何负载均衡就会是一个关键所在。

多进程共享监听socket

Node.js与进程相关的模块有 process , child_process , cluster ,这其中cluster用于方便的创建共享端口的多进程模式(The cluster module allows you to easily create a network of processes that all share server ports),这种模式使多个进程间共享一个监听状态的socket,并由系统将accept的connection分配给不同的子进程,而且实现起来也非常简单,cluster为你做了大部分事情,这里有一个test case:

  1   var  cluster = require('cluster' );
   2   var  http = require('http' );
   3   var  numCPUs = require('os' ).cpus().length;
   4  
  5   if   (cluster.isMaster) {
   6     //   Fork workers. 
  7     for  ( var  i = 0; i < numCPUs; i++ ) {
   8       cluster.fork();
   9     }
  10  
 11    cluster.on('exit',  function  (worker, code, signal) {
  12      console.log('worker ' + worker.process.pid + ' died' );
  13     });
  14  }  else   {
  15     //   Workers can share any TCP connection 
 16     //   In this case its a HTTP server 
 17    http.createServer( function  (req, res) {
  18      res.writeHead(200 );
  19      res.end("hello world\n" );
  20    }).listen(8000 );
  21  }

但是这种完全依赖于系统的负载均衡存在着一个重要缺陷:在linux和Solaris上,只要某个子进程的accept queue为空(通常为最后创建的那个子进程),系统就会将多个connetion分配到同一个子进程上,这会造成进程间负载极为不均衡。特别是在使用长连接的时候,单位时间内的new coming connection并不高,子进程的accept queue往往均为空,就会导致connection会不停的分配给同一个进程。所以这种负载均衡完全依赖于accept queue的空闲程度,只有在使用短连接,而且并发非常高的情况下,才能达到负载均衡,但是这个时候系统的load会非常高,系统也会变得不稳定起来。

Nginx 是怎么做的?

如果你了解nginx,那么你可能第一时间会想到使用nginx的处理方式,nginx有一个master和多个worker进程,master进程监听端口,负责accept connection,并把accept 的socket发送给各worker进程,由worker进程接收数据并处理。linux下,nginx是使用 socketpair 建立master和worker进程间的通信,并使用 sendmsg 、 recvmsg 等api来传输命令和文件描述符的。那么node.js是否支持这种方案呢?

答案是肯定的,作出这个回答的依据在于node.js的child_process和cluster模块均有一个send方法: child.send(message, [sendHandle])

这个方法的第二个参数就是我们想要传递的socket,而且node.js文档上还给出了一个test case:

  1   var  normal = require('child_process').fork('child.js', ['normal' ]);
   2   var  special = require('child_process').fork('child.js', ['special' ]);
   3   //   Open up the server and send sockets to child 
  4   var  server = require('net' ).createServer();
   5  server.on('connection',  function   (socket) {
   6     //   if this is a VIP 
  7     if  (socket.remoteAddress === '74.125.127.100' ) {
   8      special.send('socket' , socket);
   9       return  ;
  10     }
  11     //   just the usual dudes 
 12    normal.send('socket' , socket);
  13   });
  14  server.listen(1337);

child.js

 1  process.on('message',  function  (m, socket) {
  2     if  (m === 'socket' ) {
  3      socket.end('You were handled as a ' + process.argv[2] + ' person' );
  4     }
  5  });

简单,精炼!似乎是一个完美的解决方案。我们稍微加工一下,让他成为一个可以正常运行的http server:

master.js

  1   var  http = require('http' ),
   2      numCPUs = require('os' ).cpus().length;
   3      cp = require('child_process' ), 
   4      net = require('net' );
   5   var  workers =  [];
   6   for  ( var  i = 0; i < numCPUs; i++ ) {
   7      workers.push(cp.fork('app.js', ['normal' ]));
   8   }
   9  
 10  net.createServer( function  (s) {
  11       s.pause();
  12       var  worker =  worker.shift();
  13      worker.send('c' ,s);
  14       workers.push(worker);
  15  }).listen(80);

  1   var  http = require('http' ),
   2      cp = require('child_process' ),
   3      net = require('net' );
   4   var  server = http.createServer( function  (req,res){
   5      res.writeHead(200, {"Content-Type": "text/plain",         "Connection": "close" });
   6      res.end("hello, world" );
   7   });
   8  console.log("webServer started on " +  process.pid);
   9  process.on("message",  function  (msg,socket) {
  10      process.nextTick( function  (){
  11           if (msg == 'c' &&  socket) {
  12              socket.readable = socket.writable =  true  ;
  13               socket.resume();
  14              server.connections++ ;
  15              socket.server =  server;
  16              server.emit("connection" , socket);
  17              socket.emit("connect" );
  18           }
  19       });
  20   });
  21                                          

我们在worker进程中创建了一个http server,但是这个http server并不监听,也不绑定端口,在收到master传输过来的socket时,调用server.emit("connection", socket);就可以触发server的connection事件了。看起来很不错,简单的测试之后可以正常工作,这个方案几近于完美。在经历过共享监听socket方案的失败后,我们把服务迁移到这种架构上来。

但是,我们遇到了问题。 我们发现master进程的cpu和内存在逐渐增长,并最终到达100%,或者node.js崩溃(Assertion `fd_to_send >= 0' failed),这令我很抓狂,百般无奈之下我们求助于node.js的开发人员,在github上报告了我们遇到的问题( Issue #4587 )。就在当天晚上,node.js的开发人员 indutny 找到了问题所在,主要在于主进程在将socket发送给子进程之后,并没有销毁,而是保留在socketList中,这会导致socket在主进程中逐步累积,并最终达到上限。

indutny 很快解决了这个问题,于第二天提交了这个 commit ,按照这个commit, indutny 给send函数增加了第三个可选参数,修改后的send函数将变为:

child.send(message,[socket], [{ track: false, process: false }])

我们的master进程不需要track每个socket状态,所以我们将它设为false即可。到此,这个问题得到了完美的解决,希望这个改进可以随node.js的下一个版本一起发布。

 

 

分类:  Web ,  Node.js

标签:  node.js ,  多进程 ,  高并发 ,  负载均衡

作者: Leo_wl

    

出处: http://www.cnblogs.com/Leo_wl/

    

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

版权信息

查看更多关于高并发下的Node.js与负载均衡的详细内容...

  阅读:50次

上一篇: GCC知识

下一篇:asp.net微软图表控件MsChart