不要在nodejs中阻塞event loop
文章目錄
- 簡(jiǎn)介
- event loop和worker pool
- event loop和worker pool中的queue
- 阻塞event loop
- event loop的時(shí)間復(fù)雜度
- Event Loop中不推薦使用的Node.js核心模塊
- partitioning 或者 offloading
- V8引擎的限制
- REDOS正則表達(dá)式DOS攻擊
- JSON DOS攻擊
- 阻塞Worker Pool
- 總結(jié)
簡(jiǎn)介
我們知道event loop是nodejs中事件處理的基礎(chǔ),event loop中主要運(yùn)行的初始化和callback事件。除了event loop之外,nodejs中還有Worker Pool用來(lái)處理一些耗時(shí)的操作,比如I/O操作。
nodejs高效運(yùn)行的秘訣就是使用異步IO從而可以使用少量的線程來(lái)處理大量的客戶端請(qǐng)求。
而同時(shí),因?yàn)槭褂昧松倭康木€程,所以我們?cè)诰帉?xiě)nodejs程序的時(shí)候,一定要特別小心。
event loop和worker pool
在nodejs中有兩種類型的線程。第一類線程就是Event Loop也可以被稱為主線程,第二類就是一個(gè)Worker Pool中的n個(gè)Workers線程。
如果這兩種線程執(zhí)行callback花費(fèi)了太多的時(shí)間,那么我們就可以認(rèn)為這兩個(gè)線程被阻塞了。
線程阻塞第一方面會(huì)影響程序的性能,因?yàn)槟承┚€程被阻塞,就會(huì)導(dǎo)致系統(tǒng)資源的占用。因?yàn)榭偟馁Y源是有限的,這樣就會(huì)導(dǎo)致處理其他業(yè)務(wù)的資源變少,從而影響程序的總體性能。
第二方面,如果經(jīng)常會(huì)有線程阻塞的情況,很有可能被惡意攻擊者發(fā)起DOS攻擊,導(dǎo)致正常業(yè)務(wù)無(wú)法進(jìn)行。
nodejs使用的是事件驅(qū)動(dòng)的框架,Event Loop主要用來(lái)處理為各種事件注冊(cè)的callback,同時(shí)也負(fù)責(zé)處理非阻塞的異步請(qǐng)求,比如網(wǎng)絡(luò)I/O。
而由libuv實(shí)現(xiàn)的Worker Pool主要對(duì)外暴露了提交task的API,從而用來(lái)處理一些比較昂貴的task任務(wù)。這些任務(wù)包括CPU密集性操作和一些阻塞型IO操作。
而nodejs本身就有很多模塊使用的是Worker Pool。
比如IO密集型操作:
DNS模塊中的dns.lookup(), dns.lookupService()。
和除了fs.FSWatcher()和 顯式同步的文件系統(tǒng)的API之外,其他多有的File system模塊都是使用的Worker Pool。
CPU密集型操作:
Crypto模塊:crypto.pbkdf2(), crypto.scrypt(), crypto.randomBytes(), crypto.randomFill(), crypto.generateKeyPair()。
Zlib模塊:除了顯示同步的API之外,其他的API都是用的是worker pool。
一般來(lái)說(shuō)使用Worker Pool的模塊就是這些了,除此之外,你還可以使用nodejs的C++ add-on來(lái)自行提交任務(wù)到Worker Pool。
event loop和worker pool中的queue
在之前的文件中,我們講到了event loop中使用queue來(lái)存儲(chǔ)event的callback,實(shí)際上這種描述是不準(zhǔn)確的。
event loop實(shí)際上維護(hù)的是一個(gè)文件描述符集合。這些文件描述符使用的是操作系統(tǒng)內(nèi)核的 epoll (Linux), kqueue (OSX), event ports (Solaris), 或者 IOCP (Windows)來(lái)對(duì)事件進(jìn)行監(jiān)聽(tīng)。
當(dāng)操作系統(tǒng)檢測(cè)到事件準(zhǔn)備好之后,event loop就會(huì)調(diào)用event所綁定的callback事件,最終執(zhí)行callback。
相反的,worker pool就真的是保存了要執(zhí)行的任務(wù)隊(duì)列,這些任務(wù)隊(duì)列中的任務(wù)由各個(gè)worker來(lái)執(zhí)行。當(dāng)執(zhí)行完畢之后,Woker將會(huì)通知Event Loop該任務(wù)已經(jīng)執(zhí)行完畢。
阻塞event loop
因?yàn)閚odejs中的線程有限,如果某個(gè)線程被阻塞,就可能會(huì)影響到整個(gè)應(yīng)用程序的執(zhí)行,所以我們?cè)诔绦蛟O(shè)計(jì)的過(guò)程中,一定要小心的考慮event loop和worker pool,避免阻塞他們。
event loop主要關(guān)注的是用戶的連接和響應(yīng)用戶的請(qǐng)求,如果event loop被阻塞,那么用戶的請(qǐng)求將會(huì)得不到及時(shí)響應(yīng)。
因?yàn)閑vent loop主要執(zhí)行的是callback,所以,我們的callback執(zhí)行時(shí)間一定要短。
event loop的時(shí)間復(fù)雜度
時(shí)間復(fù)雜度一般用在判斷一個(gè)算法的運(yùn)行速度上,這里我們也可以借助時(shí)間復(fù)雜度這個(gè)概念來(lái)分析一下event loop中的callback。
如果所有的callback中的時(shí)間復(fù)雜度都是一個(gè)常量的話,那么我們可以保證所有的callback都可以很公平的被執(zhí)行。
但是如果有些callback的時(shí)間復(fù)雜度是變化的,那么就需要我們仔細(xì)考慮了。
app.get('/constant-time', (req, res) => {res.sendStatus(200); });先看一個(gè)常量時(shí)間復(fù)雜度的情況,上面的例子中我們直接設(shè)置了respose的status,是一個(gè)常量時(shí)間操作。
app.get('/countToN', (req, res) => {let n = req.query.n;// n iterations before giving someone else a turnfor (let i = 0; i < n; i++) {console.log(`Iter ${i}`);}res.sendStatus(200); });上面的例子是一個(gè)O(n)的時(shí)間復(fù)雜度,根據(jù)request中傳入的n的不同,我們可以得到不同的執(zhí)行時(shí)間。
app.get('/countToN2', (req, res) => {let n = req.query.n;// n^2 iterations before giving someone else a turnfor (let i = 0; i < n; i++) {for (let j = 0; j < n; j++) {console.log(`Iter ${i}.${j}`);}}res.sendStatus(200); });上面的例子是一個(gè)O(n^2)的時(shí)間復(fù)雜度。
這種情況應(yīng)該怎么處理呢?首先我們需要估算出系統(tǒng)能夠承受的響應(yīng)極限值,并且設(shè)定用戶傳入的參數(shù)極限值,如果用戶傳入的數(shù)據(jù)太長(zhǎng),超出了我們的處理范圍,則可以直接從用戶輸入端進(jìn)行限制,從而保證我們的程序的正常運(yùn)行。
Event Loop中不推薦使用的Node.js核心模塊
在nodejs中的核心模塊中,有一些方法是同步的阻塞API,使用起來(lái)開(kāi)銷(xiāo)比較大,比如壓縮,加密,同步IO,子進(jìn)程等等。
這些API的目的是供我們?cè)赗EPL環(huán)境中使用的,我們不應(yīng)該直接在服務(wù)器端程序中使用他們。
有哪些不推薦在server端使用的API呢?
-
Encryption:
crypto.randomBytes (同步版本)
crypto.randomFillSync
crypto.pbkdf2Sync -
Compression:
zlib.inflateSync
zlib.deflateSync -
File system:
不要使用fs的同步API -
Child process:
child_process.spawnSync
child_process.execSync
child_process.execFileSync
partitioning 或者 offloading
為了不阻塞event loop,同時(shí)給其他event一些運(yùn)行機(jī)會(huì),我們實(shí)際上有兩種解決辦法,那就是partitioning和offloading。
partitioning就是分而治之,把一個(gè)長(zhǎng)的任務(wù),分成幾塊,每次執(zhí)行一塊,同時(shí)給其他的event一些運(yùn)行時(shí)間,從而不再阻塞event loop。
舉個(gè)例子:
for (let i = 0; i < n; i++)sum += i; let avg = sum / n; console.log('avg: ' + avg);比如我們要計(jì)算n個(gè)數(shù)的平均數(shù)。上面的例子中我們的時(shí)間復(fù)雜度是O(n)。
function asyncAvg(n, avgCB) {// Save ongoing sum in JS closure.var sum = 0;function help(i, cb) {sum += i;if (i == n) {cb(sum);return;}// "Asynchronous recursion".// Schedule next operation asynchronously.setImmediate(help.bind(null, i+1, cb));}// Start the helper, with CB to call avgCB.help(1, function(sum){var avg = sum/n;avgCB(avg);}); }asyncAvg(n, function(avg){console.log('avg of 1-n: ' + avg); });這里我們用到了setImmediate,將sum的任務(wù)分解成一步一步的。雖然asyncAvg需要執(zhí)行很多次,但是每一次的event loop都可以保證不被阻塞。
partitioning雖然邏輯簡(jiǎn)單,但是對(duì)于一些大型的計(jì)算任務(wù)來(lái)說(shuō),并不合適。并且partitioning本身還是運(yùn)行在event loop中的,它并沒(méi)有享受到多核系統(tǒng)帶來(lái)的優(yōu)勢(shì)。
這個(gè)時(shí)候我們就需要將任務(wù)offloading到worker Pool中。
使用Worker Pool有兩種方式,第一種就是使用nodejs自帶的Worker Pool,我們可以自行開(kāi)發(fā)C++ addon或者node-webworker-threads。
第二種方式就是自行創(chuàng)建Worker Pool,我們可以使用Child Process 或者 Cluster來(lái)實(shí)現(xiàn)。
當(dāng)然offloading也有缺點(diǎn),它的最大缺點(diǎn)就是和Event Loop的交互損失。
V8引擎的限制
nodejs是運(yùn)行在V8引擎上的,通常來(lái)說(shuō)V8引擎已經(jīng)足夠優(yōu)秀足夠快了,但是還是存在兩個(gè)例外,那就是正則表達(dá)式和JSON操作。
REDOS正則表達(dá)式DOS攻擊
正則表達(dá)式有什么問(wèn)題呢?正則表達(dá)式有一個(gè)悲觀回溯的問(wèn)題。
什么是悲觀回溯呢?
我們舉個(gè)例子,假如大家對(duì)正則表達(dá)式已經(jīng)很熟悉了。
假如我們使用/^(x*)y$/ 來(lái)和字符串xxxxxxy來(lái)進(jìn)行匹配。
匹配之后第一個(gè)分組(也就是括號(hào)里面的匹配值)是xxxxxx。
如果我們把正則表達(dá)式改寫(xiě)為 /^(x*)xy$/ 再來(lái)和字符串xxxxxxy來(lái)進(jìn)行匹配。 匹配的結(jié)果就是xxxxx。
這個(gè)過(guò)程是怎么樣的呢?
首先(x*)會(huì)盡可能的匹配更多的x,知道遇到字符y。 這時(shí)候(x*)已經(jīng)匹配了6個(gè)x。
接著正則表達(dá)式繼續(xù)執(zhí)行(x*)之后的xy,發(fā)現(xiàn)不能匹配,這時(shí)候(x*)需要從已經(jīng)匹配的6個(gè)x中,吐出一個(gè)x,然后重新執(zhí)行正則表達(dá)式中的xy,發(fā)現(xiàn)能夠匹配,正則表達(dá)式結(jié)束。
這個(gè)過(guò)程就是一個(gè)回溯的過(guò)程。
如果正則表達(dá)式寫(xiě)的不好,那么就有可能會(huì)出現(xiàn)悲觀回溯。
還是上面的例子,但是這次我們用/^(x*)y$/ 來(lái)和字符串xxxxxx來(lái)進(jìn)行匹配。
按照上面的流程,我們知道正則表達(dá)式需要進(jìn)行6次回溯,最后匹配失敗。
考慮一些極端的情況,可能會(huì)導(dǎo)致回溯一個(gè)非常大的次數(shù),從而導(dǎo)致CPU占用率飆升。
我們稱正則表達(dá)式的DOS攻擊為REDOS。
舉個(gè)nodejs中REDOS的例子:
app.get('/redos-me', (req, res) => {let filePath = req.query.filePath;// REDOSif (filePath.match(/(\/.+)+$/)) {console.log('valid path');}else {console.log('invalid path');}res.sendStatus(200); });上面的callback中,我們本意是想匹配 /a/b/c這樣的路徑。但是如果用戶輸入filePath=///…/\n,假如有100個(gè)/,最后跟著換行符。
那么將會(huì)導(dǎo)致正則表達(dá)式的悲觀回溯。因?yàn)?表示的是匹配除換行符 \n 之外的任何單字符。但是我們只到最后才發(fā)現(xiàn)不能夠匹配,所以產(chǎn)生了REDOS攻擊。
如何避免REDOS攻擊呢?
一方面有一些現(xiàn)成的正則表達(dá)式模塊,我們可以直接使用,比如safe-regex,rxxr2和node-re2等。
一方面可以到www.regexlib.com網(wǎng)站上查找要使用的正則表達(dá)式規(guī)則,這些規(guī)則是經(jīng)過(guò)驗(yàn)證的,可以減少自己編寫(xiě)正則表達(dá)式的失誤。
JSON DOS攻擊
通常我們會(huì)使用JSON.parse 和 JSON.stringify 這兩個(gè)JSON常用的操作,但是這兩個(gè)操作的時(shí)間是和輸入的JSON長(zhǎng)度相關(guān)的。
舉個(gè)例子:
var obj = { a: 1 }; var niter = 20;var before, str, pos, res, took;for (var i = 0; i < niter; i++) {obj = { obj1: obj, obj2: obj }; // Doubles in size each iter }before = process.hrtime(); str = JSON.stringify(obj); took = process.hrtime(before); console.log('JSON.stringify took ' + took);before = process.hrtime(); pos = str.indexOf('nomatch'); took = process.hrtime(before); console.log('Pure indexof took ' + took);before = process.hrtime(); res = JSON.parse(str); took = process.hrtime(before); console.log('JSON.parse took ' + took);上面的例子中我們對(duì)obj進(jìn)行解析操作,當(dāng)然這個(gè)obj比較簡(jiǎn)單,如果用戶傳入了一個(gè)超大的json文件,那么就會(huì)導(dǎo)致event loop的阻塞。
解決辦法就是限制用戶的輸入長(zhǎng)度。或者使用異步的JSON API:比如JSONStream和Big-Friendly JSON。
阻塞Worker Pool
nodejs的理念就是用最小的線程來(lái)處理最大的客戶連接。上面我們也講過(guò)了要把復(fù)雜的操作放到Worker Pool中來(lái)借助線程池的優(yōu)勢(shì)來(lái)運(yùn)行。
但是線程池中的線程個(gè)數(shù)也是有限的。如果某一個(gè)線程執(zhí)行了一個(gè)long run task,那么就等于線程池中少了一個(gè)worker線程。
惡意攻擊者實(shí)際上是可以抓住系統(tǒng)的這個(gè)弱點(diǎn),來(lái)實(shí)施DOS攻擊。
所以對(duì)Worker Pool中l(wèi)ong run task的最優(yōu)解決辦法就是partitioning。從而讓所有的任務(wù)都有平等的執(zhí)行機(jī)會(huì)。
當(dāng)然,如果你可以很清楚的區(qū)分short task和long run task,那么我們實(shí)際上可以分別構(gòu)造不同的worker Pool來(lái)分別為不同的task任務(wù)類型服務(wù)。
總結(jié)
event loop和worker pool是nodejs中兩種不同的事件處理機(jī)制,我們需要在程序中根據(jù)實(shí)際問(wèn)題來(lái)選用。
本文作者:flydean程序那些事
本文鏈接:http://www.flydean.com/nodejs-block-eventloop/
本文來(lái)源:flydean的博客
歡迎關(guān)注我的公眾號(hào):「程序那些事」最通俗的解讀,最深刻的干貨,最簡(jiǎn)潔的教程,眾多你不知道的小技巧等你來(lái)發(fā)現(xiàn)!
總結(jié)
以上是生活随笔為你收集整理的不要在nodejs中阻塞event loop的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 在nodejs中创建cluster
- 下一篇: 深入理解nodejs的HTTP处理流程