Node.js理解

JavaScript單線程的誤解

在我接觸JavaScript(不管瀏覽器仍是NodeJS)的時間裏,老是遇到有朋友有多線程的需求。而在NodeJS方面,有朋友甚至直接說到,NodeJS是單線程的,沒法很好的利用多核CPU。html

誠然,在前端的瀏覽器中,因爲前端的JavaScript與UI佔據同一線程,執行JavaScript確實爲UI響應形成了必定程度上的麻煩。可是,除非用到超大的循環語句執行JavaScript,或是用阻塞式的Ajax,或是太過頻繁的定時器執行外,JavaScript並無給前端應用帶來明顯的問題,因此也不多有朋友抱怨JavaScript是單線程而不能很好利用多核CPU的問題,由於沒有所以出現性能瓶頸。前端

可是,咱們能夠用Ajax和Web Worker迴應這個誤解。當Ajax請求發送以後,除非是同步請求,不然其他的JavaScript代碼會很快被執行到。在Ajax發送完成,直到接收到響應的這段時間裏,這個網絡請求並不會阻塞JavaScript的執行,而網絡請求已經發生,這是必然的事。那麼,答案就很明顯了,JavaScript確實是執行在單線程上的,可是,整個Web應用執行的宿主(瀏覽器)並不是以單線程的方式在執行。而Web Worker的產生,就是直接爲了解決JavaScript與UI佔用同一線程形成的UI響應問題的,它能新開一條線程去執行JavaScript。node

 

同理,NodeJS中的JavaScript也確實是在單線程上執行,可是做爲宿主的NodeJS,它自己並不是是單線程的,NodeJS在I/O方面有動用到一小部分額外的線程協助實現異步。程序員沒有機會直接建立線程,這也是有的同窗想固然的認爲NodeJS的單線程沒法很好的利用多核CPU的緣由,他們甚至會說,不可思議由多人一塊兒協做開發一個單線程的程序。git

NodeJS 封裝了內部的異步實現後,致使程序員沒法直接操做線程,也就形成全部的業務邏輯運算都會丟到JavaScript的執行線程上,這也就意味着,在高併發請求的時候,I/O的問題是很好的解決了,可是全部的業務邏輯運算聚沙成塔地都運行在JavaScript線程上,造成了一條擁擠的JavaScript運算線程。NodeJS的弱點在這個時候會暴露出來,單線程執行運算造成的瓶頸,拖慢了I/O的效率。這大概能夠算得上是密集運算狀況下沒法很好利用多核 CPU的缺點。這條擁擠的JavaScript線程,給I/O造成了性能上限。程序員

可是,事情又並不是絕對的。回到前端瀏覽器中,爲了解決線程擁擠的狀況,Web Worker應運而生。而一樣,Node也提供了child_process.fork來建立Node的子進程。在一個Node進程就能很好的解決密集 I/O的狀況下,fork出來的其他Node子進程能夠看成常駐服務來解決運算阻塞的問題(將運算分發到多個Node子進程中上去,與Apache建立多個子進程相似)。固然child_process/Web Worker的機制永遠只能解決單臺機器的問題,大的Web應用是不可能一臺服務器就能完成全部的請求服務的。拜NodeJS在I/O上的優點,跨OS的多Node之間通訊的是不算什麼問題的。解決NodeJS的運算密集問題的答案其實也是很是簡單的,就是將運算分發到多個CPU上。請參考文章後的multi-node的性能測試,能夠看到在多Node進程的情景下,響應請求的速度被大幅度提升(感謝CNode社區的snoopy友情測試)。github

 

在文章的寫做過程當中,Node最新發布的0.6.0版本,新增了cluster模塊。該模塊的做用是能夠經過fork的方式建立出多個子進程實例,這些實例會自動共享相同的偵聽端口。你能夠根據當前計算機上的CPU數量來建立相應的實例數,以此達到分發請求,充分利用CPU的目的。詳情請參閱官方文檔。在以前的解決運算密集問題中,工程師須要multi-node這樣的庫或者其餘方案去手動分發請求,在cluster模塊的支持下,能夠釋放掉工程師在解決此問題上的大部分精力。apache

事件式編程

延續上一節的討論。咱們知道NodeJS/JavaScript具備異步的特性,從NodeJS的API設計中能夠看出來,任何涉及I/O的操做,幾乎都被設計成事件回調的形式,且大多數的類都繼承自EventEmitter。這麼作的好處有兩個,一個是充分利用無阻塞I/O的特性,提升性能;另外一個好處則是封裝了底層的線程細節,經過事件消息留出業務的關注點給編程者,從而不用關注多線程編程裏牽扯到的諸多技術細節。編程

從現實的角度而言,事件式編程也更貼合現實。舉一個業務場景爲例:家庭主婦在家中準備中餐,她須要完成兩道菜,一道拌黃瓜,一道西紅柿蛋湯。以PHP爲例,家庭主婦會先作完拌黃瓜,接着完成西紅柿蛋湯,是以順序/串行執行的。可是如今忽然出了一點意外,涼拌黃瓜須要的醬油用光了,須要她兒子出門幫她買醬油回來。那麼PHP家庭主婦在叫她兒子出門打醬油的這段時間都是屬於等待狀態的,直到醬油買回來,纔會繼續下一道菜的製做。那麼,在NodeJS的家庭主婦又會是怎樣一個場景呢,很明顯,在等待兒子打醬油回來的時間裏,她能夠暫停涼拌黃瓜的製做,而直接進行西紅柿蛋湯的過程,兒子打完醬油回來,繼續完成她的涼拌黃瓜。沒有浪費掉等待的時間。實例僞代碼以下:api

var mother = new People();
var child = new People();
child.buySoy(function (soy) {
    mother.cook("cucumber", soy);
});
mother.cook("tomato");

接下來,將上面這段代碼轉換爲基於事件/任務異步模式的代碼:promise

var proxy = new EventProxy();
var mother = new People();
proxy.bind("cook_cucumber", function (soy) {
    mother.cook("cucumber", soy);
});
proxy.bind("cook_tomato", function () {
    mother.cook("tomato");
});
var child = new People();
child.buySoy(function (soy) {
    proxy.trigger("cucumber", soy);
});
proxy.trigger("tomato");

代碼量多了不少,可是業務邏輯點都是很清楚的:經過bind方法預約義了cook_cucumber和cook_tomato兩個任務。這裏的bind方法能夠認爲是await的消息式實現,須要第一個參數來標識該任務的名字,流程在執行的過程當中產生的消息會觸發這些任務執行。能夠看出,事件式編程中,用戶只須要關注它所須要的幾個業務事件點就能夠,中間的等待都由底層爲你調配好了。這裏的代碼只是舉例事件/任務異步模式而用,在簡單的場景中,第一段代碼便可。作NodeJS的編程,會更感受是在作現實的業務場景設計和任務調度,沒有順序保證,程序結構更像是一個狀態機。

我的以爲在事件式編程中,程序員須要轉換一下思惟,才能接受和發揮好這種異步/無阻塞的優點。一樣,這種事件式編程帶來的一個問題就在於業務邏輯是鬆散和碎片式的。這對習慣了順序式,Promise式編程的同窗而言,接受它是比較痛苦的事情,並且這種散佈的業務邏輯對於非一開始就清楚設計的人而言,閱讀存在至關大的問題。

我提到事件式編程更貼近於現實生活,是更天然的,因此這種編程風格也致使你的代碼跟你的生活同樣,是一件複雜的事情。幸運的是,本身的生活要本身去面對,對於一個項目而言,並不須要每一個人都去設計整個大業務邏輯,對於架構師而言,業務邏輯是明瞭的,藉助事件式編程帶來的業務邏輯鬆耦合的好處,在設定大框架後,將業務邏輯劃分爲適當的粒度,對每個實現業務點的程序員而言,並無這個痛苦存在。二八原則在這個地方很是有效。

深度嵌套回調問題

JavaScript/NodeJS 對單個異步事件的處理十分容易,但容易出現問題出現的地方是「多個異步事件之間的結果協做」。以NodeJS服務端渲染頁面爲例,渲染須要數據,模板,本地化資源文件,這三個部分都是要經過異步來獲取的,原生代碼的寫法會致使嵌套,由於只有這樣才能保證渲染的時候數據,模板,本地化資源都已經獲取到了。但問題是,這三個步驟之間實際是無耦合的,卻由於原生代碼沒有promise的機制,將能夠並行執行(充分利用無阻塞I/O)的步驟,變成串行執行的過程,直接下降了性能。代碼以下:

var render = function (template, data) {
    _.template(template, data);
};
$.get("template", function (template) { // something 
    $.get("data", function (data) { // something 
        $.get("l10n", function (l10n) { // something 
            render(template, data);
        });
    });
});

面對這樣的代碼,許多工程師都表示不爽。這個弱點也造成了對NodeJS推廣的一個不大不小的障礙。對於追求性能和維護性的同窗,確定不知足於以上的作法。本人對於JavaScript的事件和回調都略有偏心,而且認爲事件,回調,並行,鬆耦合是能夠達成一致的。如下一段代碼是用EventProxy實現的:

