充分發(fā)揮Node.js程序性能的一些方法介紹
一個(gè)Node.JS 的進(jìn)程只會(huì)運(yùn)行在單個(gè)的物理核心上,就是因?yàn)檫@一點(diǎn),在開發(fā)可擴(kuò)展的服務(wù)器的時(shí)候就需要格外的注意。
因?yàn)橛幸幌盗蟹€(wěn)定的API,加上原生擴(kuò)展的開發(fā)來(lái)管理進(jìn)程,所以有很多不同的方法來(lái)設(shè)計(jì)一個(gè)可以并行的Node.JS運(yùn)用。在這篇博文里,我們就來(lái)比較下這些可能的架構(gòu)。
這篇文章同時(shí)也介紹compute-cluster 模塊:一個(gè)小型的Node.JS庫(kù),可以用來(lái)很方便的管理進(jìn)程,從來(lái)二線分布式計(jì)算。
遇到的問題
我們?cè)贛ozilla Persona的項(xiàng)目中需要可以處理大量不同特征的請(qǐng)求,所以我們嘗試用使用Node.JS。
為了不影響用戶體驗(yàn),我們?cè)O(shè)計(jì)的‘Interactive' 請(qǐng)求只需要輕量級(jí)的計(jì)算消耗,但是提供更快地反映時(shí)間使得UI沒有卡殼的感覺。相比之下,‘Batch'操作大概需要半秒的處理時(shí)間,而且有可能由于其他的原因,會(huì)有更長(zhǎng)的延遲。
為了更好的設(shè)計(jì),我們找了很多符合我們當(dāng)前需求的方法去解決。
考慮到擴(kuò)展性和成本,我們列出以下關(guān)鍵需求:
- 效率:能有效的使用所有空閑的處理器
- 響應(yīng):我們的“應(yīng)用”能實(shí)時(shí)快速的響應(yīng)
- 優(yōu)雅:當(dāng)請(qǐng)求量過(guò)多到不能處理的時(shí)候,我們處理我們能處理的。不能處理的要清晰的把錯(cuò)誤反饋
- 簡(jiǎn)單:我們的解決方案使用起來(lái)必須簡(jiǎn)單方便
通過(guò)以上幾點(diǎn)我們可以清楚、有目標(biāo)的去篩選
方案一:直接在主線程中處理.
當(dāng)主線程直接處理數(shù)據(jù)的時(shí)候,結(jié)果很不好:
你不能充分利用多核CPU的優(yōu)勢(shì),在交互式的請(qǐng)求/響應(yīng)中,必須等待當(dāng)前請(qǐng)求(或響應(yīng))處理完畢,毫無(wú)優(yōu)雅可言。
這個(gè)方案唯一的優(yōu)點(diǎn)是:夠簡(jiǎn)單
function myRequestHandler(request, response) [ // Let's bring everything to a grinding halt for half a second. var results = doComputationWorkSync(request.somesuch); }
在 Node.JS 程序中,希望同時(shí)處理多個(gè)請(qǐng)求,又想同步進(jìn)行處理,那你準(zhǔn)備弄個(gè)焦頭爛額吧。
方法 2: 是否使用異步處理.
如果在后臺(tái)使用異步的方法來(lái)執(zhí)行是否一定會(huì)有很大的性能改善呢?
答案是不一定.它取決于后臺(tái)運(yùn)行是否有意義
例如下面這種情況:如果在主線程上使用javascript或者本地代碼進(jìn)行計(jì)算時(shí),性能并不比同步處理更好時(shí),就不一定需要在后臺(tái)用異步方法去處理
請(qǐng)閱讀以下代碼
function 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 ... });
}
關(guān)鍵點(diǎn)就在于NodeJS異步API的使用并不依賴于多進(jìn)程的應(yīng)用
方案三:用線程庫(kù)來(lái)實(shí)現(xiàn)異步處理。
只要實(shí)現(xiàn)得當(dāng),使用本地代碼實(shí)現(xiàn)的庫(kù),在 NodeJS 調(diào)用的時(shí)候是可以突破限制從而實(shí)現(xiàn)多線程功能的。
有很多這樣的例子, Nick Campbell 編寫的 bcrypt library 就是其中優(yōu)秀的一個(gè)。
如果你在4核機(jī)器上拿這個(gè)庫(kù)來(lái)作一個(gè)測(cè)試,你將看到神奇的一幕:4倍于平時(shí)的吞吐量,并且耗盡了幾乎所有的資源!但是如果你在24核機(jī)器上測(cè)試,結(jié)果將不會(huì)有太大變化:有4個(gè)核心的使用率基本達(dá)到100%,但其他的核心基本上都處于空閑狀態(tài)。
問題出在這個(gè)庫(kù)使用了NodeJS內(nèi)部的線程池,而這個(gè)線程池并不適合用來(lái)進(jìn)行此類的計(jì)算。另外,這個(gè)線程池上限寫死了,最多只能運(yùn)行4個(gè)線程。
除了寫死了上限,這個(gè)問題更深層的原因是:
- 使用NodeJS內(nèi)部線程池進(jìn)行大量運(yùn)算的話,會(huì)妨礙其文件或網(wǎng)絡(luò)操作,使程序看起來(lái)響應(yīng)緩慢。
- 很難找到合適的方法來(lái)處理等待隊(duì)列:試想一下,如果你隊(duì)列里面已經(jīng)積壓了5分鐘計(jì)算量的線程,你還希望繼續(xù)往里面添加線程嗎?
內(nèi)建線程機(jī)制的組件庫(kù)在這種情況下并不能有效地利用多核的優(yōu)勢(shì),這降低了程序的響應(yīng)能力,并且隨著負(fù)載的加大,程序表現(xiàn)越來(lái)越差。
方案四:使用 NodeJS 的 cluster 模塊
NodeJS 0.6.x 以上的版本提供了一個(gè)cluster模塊 ,允許創(chuàng)建“共享同一個(gè)socket”的一組進(jìn)程,用來(lái)分擔(dān)負(fù)載壓力。
假如你采用了上面的方案,又同時(shí)使用 cluster 模塊,情況會(huì)怎樣呢?
這樣得出的方案將同樣具有同步處理或者內(nèi)建線程池一樣的缺點(diǎn):響應(yīng)緩慢,毫無(wú)優(yōu)雅可言。
有時(shí)候,僅僅添加新運(yùn)行實(shí)例并不能解決問題。
方案五:引入 compute-cluster 模塊
在 Persona 中,我們的解決方案是,維護(hù)一組功能單一(但各不相同)的計(jì)算進(jìn)程。
在這個(gè)過(guò)程中,我們編寫了 compute-cluster 庫(kù)。
這個(gè)庫(kù)會(huì)自動(dòng)按需啟動(dòng)和管理子進(jìn)程,這樣你就可以通過(guò)代碼的方式來(lái)使用一個(gè)本地子進(jìn)程的集群來(lái)處理數(shù)據(jù)。
使用例子:
const 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 中響應(yīng)了 message 事件,對(duì)傳入的請(qǐng)求進(jìn)行處理:
process.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); });
無(wú)需更改調(diào)用代碼,compute-cluster 模塊就可以和現(xiàn)有的異步API整合起來(lái),這樣就能以最小的代碼量換來(lái)真正的多核并行處理。
我們從四個(gè)方面來(lái)看看這個(gè)方案的表現(xiàn)。
多核并行能力:子進(jìn)程使用了全部的核心。
響應(yīng)能力:由于核心管理進(jìn)程只負(fù)責(zé)啟動(dòng)子進(jìn)程和傳遞消息,大部分時(shí)間里它都是空閑的,可以處理更多的交互請(qǐng)求。
即使機(jī)器的負(fù)載壓力很大,我們?nèi)匀豢梢岳貌僮飨到y(tǒng)的調(diào)度器來(lái)提高核心管理進(jìn)程的優(yōu)先級(jí)。
簡(jiǎn)單性:使用了異步API來(lái)隱藏了具體實(shí)現(xiàn)的細(xì)節(jié),我們可以輕易地將該模塊整合到現(xiàn)在項(xiàng)目中,甚至連調(diào)用代碼無(wú)需作改變。
現(xiàn)在我們來(lái)看看,能不能找一個(gè)方法,即使負(fù)載突然激增,系統(tǒng)的效率也不會(huì)異常下降。
當(dāng)然,最佳目標(biāo)仍然是,即使壓力激增,系統(tǒng)依然能高效運(yùn)行,并處理盡量多的請(qǐng)求。
為了幫助實(shí)現(xiàn)優(yōu)秀的方案,compute-cluster 不僅僅只是管理子進(jìn)程和傳遞消息,它還管理了其他信息。
它記錄了當(dāng)前運(yùn)行的子進(jìn)程數(shù),以及每個(gè)子進(jìn)程完成的平均時(shí)間。
有了這些記錄,我們可以在子進(jìn)程開啟之前預(yù)測(cè)它大概需要多少時(shí)間。
據(jù)此,再加上用戶設(shè)置的參數(shù)(max_request_time),我們可以不經(jīng)過(guò)處理,直接就關(guān)閉那些可能超時(shí)的請(qǐng)求。
這個(gè)特性讓你可以很容易根據(jù)用戶體驗(yàn)來(lái)確定你的代碼。比如說(shuō),“用戶登錄的時(shí)候不應(yīng)該等待超過(guò)10秒?!边@大概等價(jià)于將 max_request_time 設(shè)置為7秒(需要考慮網(wǎng)絡(luò)傳輸時(shí)間)。
我們?cè)趯?duì) Persona 服務(wù)進(jìn)行壓力測(cè)試后,得到的結(jié)果很讓人滿意。
在壓力極高的情況下,我們依然能為已認(rèn)證的用戶提供服務(wù),還阻止了一部分未認(rèn)證的用戶,并顯示了相關(guān)的錯(cuò)誤信息。
相關(guān)文章
Node.js操作Firebird數(shù)據(jù)庫(kù)教程
這篇文章主要為大家分享了Node.js操作Firebird數(shù)據(jù)庫(kù)教程,思路清晰便于大家理解,感興趣的小伙伴們可以參考一下2016-03-03Linux系統(tǒng)中如何下載、解壓和安裝特定版本的Node.js
Nodejs版本坑眾多,不同應(yīng)用可能需要不同版本,下面這篇文章主要給大家介紹了關(guān)于Linux系統(tǒng)中如何下載、解壓和安裝特定版本的Node.js的相關(guān)資料,需要的朋友可以參考下2024-01-01Vue+Node服務(wù)器查詢Mongo數(shù)據(jù)庫(kù)及頁(yè)面數(shù)據(jù)傳遞操作實(shí)例分析
這篇文章主要介紹了Vue+Node服務(wù)器查詢Mongo數(shù)據(jù)庫(kù)及頁(yè)面數(shù)據(jù)傳遞操作,結(jié)合實(shí)例形式分析了node.js查詢MongoDB數(shù)據(jù)庫(kù)及vue前臺(tái)頁(yè)面渲染等相關(guān)操作技巧,需要的朋友可以參考下2019-12-12npm?ERR!?Node.js?v20.11.0錯(cuò)誤的解決
在使用?npm?進(jìn)行包管理和構(gòu)建項(xiàng)目的過(guò)程中,有時(shí)會(huì)遇到錯(cuò)誤信息?npm?ERR!?Node.js?v20.11.0,本文就來(lái)介紹一下如何解決,感興趣的可以了解一下2024-02-02