新興的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的下一個版本一起發布。
-------------------------------------------
node.js官方於1月18日發布了0.9.7(Unstable)新版本,已經包含了此改進。