《Node.js的線程和進(jìn)程詳解》要點(diǎn):
本文介紹了Node.js的線程和進(jìn)程詳解,希望對(duì)您有用。如果有疑問(wèn),可以聯(lián)系我們。
相關(guān)主題:node.js web開(kāi)發(fā)
很多Node.js初學(xué)者都會(huì)有這樣的疑惑,Node.js到底是單線程的還是多線程的?本文解釋了Node.js對(duì)于單/多線程的關(guān)系和支持情況。同時(shí)本文還將列舉一些讓Node.js的web服務(wù)器線程阻塞的例子,最后會(huì)提供Node.js碰到這類cpu密集型問(wèn)題的解決方案。
閱讀本文前,你需要對(duì)Node.js有一個(gè)初步的認(rèn)識(shí),熟悉Node.js基本語(yǔ)法、cluster
模塊、child_process
模塊和express
框架;接觸過(guò)apache
的http
壓力測(cè)試工具ab
;了解一般web服務(wù)器對(duì)于靜態(tài)文件的處理流程。
早期有很多關(guān)于Node.js爭(zhēng)論的焦點(diǎn)都在它的單線程模型方面,在由Jani Hartikainen寫的一篇著名的文章《PHP優(yōu)于Node.js的五大理由》中,更有一條矛頭直接指向Node.js單線程脆弱的問(wèn)題。
如果PHP代碼損壞,不會(huì)拖垮整個(gè)服務(wù)器。
PHP代碼只運(yùn)行在自己的進(jìn)程范圍中,當(dāng)某個(gè)請(qǐng)求顯示錯(cuò)誤時(shí),它只對(duì)特定的請(qǐng)求產(chǎn)生影響。而在Node.js環(huán)境中,所有的請(qǐng)求均在單一的進(jìn)程服務(wù)中,當(dāng)某個(gè)請(qǐng)求導(dǎo)致未知錯(cuò)誤時(shí),整個(gè)服務(wù)器都會(huì)受到影響。
Node.js和Apache+PHP還有一個(gè)非常不同的地方就是進(jìn)程的運(yùn)行時(shí)間長(zhǎng)短,當(dāng)然這一點(diǎn)也被此文作為一個(gè)PHP優(yōu)于Node.js的理由來(lái)寫了。
PHP進(jìn)程短暫。
在PHP中,每個(gè)進(jìn)程對(duì)請(qǐng)求持續(xù)的時(shí)間很短暫,這就意味著你不必為資源配置和內(nèi)存而擔(dān)憂。而Node.js的進(jìn)程需要運(yùn)行很長(zhǎng)一段時(shí)間,你需要小心并妥善管理好內(nèi)存。比如,如果你忘記從全局?jǐn)?shù)據(jù)中刪除條目,這會(huì)輕易的導(dǎo)致內(nèi)存泄露。
在這里我們并不想引起一次關(guān)于PHP和Node.js孰優(yōu)孰劣的口水仗,PHP和Node.js各代表著一個(gè)互聯(lián)網(wǎng)時(shí)代的開(kāi)發(fā)語(yǔ)言,就如同我們討論跑車和越野車誰(shuí)更好一樣,它們都有自己所擅長(zhǎng)和適用的場(chǎng)景。
我們可以通過(guò)下面這兩張圖深入理解一下PHP和Node.js對(duì)處理Http請(qǐng)求時(shí)的區(qū)別。
PHP的模型:
Node.js的模型:
所以你在編寫Node.js代碼時(shí),要保持清醒的頭腦,任何一個(gè)隱藏著的異常被觸發(fā)后,都會(huì)將整個(gè)Node.js進(jìn)程擊潰。但是這樣的特性也為我們編寫代碼帶來(lái)便利,比如同樣要實(shí)現(xiàn)一個(gè)簡(jiǎn)單的網(wǎng)站訪問(wèn)次數(shù)統(tǒng)計(jì),Node.js只需要在內(nèi)存里定義一個(gè)變量var count=0;
,每次有用戶請(qǐng)求過(guò)來(lái)執(zhí)行count++;
即可。
var http = require('http'); var count = 0; http.createServer(function (request, response) { response.writeHead(200, {'Content-Type': 'text/plain'}); response.end((++count).toString()) }).listen(8124); console.log('Server running at http://127.0.0.1:8124/');
但是對(duì)于PHP來(lái)說(shuō)就需要使用第三方媒介來(lái)存儲(chǔ)這個(gè)count
值了,比如創(chuàng)建一個(gè)count.txt
文件來(lái)保存網(wǎng)站的訪問(wèn)次數(shù)。
<?php $counter_file = ("count.txt"); $visits = file($counter_file); $visits[0]++; $fp = fopen($counter_file,"w"); fputs($fp,"$visits[0]"); fclose($fp); echo "$visits[0]"; ?>
Google的V8 Javascript
引擎已經(jīng)在Chrome瀏覽器里證明了它的性能,所以Node.js的作者Ryan Dahl選擇了v8
作為Node.js的執(zhí)行引擎,v8
賦予Node.js高效性能的同時(shí)也注定了Node.js和大名鼎鼎的Nginx一樣,都是以單線程為基礎(chǔ)的,當(dāng)然這也正是作者Ryan Dahl設(shè)計(jì)Node.js的初衷。
Node.js的單線程具有它的優(yōu)勢(shì),但也并非十全十美,在保持單線程模型的同時(shí),它是如何保證非阻塞的呢?
首先,單線程避免了傳統(tǒng)PHP那樣頻繁創(chuàng)建、切換線程的開(kāi)銷,使執(zhí)行速度更加迅速。
第二,資源占用小,如果有對(duì)Node.js的web服務(wù)器做過(guò)壓力測(cè)試的朋友可能發(fā)現(xiàn),Node.js在大負(fù)荷下對(duì)內(nèi)存占用仍然很低,同樣的負(fù)載PHP因?yàn)橐粋€(gè)請(qǐng)求一個(gè)線程的模型,將會(huì)占用大量的物理內(nèi)存,很可能會(huì)導(dǎo)致服務(wù)器因物理內(nèi)存耗盡而頻繁交換,失去響應(yīng)。
單線程的js還保證了絕對(duì)的線程安全,不用擔(dān)心同一變量同時(shí)被多個(gè)線程進(jìn)行讀寫而造成的程序崩潰。比如我們之前做的web訪問(wèn)統(tǒng)計(jì),因?yàn)閱尉€程的絕對(duì)線程安全,所以不可能存在同時(shí)對(duì)count
變量進(jìn)行讀寫的情況,我們的統(tǒng)計(jì)代碼就算是成百的并發(fā)用戶請(qǐng)求都不會(huì)出現(xiàn)問(wèn)題,相較PHP的那種存文件記錄訪問(wèn),就會(huì)面臨并發(fā)同時(shí)寫文件的問(wèn)題。
線程安全的同時(shí)也解放了開(kāi)發(fā)人員,免去了多線程編程中忘記對(duì)變量加鎖或者解鎖造成的悲劇。
Node.js是單線程的,但是它如何做到I/O的異步和非阻塞的呢?其實(shí)Node.js在底層訪問(wèn)I/O還是多線程的,有興趣的朋友可以翻看Node.js的fs
模塊的源碼,里面會(huì)用到libuv
來(lái)處理I/O,所以在我們看來(lái)Node.js的代碼就是非阻塞和異步形式的。
阻塞/非阻塞與異步/同步是兩個(gè)不同的概念,同步不代表阻塞,但是阻塞肯定就是同步了。
舉個(gè)現(xiàn)實(shí)生活中的例子,我去食堂打飯,我選擇了A套餐,然后工作人員幫我去配餐,如果我就站在旁邊,等待工作人員給我配餐,這種情況就稱之為同步;若工作人員幫我配餐的同時(shí),排在我后面的人就開(kāi)始點(diǎn)餐,這樣整個(gè)食堂的點(diǎn)餐服務(wù)并沒(méi)有因?yàn)槲以诘却鼳套餐而停止,這種情況就稱之為非阻塞。這個(gè)例子就簡(jiǎn)單說(shuō)明了同步但非阻塞的情況。
再如果我在等待配餐的時(shí)候去買飲料,等聽(tīng)到叫號(hào)再回去拿套餐,此時(shí)我的飲料也已經(jīng)買好,這樣我在等待配餐的同時(shí)還執(zhí)行了買飲料的任務(wù),叫號(hào)就等于執(zhí)行了回調(diào),就是異步非阻塞了。
既然Node.js是單線程異步非阻塞的,是不是我們就可以高枕無(wú)憂了呢?
還是拿上面那個(gè)買套餐的例子,如果我在買飲料的時(shí)候,已經(jīng)叫我的號(hào)讓我去拿套餐,可是我等了好久才拿到飲料,所以我可能在大廳叫我的餐號(hào)之后很久才拿到A套餐,這也就是單線程的阻塞情況。
在瀏覽器中,js都是以單線程的方式運(yùn)行的,所以我們不用擔(dān)心js同時(shí)執(zhí)行帶來(lái)的沖突問(wèn)題,這對(duì)于我們編碼帶來(lái)很多的便利。
但是對(duì)于在服務(wù)端執(zhí)行的Node.js,它可能每秒有上百個(gè)請(qǐng)求需要處理,對(duì)于在瀏覽器端工作良好的單線程js是否也能同樣在服務(wù)端表現(xiàn)良好呢?
我們看如下代碼:
var start = Date.now();//獲取當(dāng)前時(shí)間戳 setTimeout(function () { console.log(Date.now() - start); for (var i = 0; i < 1000000000; i++){//執(zhí)行長(zhǎng)循環(huán) } }, 1000); setTimeout(function () { console.log(Date.now() - start); }, 2000);
最終我們的打印結(jié)果是:(結(jié)果可能因?yàn)槟愕臋C(jī)器而不同)
1000 3738
對(duì)于我們期望2秒后執(zhí)行的setTimeout
函數(shù)其實(shí)經(jīng)過(guò)了3738
毫秒之后才執(zhí)行,換而言之,因?yàn)閳?zhí)行了一個(gè)很長(zhǎng)的for
循環(huán),所以我們整個(gè)Node.js主線程被阻塞了,如果在我們處理100個(gè)用戶請(qǐng)求中,其中第一個(gè)有需要這樣大量的計(jì)算,那么其余99個(gè)就都會(huì)被延遲執(zhí)行。
其實(shí)雖然Node.js可以處理數(shù)以千記的并發(fā),但是一個(gè)Node.js進(jìn)程在某一時(shí)刻其實(shí)只是在處理一個(gè)請(qǐng)求。
線程是cpu調(diào)度的一個(gè)基本單位,一個(gè)cpu同時(shí)只能執(zhí)行一個(gè)線程的任務(wù),同樣一個(gè)線程任務(wù)也只能在一個(gè)cpu上執(zhí)行,所以如果你運(yùn)行Node.js的機(jī)器是像i5,i7這樣多核cpu,那么將無(wú)法充分利用多核cpu的性能來(lái)為Node.js服務(wù)。
在C++、C#、python等其他語(yǔ)言都有與之對(duì)應(yīng)的多線程編程,有些時(shí)候這很有趣,帶給我們靈活的編程方式;但是也可能帶給我們一堆麻煩,需要學(xué)習(xí)更多的Api知識(shí),在編寫更多代碼的同時(shí)也存在著更多的風(fēng)險(xiǎn),線程的切換和鎖也會(huì)造成系統(tǒng)資源的開(kāi)銷。
就像上面的那個(gè)例子,如果我們的Node.js有創(chuàng)建子線程的能力,那問(wèn)題就迎刃而解了:
var start = Date.now(); createThread(function () { //創(chuàng)建一個(gè)子線程執(zhí)行這10億次循環(huán) console.log(Date.now() - start); for (var i = 0; i < 1000000000; i++){} }); setTimeout(function () { //因?yàn)?0億次循環(huán)是在子線程中執(zhí)行的,所以主線程不受影響 console.log(Date.now() - start); }, 2000);
可惜也可以說(shuō)可喜的是,Node.js的核心模塊并沒(méi)有提供這樣的api給我們,我們真的不想多線程又回歸回來(lái)。不過(guò)或許多線程真的能夠解決我們某方面的問(wèn)題。
Jorge Chamorro Bieling是tagg(Threads a gogo for Node.js)
包的作者,他硬是利用phread
庫(kù)和C語(yǔ)言讓Node.js支持了多線程的開(kāi)發(fā),我們看一下tagg模塊的簡(jiǎn)單示例:
var Threads = require('threads_a_gogo');//加載tagg包 function fibo(n) {//定義斐波那契數(shù)組計(jì)算函數(shù) return n > 1 ? fibo(n - 1) + fibo(n - 2) : 1; } var t = Threads.create().eval(fibo); t.eval('fibo(35)', function(err, result) {//將fibo(35)丟入子線程運(yùn)行 if (err) throw err; //線程創(chuàng)建失敗 console.log('fibo(35)=' + result);//打印fibo執(zhí)行35次的結(jié)果 }); console.log('not block');//打印信息了,表示沒(méi)有阻塞
上面這段代碼利用tagg
包將fibo(35)
這個(gè)計(jì)算丟入了子線程中進(jìn)行,保證了Node.js主線程的舒暢,當(dāng)子線程任務(wù)執(zhí)行完畢將會(huì)執(zhí)行主線程的回調(diào)函數(shù),把結(jié)果打印到屏幕上,執(zhí)行結(jié)果如下:
not block fibo(35)=14930352
斐波那契數(shù)列,又稱黃金分割數(shù)列,這個(gè)數(shù)列從第三項(xiàng)開(kāi)始,每一項(xiàng)都等于前兩項(xiàng)之和:0、1、1、2、3、5、8、13、21、……。
注意我們上面代碼的斐波那契數(shù)組算法并不是最優(yōu)算法,只是為了模擬cpu密集型計(jì)算任務(wù)。
由于tagg
包目前只能在linux
下安裝運(yùn)行,所以我fork
了一個(gè)分支,修改了部分tagg
包的代碼,發(fā)布了tagg2
包。tagg2
包同樣具有tagg
包的多線程功能,采用新的node-gyp
命令進(jìn)行編譯,同時(shí)它跨平臺(tái)支持,mac
,linux
,windows
下都可以使用,對(duì)開(kāi)發(fā)人員的api也更加友好。安裝方法很簡(jiǎn)單,直接npm install tagg2
。
一個(gè)利用tagg2
計(jì)算斐波那契數(shù)組的http服務(wù)器代碼:
var express = require('express'); var tagg2 = require("tagg2"); var app = express(); var th_func = function(){//線程執(zhí)行函數(shù),以下內(nèi)容會(huì)在線程中執(zhí)行 var fibo =function fibo (n) {//在子線程中定義fibo函數(shù) return n > 1 ? fibo(n - 1) + fibo(n - 2) : 1; } var n = fibo(~~thread.buffer);//執(zhí)行fibo遞歸 thread.end(n);//當(dāng)線程執(zhí)行完畢,執(zhí)行thread.end帶上計(jì)算結(jié)果回調(diào)主線程 }; app.get('/', function(req, res){ var n = ~~req.query.n || 1;//獲取用戶請(qǐng)求參數(shù) var buf = new Buffer(n.toString()); tagg2.create(th_func, {buffer:buf}, function(err,result){ //創(chuàng)建一個(gè)js線程,傳入工作函數(shù),buffer參數(shù)以及回調(diào)函數(shù) if(err) return res.end(err);//如果線程創(chuàng)建失敗 res.end(result.toString());//響應(yīng)線程執(zhí)行計(jì)算的結(jié)果 }) }); app.listen(8124); console.log('listen on 8124');
其中~~req.query.n
表示將用戶傳遞的參數(shù)n
取整,功能類似Math.floor
函數(shù)。
我們用express
框架搭建了一個(gè)web服務(wù)器,根據(jù)用戶發(fā)送的參數(shù)n
的值來(lái)創(chuàng)建子線程計(jì)算斐波那契數(shù)組,當(dāng)子線程計(jì)算完畢之后將結(jié)果響應(yīng)給客戶端。由于計(jì)算是丟入子線程中運(yùn)行的,所以整個(gè)主線程不會(huì)被阻塞,還是能夠繼續(xù)處理新請(qǐng)求的。
我們利用apache
的http壓力測(cè)試工具ab
來(lái)進(jìn)行一次簡(jiǎn)單的壓力測(cè)試,看看執(zhí)行斐波那契數(shù)組35次,100客戶端并發(fā)100個(gè)請(qǐng)求,我們的QPS (Query Per Second)
每秒查詢率在多少。
ab
的全稱是ApacheBench
,是Apache
附帶的一個(gè)小工具,用于進(jìn)行HTTP服務(wù)器的性能測(cè)試,可以同時(shí)模擬多個(gè)并發(fā)請(qǐng)求。
我們的測(cè)試硬件:linux 2.6.4 4cpu 8G 64bit,網(wǎng)絡(luò)環(huán)境則是內(nèi)網(wǎng)。
ab
壓力測(cè)試命令:
ab -c 100 -n 100 http://192.168.28.5:8124/?n=35
壓力測(cè)試結(jié)果:
Server Software: Server Hostname: 192.168.28.5 Server Port: 8124 Document Path: /?n=35 Document Length: 8 bytes Concurrency Level: 100 Time taken for tests: 5.606 seconds Complete requests: 100 Failed requests: 0 Write errors: 0 Total transferred: 10600 bytes HTML transferred: 800 bytes Requests per second: 17.84 [#/sec](mean) Time per request: 5605.769 [ms](mean) Time per request: 56.058 [ms](mean, across all concurrent requests) Transfer rate: 1.85 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 3 4 0.8 4 6 Processing: 455 5367 599.7 5526 5598 Waiting: 454 5367 599.7 5526 5598 Total: 461 5372 599.3 5531 5602 Percentage of the requests served within a certain time (ms) 50% 5531 66% 5565 75% 5577 80% 5581 90% 5592 95% 5597 98% 5600 99% 5602 100% 5602 (longest request)
我們看到Requests per second
表示每秒我們服務(wù)器處理的任務(wù)數(shù)量,這里是17.84
。第二個(gè)我們比較關(guān)心的是兩個(gè)Time per request
結(jié)果,上面一行Time per request:5605.769 [ms](mean)
表示當(dāng)前這個(gè)并發(fā)量下處理每組請(qǐng)求的時(shí)間,而下面這個(gè)Time per request:56.058 [ms](mean, across all concurrent requests)
表示每個(gè)用戶平均處理時(shí)間,因?yàn)槲覀儽敬螠y(cè)試并發(fā)是100,所以結(jié)果正好是上一行的100分之1。得出本次測(cè)試平均每個(gè)用戶請(qǐng)求的平均等待時(shí)間為56.058 [ms]
。
另外我們看下最后帶有百分比的列表,可以看到50%的用戶是在5531 ms
以內(nèi)返回的,最慢的也不過(guò)5602 ms
,響應(yīng)延遲非常的平均。
我們?nèi)绻?code>cluster來(lái)啟動(dòng)4個(gè)進(jìn)程,是否可以充分利用cpu達(dá)到tagg2
那樣的QPS
呢?我們?cè)谕瑯拥木W(wǎng)絡(luò)環(huán)境和測(cè)試機(jī)上運(yùn)行如下代碼:
var cluster = require('cluster');//加載clustr模塊 var numCPUs = require('os').cpus().length;//設(shè)定啟動(dòng)進(jìn)程數(shù)為cpu個(gè)數(shù) if (cluster.isMaster) { for (var i = 0; i < numCPUs; i++) { cluster.fork();//啟動(dòng)子進(jìn)程 } } else { var express = require('express'); var app = express(); var fibo = function fibo (n) {//定義斐波那契數(shù)組算法 return n > 1 ? fibo(n - 1) + fibo(n - 2) : 1; } app.get('/', function(req, res){ var n = fibo(~~req.query.n || 1);//接收參數(shù) res.send(n.toString()); }); app.listen(8124); console.log('listen on 8124'); }
在終端屏幕上打印了4行信息:
listen on 8124 listen on 8124 listen on 8124 listen on 8124
我們成功啟動(dòng)了4個(gè)cluster之后,用同樣的ab
壓力測(cè)試命令對(duì)8124端口進(jìn)行測(cè)試,結(jié)果如下:
Server Software: Server Hostname: 192.168.28.5 Server Port: 8124 Document Path: /?n=35 Document Length: 8 bytes Concurrency Level: 100 Time taken for tests: 10.509 seconds Complete requests: 100 Failed requests: 0 Write errors: 0 Total transferred: 16500 bytes HTML transferred: 800 bytes Requests per second: 9.52 [#/sec](mean) Time per request: 10508.755 [ms](mean) Time per request: 105.088 [ms](mean, across all concurrent requests) Transfer rate: 1.53 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 4 5 0.4 5 6 Processing: 336 3539 2639.8 2929 10499 Waiting: 335 3539 2639.9 2929 10499 Total: 340 3544 2640.0 2934 10504 Percentage of the requests served within a certain time (ms) 50% 2934 66% 3763 75% 4527 80% 5153 90% 8261 95% 9719 98% 10308 99% 10504 100% 10504 (longest request)
通過(guò)和上面tagg2
包的測(cè)試結(jié)果對(duì)比,我們發(fā)現(xiàn)區(qū)別很大。首先每秒處理的任務(wù)數(shù)從17.84 [#/sec]
下降到了9.52 [#/sec]
,這說(shuō)明我們web服務(wù)器整體的吞吐率下降了;然后每個(gè)用戶請(qǐng)求的平均等待時(shí)間也從56.058 [ms]
提高到了105.088 [ms]
,用戶等待的時(shí)間也更長(zhǎng)了。
最后我們發(fā)現(xiàn)用戶請(qǐng)求處理的時(shí)長(zhǎng)非常的不均勻,50%的用戶在2934 ms
內(nèi)返回了,最慢的等待達(dá)到了10504 ms
。雖然我們使用了cluster
啟動(dòng)了4個(gè)Node.js進(jìn)程處理用戶請(qǐng)求,但是對(duì)于每個(gè)Node.js進(jìn)程來(lái)說(shuō)還是單線程的,所以當(dāng)有4個(gè)用戶跑滿了4個(gè)Node.js的cluster
進(jìn)程之后,新來(lái)的用戶請(qǐng)求就只能等待了,最后造成了先到的用戶處理時(shí)間短,后到的用戶請(qǐng)求處理時(shí)間比較長(zhǎng),就造成了用戶等待時(shí)間非常的不平均。
大家看到這里是不是開(kāi)始心潮澎湃,感覺(jué)js一統(tǒng)江湖的時(shí)代來(lái)臨了,單線程異步非阻塞的模型可以勝任大并發(fā),同時(shí)開(kāi)發(fā)也非常高效,多線程下的js可以承擔(dān)cpu密集型任務(wù),不會(huì)有主線程阻塞而引起的性能問(wèn)題。
但是,不論tagg
還是tagg2
包都是利用phtread
庫(kù)和v8
的v8::Isolate Class
類來(lái)實(shí)現(xiàn)js多線程功能的。
Isolate
代表著一個(gè)獨(dú)立的v8
引擎實(shí)例,v8
的Isolate
擁有完全分開(kāi)的狀態(tài),在一個(gè)Isolate
實(shí)例中的對(duì)象不能夠在另外一個(gè)Isolate
實(shí)例中使用。嵌入式開(kāi)發(fā)者可以在其他線程創(chuàng)建一些額外的Isolate
實(shí)例并行運(yùn)行。在任何時(shí)刻,一個(gè)Isolate
實(shí)例只能夠被一個(gè)線程進(jìn)行訪問(wèn),可以利用加鎖/解鎖進(jìn)行同步操作。
換而言之,我們?cè)谶M(jìn)行v8
的嵌入式開(kāi)發(fā)時(shí),無(wú)法在多線程中訪問(wèn)js變量,這條規(guī)則將直接導(dǎo)致我們之前的tagg2
里面線程執(zhí)行的函數(shù)無(wú)法使用Node.js的核心api,比如fs
,crypto
等模塊。如此看來(lái),tagg2
包還是有它使用的局限性,針對(duì)一些可以使用js原生的大量計(jì)算或循環(huán)可以使用tagg2
,Node.js核心api因?yàn)闊o(wú)法從主線程共享對(duì)象的關(guān)系,也就不能跨線程使用了。
最后,如果我們非要讓Node.js支持多線程,還是提倡使用官方的做法,利用libuv
庫(kù)來(lái)實(shí)現(xiàn)。
libuv
是一個(gè)跨平臺(tái)的異步I/O庫(kù),它主要用于Node.js的開(kāi)發(fā),同時(shí)他也被Mozilla's Rust language
,Luvit
,Julia
,pyuv
等使用。它主要包括了Event loops
事件循環(huán),Filesystem
文件系統(tǒng),Networking
網(wǎng)絡(luò)支持,Threads
線程,Processes
進(jìn)程,Utilities
其他工具。
在Node.js核心api中的異步多線程大多是使用libuv
來(lái)實(shí)現(xiàn)的,下一章將帶領(lǐng)大家開(kāi)發(fā)一個(gè)讓Node.js支持多線程并基于libuv
的Node.js包。
在支持html5的瀏覽器里,我們可以使用webworker
來(lái)將一些耗時(shí)的計(jì)算丟入worker進(jìn)程中執(zhí)行,這樣主進(jìn)程就不會(huì)阻塞,用戶也就不會(huì)有卡頓的感覺(jué)了。在Node.js中是否也可以使用這類技術(shù),保證主線程的通暢呢?
cluster
可以用來(lái)讓Node.js充分利用多核cpu的性能,同時(shí)也可以讓Node.js程序更加健壯,官網(wǎng)上的cluster
示例已經(jīng)告訴我們?nèi)绾沃匦聠?dòng)一個(gè)因?yàn)楫惓6紳⒌淖舆M(jìn)程。
想要像在瀏覽器端那樣啟動(dòng)worker進(jìn)程,我們需要利用Node.js核心api里的child_process
模塊。child_process
模塊提供了fork
的方法,可以啟動(dòng)一個(gè)Node.js文件,將它作為worker進(jìn)程,當(dāng)worker進(jìn)程工作完畢,把結(jié)果通過(guò)send
方法傳遞給主進(jìn)程,然后自動(dòng)退出,這樣我們就利用了多進(jìn)程來(lái)解決主線程阻塞的問(wèn)題。
我們先啟動(dòng)一個(gè)web服務(wù),還是接收參數(shù)計(jì)算斐波那契數(shù)組:
var express = require('express'); var fork = require('child_process').fork; var app = express(); app.get('/', function(req, res){ var worker = fork('./work_fibo.js') //創(chuàng)建一個(gè)工作進(jìn)程 worker.on('message', function(m) {//接收工作進(jìn)程計(jì)算結(jié)果 if('object' === typeof m && m.type === 'fibo'){ worker.kill();//發(fā)送殺死進(jìn)程的信號(hào) res.send(m.result.toString());//將結(jié)果返回客戶端 } }); worker.send({type:'fibo',num:~~req.query.n || 1}); //發(fā)送給工作進(jìn)程計(jì)算fibo的數(shù)量 }); app.listen(8124);
我們通過(guò)express
監(jiān)聽(tīng)8124端口,對(duì)每個(gè)用戶的請(qǐng)求都會(huì)去fork
一個(gè)子進(jìn)程,通過(guò)調(diào)用worker.send
方法將參數(shù)n
傳遞給子進(jìn)程,同時(shí)監(jiān)聽(tīng)子進(jìn)程發(fā)送消息的message
事件,將結(jié)果響應(yīng)給客戶端。
下面是被fork
的work_fibo.js
文件內(nèi)容:
var fibo = function fibo (n) {//定義算法 return n > 1 ? fibo(n - 1) + fibo(n - 2) : 1; } process.on('message', function(m) { //接收主進(jìn)程發(fā)送過(guò)來(lái)的消息 if(typeof m === 'object' && m.type === 'fibo'){ var num = fibo(~~m.num); //計(jì)算jibo process.send({type: 'fibo',result:num}) //計(jì)算完畢返回結(jié)果 } }); process.on('SIGHUP', function() { process.exit();//收到kill信息,進(jìn)程退出 });
我們先定義函數(shù)fibo
用來(lái)計(jì)算斐波那契數(shù)組,然后監(jiān)聽(tīng)了主線程發(fā)來(lái)的消息,計(jì)算完畢之后將結(jié)果send
到主線程。同時(shí)還監(jiān)聽(tīng)process
的SIGHUP
事件,觸發(fā)此事件就進(jìn)程退出。
這里我們有一點(diǎn)需要注意,主線程的kill
方法并不是真的使子進(jìn)程退出,而是會(huì)觸發(fā)子進(jìn)程的SIGHUP
事件,真正的退出還是依靠process.exit();
。
下面我們用ab
命令測(cè)試一下多進(jìn)程方案的處理性能和用戶請(qǐng)求延遲,測(cè)試環(huán)境不變,還是100個(gè)并發(fā)100次請(qǐng)求,計(jì)算斐波那切數(shù)組第35位:
Server Software: Server Hostname: 192.168.28.5 Server Port: 8124 Document Path: /?n=35 Document Length: 8 bytes Concurrency Level: 100 Time taken for tests: 7.036 seconds Complete requests: 100 Failed requests: 0 Write errors: 0 Total transferred: 16500 bytes HTML transferred: 800 bytes Requests per second: 14.21 [#/sec](mean) Time per request: 7035.775 [ms](mean) Time per request: 70.358 [ms](mean, across all concurrent requests) Transfer rate: 2.29 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 4 4 0.2 4 5 Processing: 4269 5855 970.3 6132 7027 Waiting: 4269 5855 970.3 6132 7027 Total: 4273 5860 970.3 6136 7032 Percentage of the requests served within a certain time (ms) 50% 6136 66% 6561 75% 6781 80% 6857 90% 6968 95% 7003 98% 7017 99% 7032 100% 7032 (longest request)
壓力測(cè)試結(jié)果QPS
約為14.21
,相比cluster
來(lái)說(shuō),還是快了很多,每個(gè)用戶請(qǐng)求的延遲都很平均,因?yàn)檫M(jìn)程的創(chuàng)建和銷毀的開(kāi)銷要大于線程,所以在性能方面略低于tagg2
,不過(guò)相對(duì)于cluster
方案,這樣的提升還是令我們滿意的。
使用child_process
模塊的fork
方法確實(shí)可以讓我們很好的解決單線程對(duì)cpu密集型任務(wù)的阻塞問(wèn)題,同時(shí)又沒(méi)有tagg2
包那樣無(wú)法使用Node.js核心api的限制。
但是如果我的worker
具有多樣性,每次在利用child_process
模塊解決問(wèn)題時(shí)都需要去創(chuàng)建一個(gè)worker.js
的工作函數(shù)文件,有點(diǎn)麻煩。我們是不是可以更加簡(jiǎn)單一些呢?
在我們啟動(dòng)Node.js程序時(shí),node
命令可以帶上-e
這個(gè)參數(shù),它將直接執(zhí)行-e
后面的字符串,如下代碼就將打印出hello world
。
node -e "console.log('hello world')"
合理的利用這個(gè)特性,我們就可以免去每次都創(chuàng)建一個(gè)文件的麻煩。
var express = require('express'); var spawn = require('child_process').spawn; var app = express(); var spawn_worker = function(n,end){//定義工作函數(shù) var fibo = function fibo (n) { return n > 1 ? fibo(n - 1) + fibo(n - 2) : 1; } end(fibo(n)); } var spawn_end = function(result){//定義工作函數(shù)結(jié)束的回調(diào)函數(shù)參數(shù) console.log(result); process.exit(); } app.get('/', function(req, res){ var n = ~~req.query.n || 1; //拼接-e后面的參數(shù) var spawn_cmd = '('+spawn_worker.toString()+'('+n+','+spawn_end.toString()+'));' console.log(spawn_cmd);//注意這個(gè)打印結(jié)果 var worker = spawn('node',['-e',spawn_cmd]);//執(zhí)行node -e "xxx"命令 var fibo_res = ''; worker.stdout.on('data', function (data) { //接收工作函數(shù)的返回 fibo_res += data.toString(); }); worker.on('close', function (code) {//將結(jié)果響應(yīng)給客戶端 res.send(fibo_res); }); }); app.listen(8124);
代碼很簡(jiǎn)單,我們主要關(guān)注3個(gè)地方。
第一、我們定義了spawn_worker
函數(shù),他其實(shí)就是將會(huì)在-e
后面執(zhí)行的工作函數(shù),所以我們把計(jì)算斐波那契數(shù)組的算法定義在內(nèi),spawn_worker
函數(shù)接收2個(gè)參數(shù),第一個(gè)參數(shù)n
表示客戶請(qǐng)求要計(jì)算的斐波那契數(shù)組的位數(shù),第二個(gè)end
參數(shù)是一個(gè)函數(shù),如果計(jì)算完畢則執(zhí)行end
,將結(jié)果傳回主線程;
第二、真正當(dāng)Node.js腳步執(zhí)行的字符串其實(shí)就是spawn_cmd
里的內(nèi)容,它的內(nèi)容我們通過(guò)運(yùn)行之后的打印信息,很容易就能明白;
第三、我們利用child_process
的spawn
方法,類似在命令行里執(zhí)行了node -e "js code"
,啟動(dòng)Node.js工作進(jìn)程,同時(shí)監(jiān)聽(tīng)子進(jìn)程的標(biāo)準(zhǔn)輸出,將數(shù)據(jù)保存起來(lái),當(dāng)子進(jìn)程退出之后把結(jié)果響應(yīng)給用戶。
現(xiàn)在主要的焦點(diǎn)就是變量spawn_cmd
到底保存了什么,我們打開(kāi)瀏覽器在地址欄里輸入:
http://127.0.0.1:8124/?n=35
下面就是程序運(yùn)行之后的打印信息,
(function (n,end){ var fibo = function fibo (n) { return n > 1 ? fibo(n - 1) + fibo(n - 2) : 1; } end(fibo(n)); }(35,function (result){ console.log(result); process.exit(); }));
對(duì)于在子進(jìn)程執(zhí)行的工作函數(shù)的兩個(gè)參數(shù)n
和end
現(xiàn)在一目了然,n
代表著用戶請(qǐng)求的參數(shù),期望獲得的斐波那契數(shù)組的位數(shù),而end
參數(shù)則是一個(gè)匿名函數(shù),在標(biāo)準(zhǔn)輸出中打印計(jì)算結(jié)果然后退出進(jìn)程。
node -e
命令雖然可以減少創(chuàng)建文件的麻煩,但同時(shí)它也有命令行長(zhǎng)度的限制,這個(gè)值各個(gè)系統(tǒng)都不相同,我們通過(guò)命令getconf ARG_MAX
來(lái)獲得最大命令長(zhǎng)度,例如:MAC OSX
下是262,144 byte
,而我的linux
虛擬機(jī)則是131072 byte
。
大部分多線程解決cpu密集型任務(wù)的方案都可以用我們之前討論的多進(jìn)程方案來(lái)替代,但是有一些比較特殊的場(chǎng)景多線程的優(yōu)勢(shì)就發(fā)揮出來(lái)了,下面就拿我們最常見(jiàn)的http web
服務(wù)器響應(yīng)一個(gè)小的靜態(tài)文件作為例子。
以express
處理小型靜態(tài)文件為例,大致的處理流程如下:
首先獲取文件狀態(tài),判斷文件的修改時(shí)間或者判斷etag
來(lái)確定是否響應(yīng)304
給客戶端,讓客戶端繼續(xù)使用本地緩存。
如果緩存已經(jīng)失效或者客戶端沒(méi)有緩存,就需要獲取文件的內(nèi)容到buffer中,為響應(yīng)作準(zhǔn)備。
然后判斷文件的MIME
類型,如果是類似html
,js
,css
等靜態(tài)資源,還需要gzip
壓縮之后傳輸給客戶端
最后將gzip壓縮完成的靜態(tài)文件響應(yīng)給客戶端。
下面是一個(gè)正常成功的Node.js處理靜態(tài)資源無(wú)緩存流程圖:
這個(gè)流程中的(2)
,(3)
,(4)
步都經(jīng)歷了從js到C++ ,打開(kāi)和釋放文件,還有調(diào)用了zlib
庫(kù)的gzip
算法,其中每個(gè)異步的算法都會(huì)有創(chuàng)建和銷毀線程的開(kāi)銷,所以這樣也是大家詬病Node.js處理靜態(tài)文件不給力的原因之一。
為了改善這個(gè)問(wèn)題,我之前有利用libuv
庫(kù)開(kāi)發(fā)了一個(gè)改善Node.js的http/https
處理靜態(tài)文件的包,名為ifile
,ifile
包,之所以可以加速Node.js的靜態(tài)文件處理性能,主要是減少了js和C++的互相調(diào)用,以及頻繁的創(chuàng)建和銷毀線程的開(kāi)銷,下圖是ifile
包處理一個(gè)靜態(tài)無(wú)緩存資源的流程圖:
由于全部工作都是在libuv
的子線程中執(zhí)行的,所以Node.js主線程不會(huì)阻塞,當(dāng)然性能也會(huì)大幅提升了,使用ifile
包非常簡(jiǎn)單,它能夠和express
無(wú)縫的對(duì)接。
var express = require('express'); var ifile = require("ifile"); var app = express(); app.use(ifile.connect()); //默認(rèn)值是 [['/static',__dirname]]; app.listen(8124);
上面這4行代碼就可以讓express
把靜態(tài)資源交給ifile
包來(lái)處理了,我們?cè)谶@里對(duì)它進(jìn)行了一個(gè)簡(jiǎn)單的壓力測(cè)試,測(cè)試用例為響應(yīng)一個(gè)大小為92kb
的jquery.1.7.1.min.js
文件,測(cè)試命令:
ab -c 500 -n 5000 -H "Accept-Encoding: gzip" http://192.168.28.5:8124/static/jquery.1.7.1.min.js
由于在ab
命令中我們加入了-H "Accept-Encoding: gzip"
,表示響應(yīng)的靜態(tài)文件希望是gzip
壓縮之后的,所以ifile
將會(huì)把壓縮之后的jquery.1.7.1.min.js
文件響應(yīng)給客戶端。結(jié)果如下:
Server Software: Server Hostname: 192.168.28.5 Server Port: 8124 Document Path: /static/jquery.1.7.1.min.js Document Length: 33016 bytes Concurrency Level: 500 Time taken for tests: 9.222 seconds Complete requests: 5000 Failed requests: 0 Write errors: 0 Total transferred: 166495000 bytes HTML transferred: 165080000 bytes Requests per second: 542.16 [#/sec](mean) Time per request: 922.232 [ms](mean) Time per request: 1.844 [ms](mean, across all concurrent requests) Transfer rate: 17630.35 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 49 210.2 1 1003 Processing: 191 829 128.6 870 1367 Waiting: 150 824 128.5 869 1091 Total: 221 878 230.7 873 1921 Percentage of the requests served within a certain time (ms) 50% 873 66% 878 75% 881 80% 885 90% 918 95% 1109 98% 1815 99% 1875 100% 1921 (longest request)
我們首先看到Document Length
一項(xiàng)結(jié)果為33016 bytes
說(shuō)明我們的jquery文件已經(jīng)被成功的gzip
壓縮,因?yàn)樵次募笮∈?code>92kb;其次,我們最關(guān)心的Requests per second:542.16 [#/sec](mean)
,說(shuō)明我們每秒能處理542個(gè)任務(wù);最后,我們看到,在這樣的壓力情況下,平均每個(gè)用戶的延遲在1.844 [ms]
。
我們看下使用express
框架處理這樣的壓力會(huì)是什么樣的結(jié)果,express
測(cè)試代碼如下:
var express = require('express'); var app = express(); app.use(express.compress());//支持gzip app.use('/static', express.static(__dirname + '/static')); app.listen(8124);
代碼同樣非常簡(jiǎn)單,注意這里我們使用:
app.use('/static', express.static(__dirname + '/static'));
而不是:
app.use(express.static(__dirname));
后者每個(gè)請(qǐng)求都會(huì)去匹配一次文件是否存在,而前者只有請(qǐng)求url
是/static
開(kāi)頭的才會(huì)去匹配靜態(tài)資源,所以前者效率更高一些。然后我們執(zhí)行相同的ab
壓力測(cè)試命令看下結(jié)果:
Server Software: Server Hostname: 192.168.28.5 Server Port: 8124 Document Path: /static/jquery.1.7.1.min.js Document Length: 33064 bytes Concurrency Level: 500 Time taken for tests: 16.665 seconds Complete requests: 5000 Failed requests: 0 Write errors: 0 Total transferred: 166890000 bytes HTML transferred: 165320000 bytes Requests per second: 300.03 [#/sec](mean) Time per request: 1666.517 [ms](mean) Time per request: 3.333 [ms](mean, across all concurrent requests) Transfer rate: 9779.59 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 173 539.8 1 7003 Processing: 509 886 350.5 809 9366 Waiting: 238 476 277.9 426 9361 Total: 510 1059 632.9 825 9367 Percentage of the requests served within a certain time (ms) 50% 825 66% 908 75% 1201 80% 1446 90% 1820 95% 1952 98% 2560 99% 3737 100% 9367 (longest request)
同樣分析一下結(jié)果,Document Length:33064 bytes
表示文檔大小為33064 bytes
,說(shuō)明我們的gzip起作用了,每秒處理任務(wù)數(shù)從ifile
包的542
下降到了300
,最長(zhǎng)用戶等待時(shí)間也延長(zhǎng)到了9367 ms
,可見(jiàn)我們的努力起到了立竿見(jiàn)影的作用,js和C++互相調(diào)用以及線程的創(chuàng)建和釋放并不是沒(méi)有損耗的。
但是當(dāng)我在express
的谷歌論壇里貼上這些測(cè)試結(jié)果,并宣傳ifile
包的時(shí)候,express
的作者TJ,給出了不一樣的評(píng)價(jià),他在回復(fù)中說(shuō)道:
請(qǐng)牢記你可能不需要這么高等級(jí)吞吐率的系統(tǒng),就算是每月百萬(wàn)級(jí)別下載量的
npm
網(wǎng)站,也僅僅每秒處理17
個(gè)請(qǐng)求而已,這樣的壓力甚至于PHP也可以處理掉(又黑了一把php)。
確實(shí)如TJ所說(shuō),性能只是我們項(xiàng)目的指標(biāo)之一而非全部,一味的去追求高性能并不是很理智。
ifile
包開(kāi)源項(xiàng)目地址:https://github.com/DoubleSpout/ifile
單線程的Node.js給我們編碼帶來(lái)了太多的便利和樂(lè)趣,我們應(yīng)該時(shí)刻保持清醒的頭腦,在寫Node.js代碼中切不可與PHP混淆,任何一個(gè)隱藏的問(wèn)題都可能擊潰整個(gè)線上正在運(yùn)行的Node.js程序。
單線程異步的Node.js不代表不會(huì)阻塞,在主線程做過(guò)多的任務(wù)可能會(huì)導(dǎo)致主線程的卡死,影響整個(gè)程序的性能,所以我們要非常小心的處理大量的循環(huán),字符串拼接和浮點(diǎn)運(yùn)算等cpu密集型任務(wù),合理的利用各種技術(shù)把任務(wù)丟給子線程或子進(jìn)程去完成,保持Node.js主線程的暢通。
線程/進(jìn)程的使用并不是沒(méi)有開(kāi)銷的,盡可能減少創(chuàng)建和銷毀線程/進(jìn)程的次數(shù),可以提升我們系統(tǒng)整體的性能和出錯(cuò)的概率。
最后請(qǐng)不要一味的追求高性能和高并發(fā),因?yàn)槲覀兛赡懿恍枰到y(tǒng)具有那么大的吞吐率。高效,敏捷,低成本的開(kāi)發(fā)才是項(xiàng)目所需要的,這也是為什么Node.js能夠在眾多開(kāi)發(fā)語(yǔ)言中脫穎而出的關(guān)鍵。
轉(zhuǎn)載請(qǐng)注明本頁(yè)網(wǎng)址:
http://www.snjht.com/jiaocheng/14640.html