Node.js的執行緒和程式
前言
很多Node.js初學者都會有這樣的疑惑,Node.js到底是單執行緒的還是多執行緒的?通過本章的學習,能夠讓讀者較為清晰的理解Node.js對於單/多執行緒的關係和支援情況。同時本章還將列舉一些讓Node.js的web伺服器執行緒阻塞的例子,最後會提供Node.js碰到這類cpu密集型問題的解決方案。
在學習本章之前,讀者需要對Node.js有一個初步的認識,熟悉Node.js基本語法、cluster模組、child_process模組和express框架;接觸過apache的http壓力測試工具ab;瞭解一般web伺服器對於靜態檔案的處理流程。
Node.js和PHP
早期有很多關於Node.js爭論的焦點都在它的單執行緒模型方面,在由Jani Hartikainen寫的一篇著名的文章《PHP優於Node.js的五大理由》中,更有一條矛頭直接指向Node.js單執行緒脆弱的問題。
如果PHP程式碼損壞,不會拖垮整個伺服器。 PHP程式碼只執行在自己的程式範圍中,當某個請求顯示錯誤時,它只對特定的請求產生影響。而在Node.js環境中,所有的請求均在單一的程式服務中,當某個請求導致未知錯誤時,整個伺服器都會受到影響。
Node.js和Apache+PHP還有一個非常不同的地方就是程式的執行時間長短,當然這一點也被此文作為一個PHP優於Node.js的理由來寫了。
PHP程式短暫。在PHP中,每個程式對請求持續的時間很短暫,這就意味著你不必為資源配置和記憶體而擔憂。而Node.js的程式需要執行很長一段時間,你需要小心並妥善管理好記憶體。比如,如果你忘記從全域性資料中刪除條目,這會輕易的導致記憶體洩露。
在這裡我們並不想引起一次關於PHP和Node.js孰優孰劣的口水仗,PHP和Node.js各代表著一個網際網路時代的開發語言,就如同我們討論跑車和越野車誰更好一樣,它們都有自己所擅長和適用的場景。我們可以通過下面這兩張圖深入理解一下PHP和Node.js對處理Http請求時的區別。
PHP的模型:
Node.js的模型:
所以你在編寫Node.js程式碼時,要保持清醒的頭腦,任何一個隱藏著的異常被觸發後,都會將整個Node.js程式擊潰。但是這樣的特性也為我們編寫程式碼帶來便利,比如同樣要實現一個簡單的網站訪問次數統計,Node.js只需要在記憶體裡定義一個變數var count=0;,每次有使用者請求過來執行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/');
但是對於PHP來說就需要使用第三方媒介來儲存這個count值了,比如建立一個count.txt檔案來儲存網站的訪問次數。
<?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]"; ?>
單執行緒的js
Google的V8 Javascript引擎已經在Chrome瀏覽器裡證明了它的效能,所以Node.js的作者Ryan Dahl選擇了v8作為Node.js的執行引擎,v8賦予Node.js高效效能的同時也註定了Node.js和大名鼎鼎的Nginx一樣,都是以單執行緒為基礎的,當然這也正是作者Ryan Dahl設計Node.js的初衷。
單執行緒的優缺點
Node.js的單執行緒具有它的優勢,但也並非十全十美,在保持單執行緒模型的同時,它是如何保證非阻塞的呢?
高效能
首先,單執行緒避免了傳統PHP那樣頻繁建立、切換執行緒的開銷,使執行速度更加迅速。第二,資源佔用小,如果有對Node.js的web伺服器做過壓力測試的朋友可能發現,Node.js在大負荷下對記憶體佔用仍然很低,同樣的負載PHP因為一個請求一個執行緒的模型,將會佔用大量的實體記憶體,很可能會導致伺服器因實體記憶體耗盡而頻繁交換,失去響應。
執行緒安全
單執行緒的js還保證了絕對的執行緒安全,不用擔心同一變數同時被多個執行緒進行讀寫而造成的程式崩潰。比如我們之前做的web訪問統計,因為單執行緒的絕對執行緒安全,所以不可能存在同時對count變數進行讀寫的情況,我們的統計程式碼就算是成百的併發使用者請求都不會出現問題,相較PHP的那種存檔案記錄訪問,就會面臨併發同時寫檔案的問題。執行緒安全的同時也解放了開發人員,免去了多執行緒程式設計中忘記對變數加鎖或者解鎖造成的悲劇。
單執行緒的非同步和非阻塞
Node.js是單執行緒的,但是它如何做到I/O的非同步和非阻塞的呢?其實Node.js在底層訪問I/O還是多執行緒的,有興趣的朋友可以翻看Node.js的fs模組的原始碼,裡面會用到libuv來處理I/O,所以在我們看來Node.js的程式碼就是非阻塞和非同步形式的。
阻塞/非阻塞與非同步/同步是兩個不同的概念,同步不代表阻塞,但是阻塞肯定就是同步了。
舉個現實生活中的例子,我去食堂打飯,我選擇了A套餐,然後工作人員幫我去配餐,如果我就站在旁邊,等待工作人員給我配餐,這種情況就稱之為同步;若工作人員幫我配餐的同時,排在我後面的人就開始點餐,這樣整個食堂的點餐服務並沒有因為我在等待A套餐而停止,這種情況就稱之為非阻塞。這個例子就簡單說明了同步但非阻塞的情況。
再如果我在等待配餐的時候去買飲料,等聽到叫號再回去拿套餐,此時我的飲料也已經買好,這樣我在等待配餐的同時還執行了買飲料的任務,叫號就等於執行了回撥,就是非同步非阻塞了。
阻塞的單執行緒
既然Node.js是單執行緒非同步非阻塞的,是不是我們就可以高枕無憂了呢?
還是拿上面那個買套餐的例子,如果我在買飲料的時候,已經叫我的號讓我去拿套餐,可是我等了好久才拿到飲料,所以我可能在大廳叫我的餐號之後很久才拿到A套餐,這也就是單執行緒的阻塞情況。
在瀏覽器中,js都是以單執行緒的方式執行的,所以我們不用擔心js同時執行帶來的衝突問題,這對於我們編碼帶來很多的便利。
但是對於在服務端執行的Node.js,它可能每秒有上百個請求需要處理,對於在瀏覽器端工作良好的單執行緒js是否也能同樣在服務端表現良好呢?
我們看如下程式碼:
var start = Date.now();//獲取當前時間戳 setTimeout(function () { console.log(Date.now() - start); for (var i = 0; i < 1000000000; i++){//執行長迴圈 } }, 1000); setTimeout(function () { console.log(Date.now() - start); }, 2000);
最終我們的列印結果是:(結果可能因為你的機器而不同)
1000 3738
對於我們期望2秒後執行的setTimeout函式其實經過了3738毫秒之後才執行,換而言之,因為執行了一個很長的for迴圈,所以我們整個Node.js主執行緒被阻塞了,如果在我們處理100個使用者請求中,其中第一個有需要這樣大量的計算,那麼其餘99個就都會被延遲執行。
其實雖然Node.js可以處理數以千記的併發,但是一個Node.js程式在某一時刻其實只是在處理一個請求。
單執行緒和多核
執行緒是cpu排程的一個基本單位,一個cpu同時只能執行一個執行緒的任務,同樣一個執行緒任務也只能在一個cpu上執行,所以如果你執行Node.js的機器是像i5,i7這樣多核cpu,那麼將無法充分利用多核cpu的效能來為Node.js服務。
多執行緒
在C++、C#、python等其他語言都有與之對應的多執行緒程式設計,有些時候這很有趣,帶給我們靈活的程式設計方式;但是也可能帶給我們一堆麻煩,需要學習更多的Api知識,在編寫更多程式碼的同時也存在著更多的風險,執行緒的切換和鎖也會造成系統資源的開銷。
就像上面的那個例子,如果我們的Node.js有建立子執行緒的能力,那問題就迎刃而解了:
var start = Date.now(); createThread(function () { //建立一個子執行緒執行這10億次迴圈 console.log(Date.now() - start); for (var i = 0; i < 1000000000; i++){} }); setTimeout(function () { //因為10億次迴圈是在子執行緒中執行的,所以主執行緒不受影響 console.log(Date.now() - start); }, 2000);
可惜也可以說可喜的是,Node.js的核心模組並沒有提供這樣的api給我們,我們真的不想多執行緒又迴歸回來。不過或許多執行緒真的能夠解決我們某方面的問題。
tagg2模組
Jorge Chamorro Bieling是tagg(Threads a gogo for Node.js)包的作者,他硬是利用phread庫和C語言讓Node.js支援了多執行緒的開發,我們看一下tagg模組的簡單示例:
var Threads = require('threads_a_gogo');//載入tagg包 function fibo(n) {//定義斐波那契陣列計算函式 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)丟入子執行緒執行 if (err) throw err; //執行緒建立失敗 console.log('fibo(35)=' + result);//列印fibo執行35次的結果 }); console.log('not block');//列印資訊了,表示沒有阻塞
上面這段程式碼利用tagg包將fibo(35)這個計算丟入了子執行緒中進行,保證了Node.js主執行緒的舒暢,當子執行緒任務執行完畢將會執行主執行緒的回撥函式,把結果列印到螢幕上,執行結果如下:
not block fibo(35)=14930352
斐波那契數列,又稱黃金分割數列,這個數列從第三項開始,每一項都等於前兩項之和:0、1、1、2、3、5、8、13、21、……。
注意我們上面程式碼的斐波那契陣列演算法並不是最優演算法,只是為了模擬cpu密集型計算任務。
由於tagg包目前只能在linux下安裝執行,所以我fork了一個分支,修改了部分tagg包的程式碼,釋出了tagg2包。tagg2包同樣具有tagg包的多執行緒功能,採用新的node-gyp命令進行編譯,同時它跨平臺支援,mac,linux,windows下都可以使用,對開發人員的api也更加友好。安裝方法很簡單,直接npm install tagg2。
一個利用tagg2計算斐波那契陣列的http伺服器程式碼:
var express = require('express'); var tagg2 = require("tagg2"); var app = express(); var th_func = function(){//執行緒執行函式,以下內容會線上程中執行 var fibo =function fibo (n) {//在子執行緒中定義fibo函式 return n > 1 ? fibo(n - 1) + fibo(n - 2) : 1; } var n = fibo(~~thread.buffer);//執行fibo遞迴 thread.end(n);//當執行緒執行完畢,執行thread.end帶上計算結果回撥主執行緒 }; app.get('/', function(req, res){ var n = ~~req.query.n || 1;//獲取使用者請求引數 var buf = new Buffer(n.toString()); tagg2.create(th_func, {buffer:buf}, function(err,result){ //建立一個js執行緒,傳入工作函式,buffer引數以及回撥函式 if(err) return res.end(err);//如果執行緒建立失敗 res.end(result.toString());//響應執行緒執行計算的結果 }) }); app.listen(8124); console.log('listen on 8124');
其中~~req.query.n表示將使用者傳遞的引數n取整,功能類似Math.floor函式。
我們用express框架搭建了一個web伺服器,根據使用者傳送的引數n的值來建立子執行緒計算斐波那契陣列,當子執行緒計算完畢之後將結果響應給客戶端。由於計算是丟入子執行緒中執行的,所以整個主執行緒不會被阻塞,還是能夠繼續處理新請求的。
我們利用apache的http壓力測試工具ab來進行一次簡單的壓力測試,看看執行斐波那契陣列35次,100客戶端併發100個請求,我們的QPS (Query Per Second)每秒查詢率在多少。
ab的全稱是ApacheBench,是Apache附帶的一個小工具,用於進行HTTP伺服器的效能測試,可以同時模擬多個併發請求。
我們的測試硬體:linux 2.6.4 4cpu 8G 64bit,網路環境則是內網。
ab壓力測試命令:
ab -c 100 -n 100 http://192.168.28.5:8124/?n=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: 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表示每秒我們伺服器處理的任務數量,這裡是17.84。第二個我們比較關心的是兩個Time per request結果,上面一行Time per request:5605.769 [ms](mean)表示當前這個併發量下處理每組請求的時間,而下面這個Time per request:56.058 [ms](mean, across all concurrent requests)表示每個使用者平均處理時間,因為我們本次測試併發是100,所以結果正好是上一行的100分之1。得出本次測試平均每個使用者請求的平均等待時間為56.058 [ms]。
另外我們看下最後帶有百分比的列表,可以看到50%的使用者是在5531 ms以內返回的,最慢的也不過5602 ms,響應延遲非常的平均。
我們如果用cluster來啟動4個程式,是否可以充分利用cpu達到tagg2那樣的QPS呢?我們在同樣的網路環境和測試機上執行如下程式碼:
var cluster = require('cluster');//載入clustr模組 var numCPUs = require('os').cpus().length;//設定啟動程式數為cpu個數 if (cluster.isMaster) { for (var i = 0; i < numCPUs; i++) { cluster.fork();//啟動子程式 } } else { var express = require('express'); var app = express(); var fibo = function fibo (n) {//定義斐波那契陣列演算法 return n > 1 ? fibo(n - 1) + fibo(n - 2) : 1; } app.get('/', function(req, res){ var n = fibo(~~req.query.n || 1);//接收引數 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
我們成功啟動了4個cluster之後,用同樣的ab壓力測試命令對8124埠進行測試,結果如下:
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)
通過和上面tagg2包的測試結果對比,我們發現區別很大。首先每秒處理的任務數從17.84 [#/sec]下降到了9.52 [#/sec],這說明我們web伺服器整體的吞吐率下降了;然後每個使用者請求的平均等待時間也從56.058 [ms]提高到了105.088 [ms],使用者等待的時間也更長了。
最後我們發現使用者請求處理的時長非常的不均勻,50%的使用者在2934 ms內返回了,最慢的等待達到了10504 ms。雖然我們使用了cluster啟動了4個Node.js程式處理使用者請求,但是對於每個Node.js程式來說還是單執行緒的,所以當有4個使用者跑滿了4個Node.js的cluster程式之後,新來的使用者請求就只能等待了,最後造成了先到的使用者處理時間短,後到的使用者請求處理時間比較長,就造成了使用者等待時間非常的不平均。
v8引擎
大家看到這裡是不是開始心潮澎湃,感覺js一統江湖的時代來臨了,單執行緒非同步非阻塞的模型可以勝任大併發,同時開發也非常高效,多執行緒下的js可以承擔cpu密集型任務,不會有主執行緒阻塞而引起的效能問題。
但是,不論tagg還是tagg2包都是利用phtread庫和v8的v8::Isolate Class類來實現js多執行緒功能的。
Isolate代表著一個獨立的v8引擎例項,v8的Isolate擁有完全分開的狀態,在一個Isolate例項中的物件不能夠在另外一個Isolate例項中使用。嵌入式開發者可以在其他執行緒建立一些額外的Isolate例項並行執行。在任何時刻,一個Isolate例項只能夠被一個執行緒進行訪問,可以利用加鎖/解鎖進行同步操作。
換而言之,我們在進行v8的嵌入式開發時,無法在多執行緒中訪問js變數,這條規則將直接導致我們之前的tagg2裡面執行緒執行的函式無法使用Node.js的核心api,比如fs,crypto等模組。如此看來,tagg2包還是有它使用的侷限性,針對一些可以使用js原生的大量計算或迴圈可以使用tagg2,Node.js核心api因為無法從主執行緒共享物件的關係,也就不能跨執行緒使用了。
libuv
最後,如果我們非要讓Node.js支援多執行緒,還是提倡使用官方的做法,利用libuv庫來實現。
libuv是一個跨平臺的非同步I/O庫,它主要用於Node.js的開發,同時他也被Mozilla's Rust language, Luvit, Julia, pyuv等使用。它主要包括了Event loops事件迴圈,Filesystem檔案系統,Networking網路支援,Threads執行緒,Processes程式,Utilities其他工具。
在Node.js核心api中的非同步多執行緒大多是使用libuv來實現的,下一章將帶領大家開發一個讓Node.js支援多執行緒並基於libuv的Node.js包。
多程式
在支援html5的瀏覽器裡,我們可以使用webworker來將一些耗時的計算丟入worker程式中執行,這樣主程式就不會阻塞,使用者也就不會有卡頓的感覺了。在Node.js中是否也可以使用這類技術,保證主執行緒的通暢呢?
cluster
cluster可以用來讓Node.js充分利用多核cpu的效能,同時也可以讓Node.js程式更加健壯,官網上的cluster示例已經告訴我們如何重新啟動一個因為異常而奔潰的子程式。
webworker
想要像在瀏覽器端那樣啟動worker程式,我們需要利用Node.js核心api裡的child_process模組。child_process模組提供了fork的方法,可以啟動一個Node.js檔案,將它作為worker程式,當worker程式工作完畢,把結果通過send方法傳遞給主程式,然後自動退出,這樣我們就利用了多程式來解決主執行緒阻塞的問題。
我們先啟動一個web服務,還是接收引數計算斐波那契陣列:
var express = require('express'); var fork = require('child_process').fork; var app = express(); app.get('/', function(req, res){ var worker = fork('./work_fibo.js') //建立一個工作程式 worker.on('message', function(m) {//接收工作程式計算結果 if('object' === typeof m && m.type === 'fibo'){ worker.kill();//傳送殺死程式的訊號 res.send(m.result.toString());//將結果返回客戶端 } }); worker.send({type:'fibo',num:~~req.query.n || 1}); //傳送給工作程式計算fibo的數量 }); app.listen(8124);
我們通過express監聽8124埠,對每個使用者的請求都會去fork一個子程式,通過呼叫worker.send方法將引數n傳遞給子程式,同時監聽子程式傳送訊息的message事件,將結果響應給客戶端。
下面是被fork的work_fibo.js檔案內容:
var fibo = function fibo (n) {//定義演算法 return n > 1 ? fibo(n - 1) + fibo(n - 2) : 1; } process.on('message', function(m) { //接收主程式傳送過來的訊息 if(typeof m === 'object' && m.type === 'fibo'){ var num = fibo(~~m.num); //計算jibo process.send({type: 'fibo',result:num}) //計算完畢返回結果 } }); process.on('SIGHUP', function() { process.exit();//收到kill資訊,程式退出 });
我們先定義函式fibo用來計算斐波那契陣列,然後監聽了主執行緒發來的訊息,計算完畢之後將結果send到主執行緒。同時還監聽process的SIGHUP事件,觸發此事件就程式退出。
這裡我們有一點需要注意,主執行緒的kill方法並不是真的使子程式退出,而是會觸發子程式的SIGHUP事件,真正的退出還是依靠process.exit();。
下面我們用ab 命令測試一下多程式方案的處理效能和使用者請求延遲,測試環境不變,還是100個併發100次請求,計算斐波那切陣列第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)
壓力測試結果QPS約為14.21,相比cluster來說,還是快了很多,每個使用者請求的延遲都很平均,因為程式的建立和銷燬的開銷要大於執行緒,所以在效能方面略低於tagg2,不過相對於cluster方案,這樣的提升還是令我們滿意的。
換一種思路
使用child_process模組的fork方法確實可以讓我們很好的解決單執行緒對cpu密集型任務的阻塞問題,同時又沒有tagg2包那樣無法使用Node.js核心api的限制。
但是如果我的worker具有多樣性,每次在利用child_process模組解決問題時都需要去建立一個worker.js的工作函式檔案,有點麻煩。我們是不是可以更加簡單一些呢?
在我們啟動Node.js程式時,node命令可以帶上-e這個引數,它將直接執行-e後面的字串,如下程式碼就將列印出hello world。
node -e "console.log('hello world')"
合理的利用這個特性,我們就可以免去每次都建立一個檔案的麻煩。
var express = require('express'); var spawn = require('child_process').spawn; var app = express(); var spawn_worker = function(n,end){//定義工作函式 var fibo = function fibo (n) { return n > 1 ? fibo(n - 1) + fibo(n - 2) : 1; } end(fibo(n)); } var spawn_end = function(result){//定義工作函式結束的回撥函式引數 console.log(result); process.exit(); } app.get('/', function(req, res){ var n = ~~req.query.n || 1; //拼接-e後面的引數 var spawn_cmd = '('+spawn_worker.toString()+'('+n+','+spawn_end.toString()+'));' console.log(spawn_cmd);//注意這個列印結果 var worker = spawn('node',['-e',spawn_cmd]);//執行node -e "xxx"命令 var fibo_res = ''; worker.stdout.on('data', function (data) { //接收工作函式的返回 fibo_res += data.toString(); }); worker.on('close', function (code) {//將結果響應給客戶端 res.send(fibo_res); }); }); app.listen(8124);
程式碼很簡單,我們主要關注3個地方。
第一、我們定義了spawn_worker函式,他其實就是將會在-e後面執行的工作函式,所以我們把計算斐波那契陣列的演算法定義在內,spawn_worker函式接收2個引數,第一個引數n表示客戶請求要計算的斐波那契陣列的位數,第二個end引數是一個函式,如果計算完畢則執行end,將結果傳回主執行緒;
第二、真正當Node.js腳步執行的字串其實就是spawn_cmd裡的內容,它的內容我們通過執行之後的列印資訊,很容易就能明白;
第三、我們利用child_process的spawn方法,類似在命令列裡執行了node -e "js code",啟動Node.js工作程式,同時監聽子程式的標準輸出,將資料儲存起來,當子程式退出之後把結果響應給使用者。
現在主要的焦點就是變數spawn_cmd到底儲存了什麼,我們開啟瀏覽器在位址列裡輸入:
http://127.0.0.1:8124/?n=35
下面就是程式執行之後的列印資訊,
(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(); }));
對於在子程式執行的工作函式的兩個引數n和end現在一目瞭然,n代表著使用者請求的引數,期望獲得的斐波那契陣列的位數,而end引數則是一個匿名函式,在標準輸出中列印計算結果然後退出程式。
node -e命令雖然可以減少建立檔案的麻煩,但同時它也有命令列長度的限制,這個值各個系統都不相同,我們通過命令getconf ARG_MAX來獲得最大命令長度,例如:MAC OSX下是262,144 byte,而我的linux虛擬機器則是131072 byte。
多程式和多執行緒
大部分多執行緒解決cpu密集型任務的方案都可以用我們之前討論的多程式方案來替代,但是有一些比較特殊的場景多執行緒的優勢就發揮出來了,下面就拿我們最常見的http web伺服器響應一個小的靜態檔案作為例子。
以express處理小型靜態檔案為例,大致的處理流程如下: 1、首先獲取檔案狀態,判斷檔案的修改時間或者判斷etag來確定是否響應304給客戶端,讓客戶端繼續使用本地快取。 2、如果快取已經失效或者客戶端沒有快取,就需要獲取檔案的內容到buffer中,為響應作準備。 3、然後判斷檔案的MIME型別,如果是類似html,js,css等靜態資源,還需要gzip壓縮之後傳輸給客戶端 4、最後將gzip壓縮完成的靜態檔案響應給客戶端。
下面是一個正常成功的Node.js處理靜態資源無快取流程圖:
這個流程中的(2),(3),(4)步都經歷了從js到C++ ,開啟和釋放檔案,還有呼叫了zlib庫的gzip演算法,其中每個非同步的演算法都會有建立和銷燬執行緒的開銷,所以這樣也是大家詬病Node.js處理靜態檔案不給力的原因之一。
為了改善這個問題,我之前有利用libuv庫開發了一個改善Node.js的http/https處理靜態檔案的包,名為ifile,ifile包,之所以可以加速Node.js的靜態檔案處理效能,主要是減少了js和C++的互相呼叫,以及頻繁的建立和銷燬執行緒的開銷,下圖是ifile包處理一個靜態無快取資源的流程圖:
由於全部工作都是在libuv的子執行緒中執行的,所以Node.js主執行緒不會阻塞,當然效能也會大幅提升了,使用ifile包非常簡單,它能夠和express無縫的對接。
var express = require('express'); var ifile = require("ifile"); var app = express(); app.use(ifile.connect()); //預設值是 [['/static',__dirname]]; app.listen(8124);
上面這4行程式碼就可以讓express把靜態資源交給ifile包來處理了,我們在這裡對它進行了一個簡單的壓力測試,測試用例為響應一個大小為92kb的jquery.1.7.1.min.js檔案,測試命令:
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",表示響應的靜態檔案希望是gzip壓縮之後的,所以ifile將會把壓縮之後的jquery.1.7.1.min.js檔案響應給客戶端。結果如下:
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一項結果為33016 bytes說明我們的jquery檔案已經被成功的gzip壓縮,因為原始檔大小是92kb;其次,我們最關心的Requests per second:542.16 [#/sec](mean),說明我們每秒能處理542個任務;最後,我們看到,在這樣的壓力情況下,平均每個使用者的延遲在1.844 [ms]。
我們看下使用express框架處理這樣的壓力會是什麼樣的結果,express測試程式碼如下:
var express = require('express'); var app = express(); app.use(express.compress());//支援gzip app.use('/static', express.static(__dirname + '/static')); app.listen(8124);
程式碼同樣非常簡單,注意這裡我們使用:
app.use('/static', express.static(__dirname + '/static'));
而不是:
app.use(express.static(__dirname));
後者每個請求都會去匹配一次檔案是否存在,而前者只有請求url是/static開頭的才會去匹配靜態資源,所以前者效率更高一些。然後我們執行相同的ab壓力測試命令看下結果:
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)
同樣分析一下結果,Document Length:33064 bytes表示文件大小為33064 bytes,說明我們的gzip起作用了,每秒處理任務數從ifile包的542下降到了300,最長使用者等待時間也延長到了9367 ms,可見我們的努力起到了立竿見影的作用,js和C++互相呼叫以及執行緒的建立和釋放並不是沒有損耗的。
但是當我在express的谷歌論壇裡貼上這些測試結果,並宣傳ifile包的時候,express的作者TJ,給出了不一樣的評價,他在回覆中說道:
請牢記你可能不需要這麼高等級吞吐率的系統,就算是每月百萬級別下載量的npm網站,也僅僅每秒處理17個請求而已,這樣的壓力甚至於PHP也可以處理掉(又黑了一把php)。
確實如TJ所說,效能只是我們專案的指標之一而非全部,一味的去追求高效能並不是很理智。
ifile包開源專案地址:https://github.com/DoubleSpout/ifile
總結
單執行緒的Node.js給我們編碼帶來了太多的便利和樂趣,我們應該時刻保持清醒的頭腦,在寫Node.js程式碼中切不可與PHP混淆,任何一個隱藏的問題都可能擊潰整個線上正在執行的Node.js程式。
單執行緒非同步的Node.js不代表不會阻塞,在主執行緒做過多的任務可能會導致主執行緒的卡死,影響整個程式的效能,所以我們要非常小心的處理大量的迴圈,字串拼接和浮點運算等cpu密集型任務,合理的利用各種技術把任務丟給子執行緒或子程式去完成,保持Node.js主執行緒的暢通。
執行緒/程式的使用並不是沒有開銷的,儘可能減少建立和銷燬執行緒/程式的次數,可以提升我們系統整體的效能和出錯的概率。
最後請不要一味的追求高效能和高併發,因為我們可能不需要系統具有那麼大的吞吐率。高效,敏捷,低成本的開發才是專案所需要的,這也是為什麼Node.js能夠在眾多開發語言中脫穎而出的關鍵。
參考文獻:
- http://smashingnode.com Smashing Node.JS By Guillermo Rauch
- http://bjouhier.wordpress.com/2012/03/11/fibers-and-threads-in-node-js-what-for Fibers and Threads in node.js – what for? By Bruno's Ramblings
- https://github.com/xk/node-threads-a-gogo TAGG: Threads à gogo for Node.js By Jorge Chamorro Bieling
- https://code.google.com/p/v8/ Google v8
- https://github.com/joyent/libuv libuv by joyent
相關文章
- Node.js 中的程式和執行緒Node.js執行緒
- Node.js 的單執行緒事件驅動模型和內建的執行緒池模型Node.js執行緒事件模型
- 程式和執行緒理解執行緒
- android程式和執行緒Android執行緒
- 程式和執行緒模型執行緒模型
- 執行緒和程式的區別執行緒
- 執行緒和程式基礎以及多執行緒的基本使用(iOS)執行緒iOS
- 程式和執行緒簡介執行緒
- Python 中執行緒和程式Python執行緒
- Java™ 教程(程式和執行緒)Java執行緒
- 漫談程式和執行緒執行緒
- 執行緒和執行緒池執行緒
- Python的多程式和多執行緒Python執行緒
- 瞭解 Android 的程式和執行緒Android執行緒
- 執行緒和程式的優缺點執行緒
- 程式、執行緒和協程的概念執行緒
- 多執行緒------執行緒與程式/執行緒排程/建立執行緒執行緒
- 程式執行緒篇——程式執行緒基礎執行緒
- nodejs裡面的程式和執行緒NodeJS執行緒
- 前置知識—程式和執行緒執行緒
- 「Learning」區別執行緒和程式執行緒
- Node.js 多執行緒完全指南Node.js執行緒
- Node.js中執行緒的完整指南 – LogRocketNode.js執行緒
- 程式-程式-執行緒執行緒
- 執行緒以及多執行緒,多程式的選擇執行緒
- 多執行緒和多執行緒同步執行緒
- [短文速讀 -5] 多執行緒程式設計引子:程式、執行緒、執行緒安全執行緒程式設計
- 程式執行緒篇——執行緒切換(上)執行緒
- 程式執行緒篇——執行緒切換(下)執行緒
- 執行緒、執行緒與程式、ULT與KLT執行緒
- Android程式框架:執行緒與執行緒池Android框架執行緒
- 程式和執行緒的區別與聯絡執行緒
- java架構-執行緒和程式的區別Java架構執行緒
- 執行緒(一)——執行緒,執行緒池,Task概念+程式碼實踐執行緒
- Python學習之程式和執行緒Python執行緒
- 寶付初識單執行緒的 Node.js執行緒Node.js
- 【java】【多執行緒】程式、執行緒的基本概念(1)Java執行緒
- java執行緒之守護執行緒和使用者執行緒Java執行緒
- 程式和執行緒有什麼區別?(Process and Threads)程式之間和執行緒之間是如何通訊的?執行緒thread