高並發下的Node.js與負載均衡


新興的Node.js已經吸引了很多開發人員的眼光,它提供給我們一個快速構建高性能的網絡應用的平台。我也開始逐步投入node.js的懷抱,在學習和使用的過程中,遇到了一些問題,也有一些經驗,我覺得有必要寫出來,作為總結,也用作分享。

眾所周知,node.js基於v8引擎,所以它本身並不支持多線程(有多線程的Module哦),那么為了充分利用server的Multi-core,就必須使用多進程的方式。那么進程之間如何負載均衡就會是一個關鍵所在。

多進程共享監聽socket

Node.js與進程相關的模塊有processchild_processcluster,這其中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進程間的通信,並使用sendmsgrecvmsg等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)新版本,已經包含了此改進。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM