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方法,來傳送訊息。