2012年4月6日 星期五

用 Node.js 實作多個 Process 監聽並處理同一個 Port

Standard
Node.js 實作了一個內建的模組『cluster』,目的在於提供開發者一個容易的方式,針對多核心機器進行支援。可惜的是,截至今天 Node.js v0.6.14 版本,『cluster』功能還是不夠完整,從官方文件上可以得知,其 API 仍處於實驗(Experimental)階段,在日後會有極大的變動(Drastic changes in future versions)。

而就 git repository 上最新的 Node.js 開發版本來看,『cluster』模組已經大幅度更改,也從簡單的 function call 變成了物件的設計。所以,目前非常不建議在自家產品中大量使用 『cluster』,至少短期內,他都不是穩定可用的,會有很大的改動空間。

不過,你如果是要實作 Web Service,會想要使用『cluster』的目的就顯而易見:『透過多個 Process 分擔流量和工作量』。若只是單單要做到這一點,我們可以自己來實作。

通常我們有兩種方式達成這個需求:

  • 反向代理(Reverse Proxy) 技術
  • 使用多個程序(Process) 監聽並處理同一個 Port

前者的做法,是建立一個常駐程式或機器,統一處理所有連線的需求,然後將這些連線做流量平衡(Load Balance),轉送並分配給後端多個程序或機器處理,待處理完成後,再將後端的回應送回給使用者。這樣的 Proxy 機制,你可以用已經相當成熟的解決方案達成,像是 Nginx 或 Apache。亦或是自己使用 Node.js 撰寫,但由於這不是本文的重點,日後再來討論這個實作的細節。

而後者,使用多個程序(Process) 監聽並處理同一個 Port,是本文的主題,也是『cluster』模組正在做的事。如果你有看過『cluster』的程式碼,就會發現它其實主要運用了兩個技術:『取得 net._handle』和『利用 process.send() 傳送 Handle 給子行程(child process)』。

知道了關鍵後,實作上就不是什麼大問題,主要流程是:

  1. 運用『net』建立一個 Server,目的在於監聽(listen) 80 Port
  2. 建立多個子行程(child process) 準備處理連線
  3. 將『net』的內建 Handle 傳給子行程(child process)
  4. 在子行程(child process) 接收主程序傳來的 Handle
  5. 在建立 HTTP Server 時,直接使用主程序傳來的 Handle
  6. 關閉只用來初使化 Handle 的 Server


主程式的範例如下:
var child_process = require('child_process');
var net = require('net');

var tcpSrv = net.createServer();
tcpSrv.listen(80, function() {
    for (var i = 1; i <= 4; i++) {
        var worker = child_process.fork('worker.js');
        worker.send(i, tcpSrv._handle);
    }

    tcpSrv.close();
});

worker.js 的程式:
var http = require('http');

process.on('message', function(id, handle) {
    http.createServer(function(req, res) {
        res.writeHead(200, {'Content-Type': 'text/plain'});
        res.end('Worker ' + id);
    }).listen(handle);
});

如此,就可以讓所有子行程共同監聽 80 port,透過瀏覽器來看,你應該可以隨機看到『Worker 1』、『Worker 2』、『Worker 3』或『Worker 4』的字樣,分屬不同 Process 回傳的網頁內容。

後記

能這樣使用的原因是,在  Node.js 中,『http』是繼承『net』的衍生實作,所以他們的底層 handle 可以通用。此外,此做法不限於 HTTP Server,也可以在用於一般 TCP Server。

差點忘了提到,Express Web Framework 這類的框架,都是繼承『http』,所以都可以使用相同的做法。