設(shè)計(jì)高性能Web服務(wù)器的要點(diǎn)在于非阻塞I/O和事件驅(qū)動(dòng)
Node最大的特點(diǎn)是異步式I/O(非阻塞I/O)與事件緊密結(jié)合的編程模式曙痘,此模式與傳統(tǒng)同步式I/O線性的編程思維不同儡嘶,因?yàn)榭刂屏骱艽蟪潭纫揽渴录突卣{(diào)函數(shù)來(lái)組織溜族,一個(gè)邏輯要拆分為若干單元。
Node是JavaScript運(yùn)行時(shí)環(huán)境妨马,為JavaScript提供了一個(gè)異步I/O編程框架盅视。Node的指導(dǎo)思想:CPU執(zhí)行指令是非常快速的询刹,但I(xiàn)/O操作相對(duì)而言是極其緩慢的谜嫉。Node解決的是給CPU執(zhí)行的算法容易,I/O請(qǐng)求卻很頻繁的情況凹联。
同步/異步
當(dāng)請(qǐng)求到來(lái)時(shí)沐兰,相對(duì)于傳統(tǒng)的進(jìn)程或線程同步處理的方式。Node只在主線程中處理請(qǐng)求蔽挠,如果遇到I/O操作則以異步方式發(fā)起調(diào)用住闯,主線程立即返回,繼續(xù)處理之后的任務(wù)。由于異步比原,一次客戶端請(qǐng)求的處理方式由流式變?yōu)殡A段式插佛。而使用Node編寫(xiě)的JavaScript代碼都運(yùn)行在主線程中。
例如:假設(shè)一次客戶端請(qǐng)求分為三個(gè)階段
- 執(zhí)行函數(shù)A
- 一次I/O操作
- 執(zhí)行函數(shù)B
同步式處理
//同步式處理客戶端請(qǐng)求
function request(){
$result = stageA();//執(zhí)行函數(shù)A
$data = readfile();//讀取文件量窘,執(zhí)行一次I/O操作
stageB($result, $data);//執(zhí)行函數(shù)B雇寇,將前兩部的結(jié)果作為參數(shù)。
}
同步式處理中每個(gè)請(qǐng)求使用一個(gè)線程或進(jìn)程處理绑改,一次請(qǐng)求處理完畢后線程被回收谢床。上圖同步式處理只顯示了兩個(gè)線程。如果客戶端更多厘线,線程數(shù)量會(huì)隨之增加识腿。
異步式處理
//異步式處理流程
function request(){
var result = stageA();//執(zhí)行函數(shù)A
// 發(fā)起異步讀取,此時(shí)主線程立即返回造壮,處理后續(xù)任務(wù)渡讼。
readfileAsync(function(data){
//在隨后的循環(huán)中執(zhí)行回調(diào)函數(shù)
stageB(result, data);
});
}
Node異步執(zhí)行中,Node使用一個(gè)主線程解決了所有問(wèn)題耳璧,異步處理流程中成箫,每一個(gè)方塊代表了一個(gè)階段任務(wù)的執(zhí)行。
Node高性能的來(lái)源得益于異步的運(yùn)行方式旨枯,如何理解異步對(duì)性能的性能的提升了蹬昌。打個(gè)比方目前出入車(chē)輛管理規(guī)定,外地來(lái)的車(chē)輛進(jìn)京需要辦理進(jìn)京證攀隔,而辦進(jìn)京證需等待一定時(shí)間皂贩。如果每個(gè)人都自己跑去辦理,就好像開(kāi)啟多個(gè)線程同步處理昆汹,辦理窗口有限明刷,就得排隊(duì)。而把這件事委托給第三方满粗,就好比不開(kāi)啟線程或進(jìn)程辈末,將耗時(shí)的I/O請(qǐng)求委托給操作系統(tǒng)。這種情況下人們從辦證的任務(wù)中解放出來(lái)映皆,因而能繼續(xù)做其他事情挤聘。若來(lái)了一個(gè)任務(wù),交給第三方去處理捅彻,則第三方就有一個(gè)接單隊(duì)列檬洞,只需要拿到所有的單,去辦理地點(diǎn)逐個(gè)辦理即可沟饥。
操作系統(tǒng)中一個(gè)杰出的設(shè)計(jì)是線程添怔,操作系統(tǒng)把CPU處理時(shí)間分片后劃分出許多短暫的時(shí)間片湾戳,在時(shí)間T1執(zhí)行一個(gè)線程的指令,到時(shí)間T2再執(zhí)行下一個(gè)線程的指令广料,各個(gè)線程輪流執(zhí)行砾脑,結(jié)果好像是所有線程都在并行前進(jìn)。這樣艾杏,編程時(shí)可以創(chuàng)建多個(gè)線程韧衣,在同一期間執(zhí)行,各個(gè)線程可以并行的完成不同的任務(wù)购桑。
在單線程中畅铭,計(jì)算機(jī)是一臺(tái)嚴(yán)格意義上的馮諾依曼式機(jī)器,一段代碼調(diào)用了另一段代碼時(shí)勃蜘,只能采用同步調(diào)用硕噩。簡(jiǎn)單倆說(shuō),必須等待這段代碼執(zhí)行完畢并返回結(jié)果后缭贡,調(diào)用方才能繼續(xù)向下執(zhí)行炉擅。
有了多線程的支持后,可以采用異步調(diào)用阳惹,也就是說(shuō)谍失,調(diào)用方和被調(diào)方可以屬于不同的線程,調(diào)用方啟動(dòng)被調(diào)方線程后莹汤,不等待對(duì)象返回結(jié)果就繼續(xù)執(zhí)行后續(xù)代碼快鱼。
計(jì)算中有些處理時(shí)比較耗時(shí)的,調(diào)用這種處理代碼時(shí)纲岭,調(diào)用方如果苦苦等待會(huì)嚴(yán)重影響程序的性能攒巍。
異步調(diào)用雖然原理并不復(fù)雜,但在使用中容易出現(xiàn)莫名其妙的問(wèn)題荒勇,特別是不同線程共享代碼或貢獻(xiàn)數(shù)據(jù)時(shí)容易出現(xiàn)問(wèn)題。因此闻坚,要設(shè)計(jì)一個(gè)安全高效的的編程方式需要比較多的設(shè)計(jì)經(jīng)驗(yàn)沽翔,因此最好不要濫用異步。
JavaScript的異步處理上窿凤,ES5的回調(diào)函數(shù)callback
使我們陷入地獄仅偎,ES6的承諾promise
使我們脫離魔障,ES7的異步等待async-await
終于帶領(lǐng)我們走向了光明雳殊。其實(shí)async-await
是promise
和generator
的語(yǔ)法糖橘沥,是為了編碼時(shí)更加流暢,同時(shí)增強(qiáng)代碼的可讀性夯秃。async
用來(lái)表示函數(shù)是異步的座咆,使用async
定義的函數(shù)會(huì)返回一個(gè)promise
對(duì)象痢艺,因此可使用then
方法添加回調(diào)函數(shù)。await
可以理解為async wait
的縮寫(xiě)介陶,await
必須出現(xiàn)在async
函數(shù)內(nèi)部堤舒,因此它是不能夠單獨(dú)使用的。await
后面可以跟任何JS表達(dá)式哺呜,作用是用來(lái)等待promise
對(duì)象的狀態(tài)被resolved
舌缤。如果await
異步等待的是promise
對(duì)象則會(huì)造成異步函數(shù)停止執(zhí)行并且等待promise
的解決,如果等待的是正常的表達(dá)式則會(huì)立即執(zhí)行某残。
非阻塞I/O
什么是阻塞(block)呢国撵?線程在執(zhí)行中若遇到磁盤(pán)讀寫(xiě)或網(wǎng)絡(luò)通信(統(tǒng)稱為I/O操作),通常要耗費(fèi)較長(zhǎng)的時(shí)間玻墅,此時(shí)操作系統(tǒng)會(huì)剝奪這個(gè)線程的CPU控制權(quán)介牙,使其暫停執(zhí)行,同時(shí)將資源讓渡給其他工作線程椭豫,這種線程調(diào)度的方式稱為阻塞耻瑟。
- 傳統(tǒng)同步式I/O(Synchronous I/O)或阻塞式I/O(Blocking I/O)
- 異步式I/O(Asynchronous I/O)或非阻塞式I/O(Non-blocking I/O)
當(dāng)I/O操作完畢后,操作系統(tǒng)將這個(gè)線程的阻塞狀態(tài)解除赏酥,恢復(fù)其對(duì)CPU的控制權(quán)喳整,令其繼續(xù)執(zhí)行。這種I/O模式即傳統(tǒng)的同步式I/O(Synchronous I/O)或阻塞式I/O(Blocking I/O)裸扶。
異步式I/O(Asynchronous I/O)或非阻塞式I/O(Non-blocking I/O)則針對(duì)所有I/O操作不采用阻塞的策略框都。當(dāng)線程遇到I/O操作時(shí),不會(huì)阻塞的方式等待I/O操作的完成或數(shù)據(jù)的返回呵晨,而只是將I/O請(qǐng)求發(fā)送給操作系統(tǒng)魏保,繼續(xù)執(zhí)行下一條語(yǔ)句。當(dāng)操作系統(tǒng)完成I/O操作時(shí)摸屠,以事件的形式通知執(zhí)行I/O操作的線程谓罗,線程會(huì)在特定時(shí)候處理這個(gè)事件。為了處理異步I/O季二,線程必須有事件循環(huán)檩咱,不斷地檢查有沒(méi)有未處理的事件,依次予以處理胯舷。
阻塞模式下刻蚯,一個(gè)線程只能處理一項(xiàng)任務(wù),要想提高吞吐量必須通過(guò)多線程桑嘶。而非阻塞模式下炊汹,一個(gè)線程永遠(yuǎn)在執(zhí)行計(jì)算操作,這個(gè)線程所使用的CPU核心利用率永遠(yuǎn)是100%逃顶,I/O以事件的方式通知讨便。
為什么Node.js使用單線程充甚、非阻塞的事件編程模型?
在阻塞模式下器钟,多線程往往能提高系統(tǒng)吞吐量津坑,因?yàn)橐粋€(gè)線程阻塞時(shí)還有其他線程在工作,多線程可讓CPU資源不被阻塞中的線程浪費(fèi)傲霸。而在非阻塞模式下疆瑰,線程不會(huì)被I/O阻塞,永遠(yuǎn)在利用CPU昙啄。多線程帶來(lái)的好處僅僅是在多核CPU的情況下利用更多的核穆役,而Node.js的單線程也能帶來(lái)同樣的好處。這就是為什么Node.js使用單線程梳凛、非阻塞的事件編程模型耿币。
單線程事件驅(qū)動(dòng)的異步式I/O比傳統(tǒng)的多線程阻塞式I/O好在哪里呢?
簡(jiǎn)而言之韧拒,異步式I/O就是少了多線程的開(kāi)銷(xiāo)淹接。對(duì)操作系統(tǒng)來(lái)說(shuō),創(chuàng)建一個(gè)線程的代價(jià)是十分昂貴的叛溢,需要給它分配內(nèi)存塑悼、列入調(diào)度,同時(shí)在線程切換時(shí)還需執(zhí)行內(nèi)存換頁(yè)楷掉,CPU的緩存被清空厢蒜,切換回來(lái)時(shí)還要重新從內(nèi)存中讀取數(shù)據(jù),破壞了數(shù)據(jù)的局部性烹植。
異步I/O
關(guān)于異步I/O典型的場(chǎng)景是AJAX調(diào)用斑鸦,其收到響應(yīng)在是發(fā)送AJAX結(jié)束之后輸出的。在調(diào)用AJAX后草雕,后續(xù)代碼時(shí)被立即執(zhí)行的巷屿,而收到響應(yīng)的執(zhí)行時(shí)間是不被預(yù)期的。我們只知道將在這個(gè)異步請(qǐng)求結(jié)束后執(zhí)行墩虹,但并不知道具體的時(shí)間點(diǎn)嘱巾。異步調(diào)用中對(duì)于結(jié)果值的捕獲是符合“Don't call me, I will call you.”的原則的败晴,這也是注重結(jié)果不關(guān)心過(guò)程的一種表現(xiàn)。
$.post(url, data, function(res){
console.log('收到響應(yīng)');
});
console.log('發(fā)送AJAX結(jié)束');
Node中異步I/O非常常見(jiàn)栽渴,以讀取文件為例尖坤。
var fs = require('fs');
fs.readFile(path, function(err,res){
console.log('文件讀取完畢');
});
console.log('發(fā)起文件讀取');
Node.js為什么使用單線程?
Node.js保持了JS在瀏覽器中單線程的特點(diǎn)闲擦,在Node中JS與其余線程是無(wú)法共享任何狀態(tài)的慢味。單線程最大好處是不用像多線程編程那樣處處在意狀態(tài)的同步問(wèn)題场梆,這里沒(méi)有死鎖的存在,也沒(méi)有線程上下文交換帶來(lái)的性能上的開(kāi)銷(xiāo)纯路。
同樣或油,單線程也有自身的弱點(diǎn),具體表現(xiàn)在
- 無(wú)法利用多核CPU
- 錯(cuò)誤會(huì)引起整個(gè)應(yīng)用退出驰唬,應(yīng)用的健壯性值得考驗(yàn)顶岸。
- 大量計(jì)算占用CPU導(dǎo)致無(wú)法繼續(xù)調(diào)用異步I/O
像瀏覽器中的JS與UI公用一個(gè)線程一樣,JS長(zhǎng)時(shí)間執(zhí)行會(huì)導(dǎo)致UI的渲染和響應(yīng)被中斷叫编。在Node中辖佣,長(zhǎng)時(shí)間的CPU占用也會(huì)導(dǎo)致后續(xù)的異步I/O發(fā)不出調(diào)用,已完成的異步I/O的回調(diào)函數(shù)也會(huì)得不到及時(shí)執(zhí)行搓逾。
最早解決這種大計(jì)算問(wèn)題的方案是Google公司開(kāi)發(fā)的Gears卷谈,它啟用了一個(gè)完全能獨(dú)立的進(jìn)程,將需要計(jì)算的程序發(fā)送給這個(gè)進(jìn)程霞篡,在結(jié)果得出后世蔗,通過(guò)事件將結(jié)果傳遞回來(lái)。這個(gè)模型將計(jì)算分發(fā)到其他進(jìn)程上朗兵,以次來(lái)降低運(yùn)算造成阻塞的幾率污淋。
后臺(tái)H5制定了Web Workers的標(biāo)準(zhǔn),Google放棄了Gears矛市,全力支持Web Workers芙沥。Web Workers能夠創(chuàng)建工作線程來(lái)進(jìn)行計(jì)算,以解決JS大計(jì)算阻塞UI渲染的問(wèn)題浊吏。工作線程為了不阻塞主線程而昨,通過(guò)消息傳遞的方式來(lái)傳遞運(yùn)行結(jié)果,這也使得工作進(jìn)程不能訪問(wèn)主線程中的UI找田。
Node采用了與Web Workers相同的思路來(lái)解決單線程中大量計(jì)算的問(wèn)題(child_process)歌憨。子進(jìn)程的出現(xiàn),意味著Node可從容地應(yīng)對(duì)單線程在健壯性和無(wú)法利用多核CPU方面的問(wèn)題墩衙。通過(guò)將計(jì)算分發(fā)到各個(gè)子進(jìn)程务嫡,可將大量計(jì)算分解掉,然后在通過(guò)進(jìn)程之間的事件消息來(lái)傳遞結(jié)果漆改,這可以很好地保持應(yīng)用模型的簡(jiǎn)單和低依賴心铃。通過(guò)Master-Worker的管理方式,也可很好地管理各個(gè)工作進(jìn)程挫剑,以達(dá)到更高的健壯性去扣。
關(guān)于如何通過(guò)子進(jìn)程來(lái)充分利用硬件資源和提升應(yīng)用的健壯性,這是一個(gè)值得探究的話題樊破。
事件循環(huán)
Node.js所有的異步I/O操作在完成時(shí)都會(huì)發(fā)送一個(gè)事件到事件隊(duì)列愉棱。從開(kāi)發(fā)看來(lái)事件由EventEmitter對(duì)象提供唆铐。
Node.js在什么時(shí)候會(huì)進(jìn)入事件循環(huán)呢?Node.js程序由事件循環(huán)開(kāi)始到事件循環(huán)結(jié)束奔滑,所有的邏輯都是事件的回調(diào)函數(shù)艾岂,所以Node.js始終在事件循環(huán)中,程序入口就是事件循環(huán)第一個(gè)事件的回調(diào)函數(shù)朋其。事件的回調(diào)函數(shù)在執(zhí)行過(guò)程中王浴,可能會(huì)發(fā)出I/O請(qǐng)求或直接發(fā)射(emit)事件,執(zhí)行完畢后再返回事件循環(huán)令宿,事件循環(huán)會(huì)檢查事件隊(duì)列中有沒(méi)有未處理的事件叼耙,直至程序結(jié)束。
Node.js沒(méi)有顯式的事件循環(huán)粒没,它對(duì)開(kāi)發(fā)者不可見(jiàn)筛婉,由libev庫(kù)實(shí)現(xiàn)。libev支持多種類(lèi)型的事件癞松,如ev_io爽撒、ev_timer、ev_signal响蓉、ev_idle等硕勿,在Node.js中均被EventEmitter封裝。libev事件循環(huán)的每一次迭代枫甲,在Node.js中就是一次Tick源武,libev不斷檢查是否有活動(dòng)的、可供檢測(cè)的事件監(jiān)聽(tīng)器想幻,直至檢測(cè)不到時(shí)才退出事件循環(huán)粱栖,進(jìn)程結(jié)束。
異步I/O與事件驅(qū)動(dòng)
Node.js采用異步式I/O與事件驅(qū)動(dòng)的設(shè)計(jì)脏毯,對(duì)于高并發(fā)的解決方案闹究,傳統(tǒng)采用多線程模型,也就是為每個(gè)業(yè)務(wù)邏輯提供一個(gè)系統(tǒng)線程食店,通過(guò)系統(tǒng)線程切換來(lái)彌補(bǔ)同步式I/O調(diào)用時(shí)的時(shí)間開(kāi)銷(xiāo)渣淤。
Node.js采用單線程模型,對(duì)于所有I/O都采用異步式的請(qǐng)求方式吉嫩,避免頻繁的上下文切換价认。Node在執(zhí)行過(guò)程中會(huì)維護(hù)一個(gè)時(shí)間隊(duì)列,程序在執(zhí)行時(shí)進(jìn)入事件循環(huán)等待下一個(gè)事件到來(lái)自娩,每個(gè)異步式I/O請(qǐng)求完成后會(huì)被推送到事件隊(duì)列用踩,等待程序進(jìn)程進(jìn)行處理。
Node.js的異步機(jī)制是基于事件的,所有磁盤(pán)I/O捶箱、網(wǎng)絡(luò)通信、數(shù)據(jù)庫(kù)查詢都以非阻塞的方式請(qǐng)求动漾,返回的結(jié)果由事件循環(huán)來(lái)處理丁屎。
Node.js進(jìn)程在同一時(shí)刻只會(huì)處理一個(gè)事件,完成后立即進(jìn)入事件循環(huán)檢查并處理后續(xù)的事件旱眯。其好處是CPU和內(nèi)存在同一時(shí)刻集中處理一件事晨川,同時(shí)盡可能讓耗時(shí)的I/O操作并行執(zhí)行。對(duì)于低速連接攻擊删豺,Node.js只是在事件隊(duì)列中增加請(qǐng)求共虑,等待操作系統(tǒng)的回應(yīng),因而不會(huì)有任何多線程開(kāi)銷(xiāo)呀页,很大程度可提高Web應(yīng)用的健壯性妈拌,防止惡意攻擊。
異步事件模式的弊端是不符合開(kāi)發(fā)者的常規(guī)線性思路蓬蝶,需要把一個(gè)完整的邏輯拆分為一個(gè)個(gè)事件尘分,增加了開(kāi)發(fā)和調(diào)試的難度。