var proxy = new EventProxy();
var render = function (template, data, l10n) {
    _.template(template, data);
};
proxy.assign("template", "data", "l10n", render);
$.get("template", function (template) { // something 
    proxy.trigger("template", template);
});
$.get("data", function (data) { // something 
    proxy.trigger("data", data);
});
$.get("l10n", function (l10n) { // something 
    proxy.trigger("l10n", l10n);
});

代碼量看起來比原生實現略多,可是從邏輯而言十分清晰。模板、數據、本地化資源並行獲取,性能上的提升不言而喻,assign方法充分利用了事件機制來保證最終結果的正確性。在事件,回調,並行,鬆耦合幾個點上都達到指望的要求。

關於更多EventProxy的細節可參考其官方頁面

深度回調問題的延伸

EventProxy解決深度回調的方式徹底基於事件機制,這須要創建在事件式編程的認同上,那麼必然也存在對事件式編程不認同的同窗,並且習慣順序式,promise式,向其推廣bind/trigger模式實在難以被他們接受。JscexStreamline.js是目前比較成熟的同步式編程的解決方案。能夠經過同步式的思惟來進行編程,最終執行的代碼是經過編譯後的目標代碼,以此經過工具來協助用戶轉變思惟。

結語

對於優秀的東西,咱們不能由於其表面的瑕疵而棄之不用,總會有折衷的方案來知足需求。NodeJS在實時性方面的功效有目共睹,即使會有一些明顯的缺點,可是隨着一些解決方案的出現,相信沒有什麼能夠擋住其前進的腳步。

附錄(多核環境下的併發測試)

服務器環境:

  • 網絡環境:內網
  • 壓力測試服務器:
  • 服務器系統:Linux 2.6.18
  • 服務器配置:Intel(R) Xeon(TM) CPU 3.40GHz 4 CPUS
  • 內存:6GB
  • NodeJS版本: v0.4.12

客戶端測試環境:

  • 發包工具:apache 2.2.19自帶的ab測試工具
  • 服務器系統:Linux 2.6.18
  • 服務器配置:Pentium(R) Dual-Core CPU E5800 @ 3.20GHz 2CPUS
  • 內存:1GB

單線程Node代碼:

var http = require('http');
var server = http.createServer(function (request, response) {
    var j = 0;
    for (var i = 0; i & lt; 100000; i++) {
        j += 2 / 3;
    }
    response.end(j + '');
});
server.listen(8881);
console.log('Server running at http://10.1.10.150:8881/');

四進程Node代碼:

var http = require('http');
var server = http.createServer(function (request, response) {
    var j = 0;
    for (var i = 0; i & lt; 100000; i++) {
        j += 2 / 3;
    }
    response.end(j + '');
});
var nodes = require("./lib/multi-node").listen({
    port: 8883,
    nodes: 4
}, server);
console.log('Server running at http://10.1.10.150:8883/');

這裏簡單介紹一下multi-node這個插件,這個插件就是利用require("child_process").spawn()方法來建立多個子線程。因爲浮點計算和字符串拼接都是比較耗CPU的運算,因此這裏咱們循環10W次,每次對j加上0.66666。最後比較一下,開多子進程node到底比單進程node在CPU密集運算上快多少。

如下是測試結果:

Comm.

500/30

500/30

1000/30

1000/30

3000/30

3000/30

Type

單進程

多子進程

單進程

多子進程

單進程

多子進程

RPS

2595

5597

2540

5509

2571

5560

TPQ

0.38

0.18

0.39

0.19

0.39

0.18

80% REQ

72

65

102

85

157

142

Fail

0

0

0

0

0

0

 

說明:

  • 單進程:只開一個node.js進程。
  • 多子進程:開一個node.js進程,而且開3個它的子進程。
  • 3000/30:表明命令 ./ab -c 3000 -t 30 http://10.1.10.150:8888/。3000個客戶端,最多發30秒,最多發5W個請求。
  • RPS:表明每秒處理請求數,併發的主要指標。
  • TPQ:每一個請求處理的時間,單位毫秒。
  • Fail:表明平均處理失敗請求個數。
  • 80% Req:表明80%的請求在多少毫秒內返回。

從結果及圖1~3上看:開多個子進程能夠顯著緩解node.js的CPU利用率不足的狀況,提升node.js的CPU密集計算能力。

圖1:單個進程的node.js在壓力測試下的狀況,沒法充分利用4核CPU的服務器性能。

圖2:多進程node,能夠充分利用4核CPU榨乾服務器的性能。

圖3:多子進程截圖,能夠看到一共跑了4個進程。

相關文章
相關標籤/搜索