[轉] Node.js的線程和進程

[From] http://www.admin10000.com/document/4196.htmlphp

 

前言

  不少Node.js初學者都會有這樣的疑惑,Node.js究竟是單線程的仍是多線程的?經過本章的學習,可以讓讀者較爲清晰的理解Node.js對於單/多線程的關係和支持狀況。同時本章還將列舉一些讓Node.js的web服務器線程阻塞的例子,最後會提供Node.js碰到這類cpu密集型問題的解決方案。css

  在學習本章以前,讀者須要對Node.js有一個初步的認識,熟悉Node.js基本語法、cluster模塊、child_process模塊和express框架;接觸過apache的http壓力測試工具ab;瞭解通常web服務器對於靜態文件的處理流程。html

 Node.js和PHP

  早期有不少關於Node.js爭論的焦點都在它的單線程模型方面,在由Jani Hartikainen寫的一篇著名的文章《PHP優於Node.js的五大理由》中,更有一條矛頭直接指向Node.js單線程脆弱的問題。html5

若是PHP代碼損壞,不會拖垮整個服務器。 PHP代碼只運行在本身的進程範圍中,當某個請求顯示錯誤時,它只對特定的請求產生影響。而在Node.js環境中,全部的請求均在單一的進程服務中,當某個請求致使未知錯誤時,整個服務器都會受到影響。node

  Node.js和Apache+PHP還有一個很是不一樣的地方就是進程的運行時間長短,固然這一點也被此文做爲一個PHP優於Node.js的理由來寫了。python

PHP進程短暫。在PHP中,每一個進程對請求持續的時間很短暫,這就意味着你沒必要爲資源配置和內存而擔心。而Node.js的進程須要運行很長一段時間,你須要當心並妥善管理好內存。好比,若是你忘記從全局數據中刪除條目,這會輕易的致使內存泄露。jquery

  在這裏咱們並不想引發一次關於PHP和Node.js孰優孰劣的口水仗,PHP和Node.js各表明着一個互聯網時代的開發語言,就如同咱們討論跑車和越野車誰更好同樣,它們都有本身所擅長和適用的場景。咱們能夠經過下面這兩張圖深刻理解一下PHP和Node.js對處理Http請求時的區別。linux

  PHP的模型:git

  Node.js的模型:github

  因此你在編寫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三、2一、……。

  注意咱們上面代碼的斐波那契數組算法並非最優算法,只是爲了模擬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個地方。

  第1、咱們定義了spawn_worker函數,他其實就是將會在-e後面執行的工做函數,因此咱們把計算斐波那契數組的算法定義在內,spawn_worker函數接收2個參數,第一個參數n表示客戶請求要計算的斐波那契數組的位數,第二個end參數是一個函數,若是計算完畢則執行end,將結果傳回主線程;

  第2、真正當Node.js腳步執行的字符串其實就是spawn_cmd裏的內容,它的內容咱們經過運行以後的打印信息,很容易就能明白;

  第3、咱們利用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處理小型靜態文件爲例,大體的處理流程以下: 一、首先獲取文件狀態,判斷文件的修改時間或者判斷etag來肯定是否響應304給客戶端,讓客戶端繼續使用本地緩存。 二、若是緩存已經失效或者客戶端沒有緩存,就須要獲取文件的內容到buffer中,爲響應做準備。 三、而後判斷文件的MIME類型,若是是相似html,js,css等靜態資源,還須要gzip壓縮以後傳輸給客戶端 四、最後將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可以在衆多開發語言中脫穎而出的關鍵。

 參考文獻:

相關文章
相關標籤/搜索