iT邦幫忙

DAY 30
4

node.js伺服器實戰系列 第 30

node.js伺服器實戰(30) - scaling node.js

Javascript有一個特色,就是使用單一執行緒的Event Loop來執行所有的事件(函數)。這個模型讓他反應速度很快,但是有一些後遺症。
node.js的生命週期

當node.js程式開始執行時,會先把在global scope的程式碼執行完畢,其他定義在函數內的而且沒有在global scope內立即執行的程式,要等到函數被事件觸發才會執行。global執行完畢後,node.js會進入一個event loop,所有觸發的事件函數會依照觸發的先後順序放進一個佇列,用一個無窮迴圈一一取出執行。這個結構是單一執行緒的,而且不會有兩個函數同時執行。

對於node.js來說,所有的I/O,包括網路連線等等(例如http的request事件),都只是一個執行函數的動作,所以額外負擔很小,反應速度很快。

後遺症

這個執行模式有兩個問題,首先,如果某個事件函數跑太久,就會影響到其他函數的執行。這個是Javascript的天生限制,所以比較不適合執行許多需要大量運算的狀況。

其次,每個instance只會用到一個執行緒,對於目前CPU一般都是多核心的環境來說,很難完整利用系統資源。

反向代理

對於資源沒有發揮的問題,一般常見的解決方式是,在node.js程式之前,執行一個反向代理程式或是伺服器,例如nginx、或是使用node.js的node-http-proxy模組,利用簡單的規則來建立反向代理。透過反向代理,就可以依照核心數量合理的執行更多的instances,徹底發揮硬體的效能。

如果需要跨機器做load balance,利用代理伺服器甚至load balancer都是解決的方法。但是對於只是要在同一台執行多個instance的需求,剛發佈的node-v0.6.0提供了另外的解決方式。

node.js的cluster模組

node-v0.6.x的重大改進,除了完整支援Windows作業系統,在child_process裡面還新增了一個fork功能。node.js新增的cluster模組就是利用這個功能,在伺服器程式開始執行時,建立多個worker instance,監聽同一個port。透過這個方式,可以解決部份資源利用不足的問題。

最簡單的cluster,是在執行node時增加cluster參數,這個時候node會偵測系統的核心數,並且依照核心數執行同樣數量的worker。

對於有需要自己控制instance數量的情況,node.js提供了cluster模組,可以在程式中拿來做更好的控制。

使用起來有點像在寫Linux的Daemon,簡單的程式像這樣:

 var cluster = require('cluster');
 var http = require('http');
 var numCPUs = require('os').cpus().length;
 
 if (cluster.isMaster) {
   // Fork workers.
   for (var i = 0; i < numCPUs; i++) {
     cluster.fork();
   } 
 
   cluster.on('death', function(worker) {
     console.log('worker ' + worker.pid + ' died');
   });
 } else {
   // Worker processes have a http server.
   http.Server(function(req, res) {
     res.writeHead(200);
     res.end("hello world\n");
   }).listen(8000);
 }

利用cluster.isMaster可以判斷目前是在主程式還是在fork出去的child process,如果是在主程式,就利用cluster.fork()產生worker,如果是child process,就直接執行伺服器程式。

接下來應用在自己的伺服器程式上吧。為了可以設定worker數量,先把config移出伺服器程式,再利用require載入(config.js):

 module.exports = {
     workers: 3,
     dirindex: ['default.htm', 'index.html']
 };

伺服器本身沒改動,改動的是使用伺服器的程式(examples/simple.js):

 var Evolve = require('../lib/evolve');
 var tools = require('../lib/tools');
 var path = require('path');
 var cluster = require('cluster');
 var config = require('./config');
 
 if(cluster.isMaster) {
     var i=0, l=config.workers||2;
     for(; i<l; i++) {
         var worker = cluster.fork();
     }
     cluster.on('death', function(worker) {
         console.log('worker: ' + worker.pid + ' died.');
         cluster.fork();
     });
 } else {
 
     var server = new Evolve(config)
     .handle('pre',  tools.cookieHandler)
     .host('localhost:8443')
     .map('/', path.join(__dirname, '../www'))
     .get('/hello', function (request, response, cb) {
         cb(false, '/hello', {
             type: 'text/html',
             data: 'hello'
         }, true);
     })
     .get('/hello_mvc', function (request, response, cb) {
         var HelloModel = require('./models')['hello_mvc'];
         var HelloView = require('./views')['hello_mvc'];
         var m = new HelloModel(new HelloView(cb));
         m.execute();
     })
     .get('/hello_mvc1', function (request, response, cb) {
         var HelloModel = require('./models')['hello_mvc1'];
         var HelloView = require('./views')['hello_mvc1'];
         var m = new HelloModel(new HelloView(cb));
         m.execute();
     })
     .get('/hello_mvc2', function (request, response, cb) {
         var HelloModel = require('./models')['hello_mvc2'];
         var HelloView = require('./views')['hello_mvc2'];
         var m = new HelloModel(new HelloView(cb));
         m.execute();
     })
     .listen(8443, 'localhost');
 }

在config.js中設定要跑三個worker,啟動時會看到:

透過procexp.exe可以看到:

總共有四個node.exe行程在執行,其中三個是子行程。

不過如何分配負載並不是node.js的工作...據說是交給作業系統處理,通常是使用round robin演算法,所以效果不一定很好。

如果worker與master需要互相溝通,可以利用process的message事件以及send方法,來傳送訊息。

相關文章


上一篇
node.js伺服器實戰(29) - 簡化非同步操作
下一篇
node.js伺服器實戰(31) - 結語
系列文
node.js伺服器實戰33

1 則留言

0
jamesjan
iT邦高手 1 級 ‧ 2011-11-10 09:05:26

恭喜費大完成鐵人賽灑花拍手讚

fillano iT邦超人 1 級 ‧ 2011-11-10 23:26:07 檢舉

還沒作結哪,不過先謝啦!!

我要留言

立即登入留言