這篇文章主要介紹了充分發揮Node.js程序性能的一些方法介紹,Node.js是把JavaScript用於服務器端的框架,需要的朋友可以參考下
一個Node.JS 的進程只會運行在單個的物理核心上,就是因為這一點,在開發可擴展的服務器的時候就需要格外的注意。
因為有一系列穩定的API,加上原生擴展的開發來管理進程,所以有很多不同的方法來設計一個可以並行的Node.JS運用。在這篇博文裡,我們就來比較下這些可能的架構。
這篇文章同時也介紹compute-cluster 模塊:一個小型的Node.JS庫,可以用來很方便的管理進程,從來二線分布式計算。
遇到的問題
我們在Mozilla Persona的項目中需要可以處理大量不同特征的請求,所以我們嘗試用使用Node.JS。
為了不影響用戶體驗,我們設計的‘Interactive' 請求只需要輕量級的計算消耗,但是提供更快地反映時間使得UI沒有卡殼的感覺。相比之下,‘Batch'操作大概需要半秒的處理時間,而且有可能由於其他的原因,會有更長的延遲。
為了更好的設計,我們找了很多符合我們當前需求的方法去解決。
考慮到擴展性和成本,我們列出以下關鍵需求:
效率:能有效的使用所有空閒的處理器
響應:我們的“應用”能實時快速的響應
優雅:當請求量過多到不能處理的時候,我們處理我們能處理的。不能處理的要清晰的把錯誤反饋
簡單:我們的解決方案使用起來必須簡單方便
通過以上幾點我們可以清楚、有目標的去篩選
方案一:直接在主線程中處理.
當主線程直接處理數據的時候,結果很不好:
你不能充分利用多核CPU的優勢,在交互式的請求/響應中,必須等待當前請求(或響應)處理完畢,毫無優雅可言。
這個方案唯一的優點是:夠簡單
?
1
2
3
4function myRequestHandler(request, response) [
// Let's bring everything to a grinding halt for half a second.
var results = doComputationWorkSync(request.somesuch);
}
在 Node.JS 程序中,希望同時處理多個請求,又想同步進行處理,那你准備弄個焦頭爛額吧。
方法 2: 是否使用異步處理.
如果在後台使用異步的方法來執行是否一定會有很大的性能改善呢?
答案是不一定.它取決於後台運行是否有意義
例如下面這種情況:如果在主線程上使用javascript或者本地代碼進行計算時,性能並不比同步處理更好時,就不一定需要在後台用異步方法去處理
請閱讀以下代碼
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16function doComputationWork(input, callback) {
// Because the internal implementation of this asynchronous
// function is itself synchronously run on the main thread,
// you still starve the entire process.
var output = doComputationWorkSync(input);
process.nextTick(function() {
callback(null, output);
});
}
function myRequestHandler(request, response) [
// Even though this *looks* better, we're still bringing everything
// to a grinding halt.
doComputationWork(request.somesuch, function(err, results) {
// ... do something with results ...
});
}
關鍵點就在於NodeJS異步API的使用並不依賴於多進程的應用
方案三:用線程庫來實現異步處理。
只要實現得當,使用本地代碼實現的庫,在 NodeJS 調用的時候是可以突破限制從而實現多線程功能的。
有很多這樣的例子, Nick Campbell 編寫的 bcrypt library 就是其中優秀的一個。
如果你在4核機器上拿這個庫來作一個測試,你將看到神奇的一幕:4倍於平時的吞吐量,並且耗盡了幾乎所有的資源!但是如果你在24核機器上測試,結果將不會有太大變化:有4個核心的使用率基本達到100%,但其他的核心基本上都處於空閒狀態。
問題出在這個庫使用了NodeJS內部的線程池,而這個線程池並不適合用來進行此類的計算。另外,這個線程池上限寫死了,最多只能運行4個線程。
除了寫死了上限,這個問題更深層的原因是:
使用NodeJS內部線程池進行大量運算的話,會妨礙其文件或網絡操作,使程序看起來響應緩慢。
很難找到合適的方法來處理等待隊列:試想一下,如果你隊列裡面已經積壓了5分鐘計算量的線程,你還希望繼續往裡面添加線程嗎?
內建線程機制的組件庫在這種情況下並不能有效地利用多核的優勢,這降低了程序的響應能力,並且隨著負載的加大,程序表現越來越差。
方案四:使用 NodeJS 的 cluster 模塊
NodeJS 0.6.x 以上的版本提供了一個cluster模塊 ,允許創建“共享同一個socket”的一組進程,用來分擔負載壓力。
假如你采用了上面的方案,又同時使用 cluster 模塊,情況會怎樣呢?
這樣得出的方案將同樣具有同步處理或者內建線程池一樣的缺點:響應緩慢,毫無優雅可言。
有時候,僅僅添加新運行實例並不能解決問題。
方案五:引入 compute-cluster 模塊
在 Persona 中,我們的解決方案是,維護一組功能單一(但各不相同)的計算進程。
在這個過程中,我們編寫了 compute-cluster 庫。
這個庫會自動按需啟動和管理子進程,這樣你就可以通過代碼的方式來使用一個本地子進程的集群來處理數據。
使用例子:
?
1
2
3
4
5
6
7
8
9
10
11
12const computecluster = require('compute-cluster');
// allocate a compute cluster
var cc = new computecluster({ module: './worker.js' });
// run work in parallel
cc.enqueue({ input: "foo" }, function (error, result) {
console.log("foo done", result);
});
cc.enqueue({ input: "bar" }, function (error, result) {
console.log("bar done", result);
});
fileworker.js 中響應了 message 事件,對傳入的請求進行處理:
?
1
2
3
4
5
6
7process.on('message', function(m) {
var output;
// do lots of work here, and we don't care that we're blocking the
// main thread because this process is intended to do one thing at a time.
var output = doComputationWorkSync(m.input);
process.send(output);
});
無需更改調用代碼,compute-cluster 模塊就可以和現有的異步API整合起來,這樣就能以最小的代碼量換來真正的多核並行處理。
我們從四個方面來看看這個方案的表現。
多核並行能力:子進程使用了全部的核心。
響應能力:由於核心管理進程只負責啟動子進程和傳遞消息,大部分時間裡它都是空閒的,可以處理更多的交互請求。
即使機器的負載壓力很大,我們仍然可以利用操作系統的調度器來提高核心管理進程的優先級。
簡單性:使用了異步API來隱藏了具體實現的細節,我們可以輕易地將該模塊整合到現在項目中,甚至連調用代碼無需作改變。
現在我們來看看,能不能找一個方法,即使負載突然激增,系統的效率也不會異常下降。
當然,最佳目標仍然是,即使壓力激增,系統依然能高效運行,並處理盡量多的請求。
為了幫助實現優秀的方案,compute-cluster 不僅僅只是管理子進程和傳遞消息,它還管理了其他信息。
它記錄了當前運行的子進程數,以及每個子進程完成的平均時間。
有了這些記錄,我們可以在子進程開啟之前預測它大概需要多少時間。
據此,再加上用戶設置的參數(max_request_time),我們可以不經過處理,直接就關閉那些可能超時的請求。
這個特性讓你可以很容易根據用戶體驗來確定你的代碼。比如說,“用戶登錄的時候不應該等待超過10秒。”這大概等價於將 max_request_time 設置為7秒(需要考慮網絡傳輸時間)。
我們在對 Persona 服務進行壓力測試後,得到的結果很讓人滿意。
在壓力極高的情況下,我們依然能為已認證的用戶提供服務,還阻止了一部分未認證的用戶,並顯示了相關的錯誤信息。