多核编程的重要性无需多说, 我们直奔主题,目前nodejs 的网络服务器有以下几种支持多进程的方式:
<br/>
<br/><strong>#1 </strong>开启多个进程,每个进程绑定不同的端口,用反向代理服务器如 Nginx 做负载均衡,好处是我们可以借助强大的 Nginx 做一些过滤检查之类的操作,同时能够实现比较好的均衡策略,但坏处也是显而易见 —我们引入了一个间接层。
<br/>
<br/><strong>#2</strong> 多进程绑定在同一个端口侦听
<br/>在nodejs 中,提供了进程间发送“文件句柄” 的功能,这个功能实在是太有用了(貌似是yahoo 的工程师提交的一个patch)
<br/>不明真相的群众可以看这里: http://www.lst.de/~okir/blackhats/node121.html
<br/>在 node 中我们可以通过以下函数达到效果:
<br/><p><pre><code>stream.write(string, encoding=‘utf8’, [fd])</pre></code></p>
<br/>或在 node v0.5.9+ 中 fork 子进程之后:
<br/><p><pre><code>child.send(message, [sendHandle])</pre></code></p>
<br/>
<br/>所以我们设计以下方案:master 进程生成了listen 端口之后,发送这个 listenfd 给所有的worker 子进程,worker 子进程接收到handle 之后,执行listen 操作:
<br/>
<br/>master :
<br/><pre><code>function startWorker(handle){
<br/> output(“start workers :” + WORKER_NUMBER);
<br/> worker_succ_count = 0;
<br/> for(var i=0; i<WORKER_NUMBER; i++){
<br/> var c = cp.fork(WORKER_PATH);
<br/> c.send({“server” : true}, handle);
<br/> }
<br/>}
<br/>
<br/>function startServer(){
<br/> var tcpServer = net.createServer();
<br/> tcpServer.on(“error”, function(err){
<br/> output(“server error ,check the port…”);
<br/> about_exit();
<br/> })
<br/> tcpServer.listen(PORT , function(){
<br/> startWorker(tcpServer._handle);
<br/> tcpServer.close();
<br/> });
<br/>}
<br/>
<br/>startServer();</pre></code>
<br/>注意,因为我们只需要一个handle ,httpServer 其实是netServer 的一层封装,所以我们在master进程启动netServer ,发送这个listen套接字 “handle” 到各个子进程
<br/>worker :<pre><code>server = http.createServer(function(req, res){
<br/> var i,r;
<br/> for(i=0; i<10000; i++){
<br/> r = Math.random();
<br/> }
<br/> res.writeHead(200 ,{“content-type” : “text/html”});
<br/> res.end(“hello,world”);
<br/> child_req_count++;
<br/>});
<br/>
<br/>process.on(“message”,function(m ,handle){
<br/> if(handle){
<br/> server.listen(handle, function(err){
<br/> if(err){
<br/> output(“worker listen error”);
<br/> }else{
<br/> process.send({“listenOK” : true});
<br/> output(“worker listen ok”);
<br/> }
<br/> });
<br/> …
<br/> });</pre></code>
<br/>
<br/>worker 进程收到handle后,立即进行listen ,这样就会有多个worker进程 listen同一个socket端口,即同一个套接字被加入到多个进程的epoll 监控结构中,当一个外部连接到来时,此时只有一个幸运的worker 进程得到激活事件,接收这个连接。(在UNP 中讲到这种情况下会导致 “惊群” 效应,但据江湖传闻2.6以上的Linux 系统中,阻塞式的listenfd 已消除惊群现象,非阻塞的listenfd 依然存在,即我们的epoll还是会存在这个问题的,但个人认为nodejs 的epoll 结构中往往有很多的监控句柄而非仅listenfd,所以这时候惊群造成的影响应该是比较小的…)
<br/>
<br/>我们开5个worker 测试 (以下测试均为开启keep-alive模式,本机测试):
<br/>
<br/>测试业务如上代码所示:运行10K次 Math.random(), 然后输出 ”hello,world“;
<br/>
<br/>系统配置:
<br/><blockquote>Linux 2.6.18-164.el5xen x86_64
<br/>
<br/>CPU X5 ,Intel® Xeon® CPU E5620 @ 2.40GHz
<br/>
<br/>free -m
<br/> total used free shared buffers cached
<br/>Mem: 7500 3672 3827 0 863 1183
<br/>
<br/></blockquote>
<br/>
<br/><p>siege -c 100 -r 1000 -b localhost:3458/ </p>
<br/>
<br/>结果为:
<br/>
<br/><blockquote>ransactions: 100000 hits
<br/>Availability: 100.00 %
<br/>Elapsed time: 10.95 secs
<br/>Data transferred: 1.05 MB
<br/>Response time: 0.01 secs
<br/>Transaction rate: 9132.42 trans/sec
<br/>Throughput: 0.10 MB/sec
<br/>Concurrency: 55.61
<br/>Successful transactions: 100000</blockquote>
<br/>
<br/>5 个worker 处理的请求量分别是:
<br/><blockquote>child req total : 23000
<br/>child req total : 16000
<br/>child req total : 17000
<br/>child req total : 22000
<br/>child req total : 22000</blockquote>
<br/>
<br/>再测一次:
<br/><blockquote>child req total : 13000
<br/>child req total : 30000
<br/>child req total : 14000
<br/>child req total : 22000
<br/>child req total : 21000</blockquote>
<br/>
<br/>在这种情况下,我们的负载均衡是建立在各个worker“随机接收“的特征基础上的,由操作系统来保证的,长期运行情况下应该是均衡的,但短期内还是会有可能导致负载倾斜的现象,特别是在客户端使用keep-alive连接并长期不关闭的情况下。
<br/>
<br/>
<br/><strong>#3</strong> 一个进程负责监听、接收连接,然后把接收到的连接平均发送到子进程中去处理
<br/>
<br/>我们先看一下正常情况下一个http server 服务的流程 ,其大体可分为几 个阶段:
<br/>
<br/><em>listenfd 绑定侦听</em> -> <em>接收到的Tcp 连接对象</em> → <em>包装成socket 对象</em> → <em>生成(req ,res)对象</em> -> <em>调用用户代码</em>
<br/>
<br/>
<br/><blockquote>
<br/>(#1) TCP.bind — > TCP.listen (process.binding(“tcp_wrap”))
<br/> |
<br/> |TCP.emit(“connection” ,handle)
<br/> |
<br/>(#2) Wrap TCP handle to Socket (Tcp.onconnection)
<br/> |
<br/> |Net.Server.emit(”connection” , socket)
<br/> |
<br/>(#3)Create Req ,Res based on a Socket(net.server.connectionListen)
<br/> |
<br/> |Http.server.emit(“request” ,req ,res)
<br/> |
<br/>(#4)your code writen here :function(req ,res){
<br/> res.writeHead(200 ,“content-type/text/html”);
<br/> res.end(“hello,world”)
<br/>}
<br/></blockquote>
<br/>
<br/>
<br/>nodejs 的child.send(message, [sendHandle]) 函数 ,此处的 sendHandle 这时应该为一个 tcp_wrap 对象,所以我们不能直接使用 net.createServer 返回给我们的socket ,否则的话我们需要”回滚“ 从Tcp 到 Socket 这一步骤,不仅浪费资源,同时也是不安全的,所以我们在tcpMaster 中 直接使用 tcp_wrap :
<br/><pre><code>
<br/>var TCP = process.binding(“tcp_wrap”).TCP;
<br/>
<br/>var childs = [];
<br/>var last_child_pos = 0;
<br/>function startWorker(){
<br/> for(var i=0; i<WORKER_NUMBER; i++){
<br/> var c = cp.fork(WORKER_PATH);
<br/> childs.push©;
<br/> }
<br/>}
<br/>function startServer(){
<br/> server = new TCP();
<br/> server.bind(ADDRESS, PORT);
<br/> server.onconnection = onconnection;
<br/> server.listen(BACK_LOG);
<br/> }
<br/>function onconnection(handle){
<br/> //output(“master on connection”);
<br/> last_child_pos++;
<br/> if(last_child_pos >= WORKER_NUMBER){
<br/> last_child_pos = 0;
<br/> }
<br/> childs[last_child_pos].send({“handle” : true}, handle);
<br/> handle.close();
<br/>}
<br/>startServer();
<br/>startWorker();
<br/></pre></code>
<br/>以上为tcpMaster 进程把接收的tcp 连接 均匀分配给 tcpWorkers :
<br/>
<br/><pre><code>function onhandle(self, handle){
<br/> if(self.maxConnections && self.connections >= self.maxConnections){
<br/> handle.close();
<br/> return;
<br/> }
<br/> var socket = new net.Socket({
<br/> handle : handle,
<br/> allowHalfOpen : self.allowHalfOpen
<br/> });
<br/> socket.readable = socket.writable = true;
<br/> socket.resume();
<br/> self.connections++;
<br/> socket.server = self;
<br/> self.emit(“connection”, socket);
<br/> socket.emit(“connect”);
<br/> }
<br/>server = http.createServer(function(req, res){
<br/> var r, i;
<br/> for(i=0; i<10000; i++){
<br/> r = Math.random();
<br/> }
<br/> res.writeHead(200 ,{“content-type” : “text/html”});
<br/> res.end(“hello,world”);
<br/> child_req_count++;
<br/> });
<br/>}
<br/> process.on(“message”,function(m ,handle){
<br/> if(handle){
<br/> onhandle(server, handle);
<br/> }
<br/> if(m.status == “update”){
<br/> process.send({“status” : process.memoryUsage()});
<br/> }
<br/> }); </pre></code>
<br/>
<br/>以上为tcpWorker 将接收到的tcp handle 封装成socket ,为了充分的与http.server类兼容,我们还对connections的数量进行检查,并把socket.server 设为当前的server ,然后激发http.server 的 ”connection“ 事件.
<br/>
<br/>通过这种方式,我们用尽量小的开销,在充分保证http.server 类的兼容性的前提下,用尽量少而优雅的代码实现了负载均衡与高效并行。
<br/>测试结果如下:
<br/><blockquote>
<br/>ransactions: 100000 hits
<br/>Availability: 100.00 %
<br/>Elapsed time: 10.47 secs
<br/>Data transferred: 1.05 MB
<br/>Response time: 0.01 secs
<br/>Transaction rate: 9551.10 trans/sec
<br/>Throughput: 0.10 MB/sec
<br/>Concurrency: 60.68
<br/>Successful transactions: 100000</blockquote>
<br/><blockquote>child req total : 20000
<br/>child req total : 20000
<br/>child req total : 20000
<br/>child req total : 20000
<br/>child req total : 20000</blockquote>
<br/>数据会有所起伏,qps 总体在 8000~11000 范围内,注意以上worker 数目均设为5个,适量增大worker数目,qps 可以稳定达到10k,但这时系统load比较高,使用时需谨慎选择。
<br/>
<br/>几次测试完成后,我们查看/proc/[tcpMaster]/fd , 其占用的端口如下:
<br/><blockquote>0 -> /dev/pts/30
<br/>1 -> /dev/pts/30
<br/>10 -> socket:[71040]
<br/>11 -> socket:[71044]
<br/>12 -> socket:[71054]
<br/>2 -> /dev/pts/30
<br/>3 -> eventpoll:[71027]
<br/>4 -> pipe:[71028]
<br/>5 -> pipe:[71028]
<br/>6 -> socket:[71030]
<br/>8 -> socket:[71032]
<br/>9 -> socket:[71036]</blockquote>
<br/>查看其中一个tcpWorker:
<br/><blockquote>0 -> socket:[71031]
<br/>1 -> /dev/pts/30
<br/>2 -> /dev/pts/30
<br/>3 -> eventpoll:[71049]
<br/>4 -> pipe:[71050]
<br/>5 -> pipe:[71050]</blockquote>
<br/>tcpMaster 的fds 意义分别如下:
<br/><ul>
<br/> <li>1个socket为listenfd </li>
<br/> <li>5个socket 用作父子进程通信 </li>
<br/> <li>2个pipe(一对)用于asyn_watcher/signal_watcher 的触发</li>
<br/> <li>剩余的不解释…</li>
<br/></ul>
<br/>
<br/>tcpWorker 的fds 意义分别如下:
<br/><ul>
<br/> <li>1个socket(这儿就是stdin)用作与父进程通信</li>
<br/> <li>其余fd与master中fd作用类似</li>
<br/></ul>
<br/>
<br/>所以tcpMaster/tcpWorker 端口占用正常,没有句柄泄露问题,负载均衡可控,但负责接收socket的master需要重新分配发送socket ,引入了额外的开销.
<br/>
<br/>小结:
<br/>
<br/>本文介绍了2种比较高效的多进程运行方式,两种方式各有优劣,需要使用者自行选择,在node v0.5.10+ 中,内置了cluster 库,不过在我看来,其宣传意义大于实用意义,因为这样官方就可理直气壮的宣称直接支持多进程运行方式,nodejs 官方为了让API 接口傻瓜化,用了一些比较trick 的方法,代码也比较绕,且这种多进程的方式,不可避免的要牵涉到进程通信、进程管理之类的东西,但我们往往有自己的需求,现在nodejs官方把它固化到lib中,我们就无法自由的更改添加一些功能。
<br/>
<br/>此外,有两个node 的module ,multi-node 和 cluster ,采用的策略和本文介绍的类似,但使用这些module往往有一些缺点:
<br/><ul>
<br/> <li>更新不及时</li>
<br/> <li> 复杂庞大,往往绑定了很多其他的功能,用户往往被绑架</li>
<br/> <li> 遇到问题难以hack</li>
<br/></ul>
<br/>
<br/>
<br/>基于本文的介绍,你可以很方便的打造自己的高性能的、易维护的、最简的、优雅实用的cluster ,enjoy it!
<br/>
<br/>源码地址:<a href=“https://github.com/windyrobin/iCluster” title=“https://github.com/windyrobin/iCluster”>https://github.com/windyrobin/iCluster</a>
<br/>
<br/>以下文章有些老,但和本文的策略很相似(俺是独立构思完后看到的,别喷俺抄袭哦):
<br/>http://developer.yahoo.com/blogs/ydn/posts/2010/07/multicore_http_server_with_nodejs/
<br/>
惊群效应:指一个fd的事件被触发后,等候这个fd的所有线程都被唤醒。 <br/>哈哈,终于知道爱多是谁了。。原来我每天都跟他见面。。。网络多么可怕。。。
onhandle的精华我想应该是使用子进程的server代替父进程原来listen的server吧,相关node的源代码可以参考net模块的onconnection部分代码: <br/>https://github.com/joyent/node/blob/master/lib/net.js#L749
雅虎那篇之后可继续参考这篇文章:http://blog.std.in/2010/07/08/nodejs-webworker-design/ 作者当时在雅虎,后来好像去了脸谱
如果真有一天node.js可以像php那样大量用于生产环境,一般中小型项目还是离不开nginx,毕竟用nginx做负载均衡比lvs等要廉价许多。 <br/>同时当拥有多个node.js服务器时,将niginx和cluster两者一起结合起来,部署的服务器代价至少是php的一半。
看好 node 自带的cluster
坑爹的Windows连自带的 ‘cluster’ 都不支持,郁闷啊…
[…] 单进程无法完全利用CPU资源:Cluster:https://github.com/LearnBoost/cluster 可以很好地解决多核利用问题,而nodejs 0.6+以上版本将直接支持cluster参数启动。关于多进程同时监听同一端口的问题,可以查看 http://cnodejs.org/blog/?p=3471 […]
[…] 单进程无法完全利用CPU资源:Cluster:https://github.com/LearnBoost/cluster 可以很好地解决多核利用问题,而nodejs 0.6+以上版本将直接支持cluster参数启动。关于多进程同时监听同一端口的问题,可以查看 http://cnodejs.org/blog/?p=3471 […]
[…] 单进程无法完全利用CPU资源:Cluster:https://github.com/LearnBoost/cluster 可以很好地解决多核利用问题,而nodejs 0.6+以上版本将直接支持cluster参数启动。关于多进程同时监听同一端口的问题,可以查看 http://cnodejs.org/blog/?p=3471 […]
最近也在研究这个问题。cluster实现的是多个进程分享同一个端口的请求。貌似还不能实现根据请求的不同(比如请求的域名不同) 分配到指定的worker上。
问一下,第三种方式childs[last_child_pos].send({“handle” : true}, handle);这里运行时报错: child_process.js:135 throw errnoException(errno, ‘write’, ‘cannot write to IPC channel.’); ^ Error: write ENOTSUP - cannot write to IPC channel. at errnoException (child_process.js:483:11) at ChildProcess.send (child_process.js:135:13) at TCP.onconnection (C:\Users\Administrator\Desktop\windyrobin-iCluster-ce7e656\windyrobin-iCluster-ce7e656\tcpMaster.js:63:24) 是什么原因。windows下,node 0.6.17
没玩过windows,没记错的话,在一些版本中,fork 方法貌似的确有不兼容的问题
@windyrobin 嗯。今天下了最新版本v0.8.0,it’s ok.
也就是说用Nginx做多服务器的负载均衡,用node-cluster做一台机器多CPU的负载均衡吧?
node-cluster应该是不能跨服务器玩的吧?另外给第一个CPU分了2000个请求,如果处理第一个请求的时候挂了,剩下那1999个请求咋办。。纠结。
在windows 下这个能跑得通吗?你下了v0.8.8也跑不起来啊。你是怎么跑起来的?
重新拜读,依然受益匪浅。
关于 #3
server = http.createServer(function(req, res){
...
}
process.on('message', function(m, handle){
...
server.emit('connection', handle)
})
如果我没有理解错的话,这个server(worker)是从未listen过的,所以他不会主动获取任何请求,通过 self.emit('connection', handle)
, 这个server(worker)就会一直pull这个connection中的新消息,而其他的worker,则不会理会这个connection。
此处的handle和 #2 中的 server.listen(handle)
不是一个概念。 #2 中的handle是一个以listen模式工作的socket,会响应connection事件,而 #3 中的handle则是一个connection,只会响应data事件。
最后还有一个问题,下面的代码是何意啊?求解答
var TCP = process.binding("tcp_wrap").TCP;
的确是的,listen 操作仅在 进程需要接受外来连接时,及 socket api 中的 accpect 时才需要,第二种方式worker仅需要 处理极影accept 过的socket,所以不需listen
我之所以使用 TCP ,因为注意了其 层次关系 tcp -> socket server -> http server 其存在一定的层次关系,我们仅需要在master 做什么呢? 仅需接受一个tcp socket 罢了, 所以我们直接使用node 提供的最底层的 tcp 接口,其实调用 net.createServer ,来listen,accept 也是ok的,但我们仅需要最简洁最裸露,最小干扰、最大控制力的接口,你仔细看下 这个顺序: listenfd 绑定侦听 -> 接收到的Tcp 连接对象 → 包装成socket 对象 → 生成(req ,res)对象 -> 调用用户代码
0。8肯定跑不过,是0.6下的
前期开发时是不是可以不用考虑多核的问题,到部署时再做很小的更改就行了?
谢谢 , 学习了.