譯自:http://developer.yahoo.net/blog/archives/2010/07/multicore_http_server_with_nodejs.html(稍有修改), 轉載請指明原文鏈接:http://www.grati.org/?p=307
簡單的說,NodeJS是一個使用了Google高效能 V8引擎 的服務器端JavaScript實做。它提供了一個(幾乎)完全非阻塞I/O,與JavaScript提供的閉包和匿名函數結合,使之成為寫撰寫高流量網路服務程序的優秀平台。在我們內部,雅虎郵件隊正調研能否使用NodeJS開發一些我們即將推出的新服務。我們認為分享我們的勞動成果是一件十分有意義的事情。
目錄
在多處理器系統上使用NodeJS的情況
NodeJS中並不是完美無缺的。雖然單一行程的性能表現相當不錯,但一個CPU最終還是不夠用(由於JS引擎自身的運行原理,NodeJS使用單行程執行JS程式,詳見「JS和多執行序」)。Node本身並沒有擴展能力來充分利用多CPU系統的計算能力。實際上當前版本的NodeJS程序只能在一個上CPU執行,在2.5GHz的英特爾至強處理器下運行HTTP代理服務的性能大約為2100 reqs/s。
雖然Node相對穩定,但它仍然會偶爾崩潰。如果你實用一個單獨的NodeJS 行程作為服務,崩潰會對可用性造成不良影響。例如段暫存器錯誤 記憶體溢位等錯誤在用C++編寫的程序上相當普遍。如果有多個行程同時處理請求,當一個行程出錯退出,傳入的請求可以被導向給其他行程
。
充分利用多核心處理器的優勢
有如下幾種方法可以使NodeJS利用多處理器,每個方法都有自己的優缺點。
直到node-v0.1.98
,充分利用多處理器的最佳做法是為每個處理器單獨啟動一個NodeJS進程,每個行程都運行HTTP服務並綁定到不同的端口。這樣需要一個負載平衡軟體,將客戶端請求轉發到各行程,這個軟體知道每個服務行程
的port。這樣處理效能也不錯,但配置管理多行程比較複雜,因此不是最佳方案。
當然,這種架構也有好處,它允許負載平衡軟體按照指定的策略將請求路由到不同行程上。(例如,透過IP,透過cookie等)。
在node-v0.1.98
中 ,雅虎貢獻了一個用於傳遞和重用文件描述符的核心patch,允許如Connect和multi-node等HTTP框架使用多個行程同時提供HTTP服務,而且不需要修改原有的程式碼和配置。
概括的講,這些框架使用的方法是建立一個行程監聽埠(比如說監聽埠80)。然而,這個行程不是接受Socket連接,而是使用net.Stream.write()將其傳遞給了其他子行程(其內部是使用sendmsg(2)發送,並使用recvmsg來獲取文件表描述符)。每個子行程排隊將收到的文件描述符插入自己的事件迭代中並在空閒時處理客戶端的連接。OS核心本身負責行程間的負載平衡。
重要的是,這實際上是一個高效但沒有策略的L4負載平衡器,每個客戶端的請求可能被任意一個行程處理。任何處理請求所需的應用程序的狀態,都不能像單行程時那樣簡單的保存在一個NodeJS實例當中。
某些情況下,你可能可能無法使用或者不想使用上述兩種方法。例如,負載均衡程序無法按照應用程序所需的路由規則轉發請求(如,有複雜應用邏輯的路由規則或者需要SELinux連接信息的路由規則)。在這種情況下,可以使用單個進程接受連接,檢查並傳遞給其他進程處理。
下面的例子需要node-v0.1.100
或更高版本以及node-webworker 。node-webworker是新興的HTML5 Web Workers標準的NodeJS實現,這個標準允許並行執行JavaScript代碼。您可以使用npm安裝node-webworker,命令如下 npm install webworker@stable。
詳細介紹Web Workers的原理超出了這篇文章的範圍,你可以認為Web Worker是一個獨立的執行上下文(類似行程),它可以由JavaScript程式碼生成並來回傳遞數據。node-webworker允許使用如下消息傳遞機制傳遞文件描述符:
首先,主行程的原始碼master.js:
var net = require('net'); var path = require('path'); var sys = require('sys'); var Worker = require('webworker/webworker').Worker; var NUM_WORKERS = 5; var workers = []; var numReqs = 0; for (var i = 0; i < NUM_WORKERS; i++) { workers[i] = new Worker(path.join(__dirname, 'worker.js')); } net.createServer(function(s) { s.pause(); var hv = 0; s.remoteAddress.split('.').forEach(function(v) { hv += parseInt(v); }); var wid = hv % NUM_WORKERS; sys.debug('Request from ' + s.remoteAddress + ' going to worker ' + wid); workers[wid].postMessage(++numReqs, s.fd); }).listen(80);
主行程將執行如下操作:
- 主行程將建立net.Server實例並在80埠上偵聽連接請求。
- 當請求到來時,主行程
- 根據請求端的IP地址決定將請求發送至哪一個wroker。
- 調用請求流對象的
net.
Stream.pause()
方法。這可以防止主行程從讀取套接字中讀取數據 — wroker行程應該看到遠程端發送的所有數據。 - 使用
postMessage()
方法將(遞增後的)全局請求計數器和剛剛收到
套接字描述符發送到指定的worker
然後,worker進程的源代碼worker.js:
var http = require('http'); var net = require('net'); var sys = require('sys'); process.setuid('nobody'); var srv = http.createServer(function(req, resp) { resp.writeHead(200, {'Content-Type' : 'text/plain'}); resp.write( 'process=' + process.pid + '; reqno=' + req.connection.reqNo + '\n' ); resp.end(); }); onmessage = function(msg) { var s = new net.Stream(msg.fd); s.type = srv.type; s.server = srv; s.resume(); s.reqNo = msg.data; srv.emit('connection', s); };
worker執行如下操作:
- 將自己的權限降為
nobody
用戶。 - 創建一個HTTP服務器實例但並不調用任何
listen()
方法。我們將通過主進程收到的描述符來傳遞請求。 - 等待從主進程接收套接字描述符和相關信息
- 將從主進程收到的請求計數保存進流對象(stream.object)中,代碼有些亂,但讓我們可以使用HTTP相關的類來處理這些數據。
- 將net.Stream實例和收到的TCP鏈接接合,然後通過手動觸發事件將其融入HTTP請求的處理流程中。
- 現在,我們如上建立的請求處理程序可以正常運行了: HTTP服務實例完全擁有連接並將像平常一樣解理客戶端的請求。注意一個小技巧,請求處理程序訪問流對象的reqNo屬性,並根據主進程中的計數變量(既用於記錄請求數的全局變量numReqs)將其設置為實際的請求數。
最後,一定要使用超級用戶執行master.js
,因為我們希望程序監聽特權端口(80)。然後使用curl
發出一些請求,並看看是那個進程處理這些請求。
% sudo node ./master.js % curl 'http://localhost:80' process=13049; reqno=2
當然,前面例子用到的基於IP的哈希算法是玩具級的,任何一個合格的HTTP負載均衡器能可以實現。在現實中,你可能想根據客戶端的請求,將連接分派到運行在正確的SELinux上下文中的worker。(參見,node-selinux)根據HTTP請求本身的的信息(如:path,vhost)作出路由決策稍微複雜些,且使用類似的技術也可行。
結論
最 後,我希望本文能夠說明當前NodeJS利用多處理器的情況:一些現有的HTTP框架可以給各種NodeJS應用提供多處理器支持;node- webworkers為管理NodeJS中的並行機制提供了一個好方法(基於chlid_proess);怎樣實用NodeJS自身實現L7 HTTP路由器。
Hi,你好,也看好NodeJS吗?
哈囉:
本身很喜歡js相關的應用,所以都會特別注意相關的資訊,
最近剛好想要找找看有沒有適合的web framework,跟npm相關訊息,就找到你這篇,謝謝你的文章哦…
lishen 博客的文章非常有意思 收藏起來詳閱