咱們知道event loop是nodejs中事件處理的基礎,event loop中主要運行的初始化和callback事件。除了event loop以外,nodejs中還有Worker Pool用來處理一些耗時的操做,好比I/O操做。node
nodejs高效運行的祕訣就是使用異步IO從而可使用少許的線程來處理大量的客戶端請求。web
而同時,由於使用了少許的線程,因此咱們在編寫nodejs程序的時候,必定要特別當心。正則表達式
在nodejs中有兩種類型的線程。第一類線程就是Event Loop也能夠被稱爲主線程,第二類就是一個Worker Pool中的n個Workers線程。算法
若是這兩種線程執行callback花費了太多的時間,那麼咱們就能夠認爲這兩個線程被阻塞了。json
線程阻塞第一方面會影響程序的性能,由於某些線程被阻塞,就會致使系統資源的佔用。由於總的資源是有限的,這樣就會致使處理其餘業務的資源變少,從而影響程序的整體性能。服務器
第二方面,若是常常會有線程阻塞的狀況,頗有可能被惡意攻擊者發起DOS攻擊,致使正常業務沒法進行。網絡
nodejs使用的是事件驅動的框架,Event Loop主要用來處理爲各類事件註冊的callback,同時也負責處理非阻塞的異步請求,好比網絡I/O。app
而由libuv實現的Worker Pool主要對外暴露了提交task的API,從而用來處理一些比較昂貴的task任務。這些任務包括CPU密集性操做和一些阻塞型IO操做。框架
而nodejs自己就有不少模塊使用的是Worker Pool。dom
好比IO密集型操做:
DNS模塊中的dns.lookup(), dns.lookupService()。
和除了fs.FSWatcher()和 顯式同步的文件系統的API以外,其餘多有的File system模塊都是使用的Worker Pool。
CPU密集型操做:
Crypto模塊:crypto.pbkdf2(), crypto.scrypt(), crypto.randomBytes(), crypto.randomFill(), crypto.generateKeyPair()。
Zlib模塊:除了顯示同步的API以外,其餘的API都是用的是worker pool。
通常來講使用Worker Pool的模塊就是這些了,除此以外,你還可使用nodejs的C++ add-on來自行提交任務到Worker Pool。
在以前的文件中,咱們講到了event loop中使用queue來存儲event的callback,實際上這種描述是不許確的。
event loop實際上維護的是一個文件描述符集合。這些文件描述符使用的是操做系統內核的 epoll (Linux), kqueue (OSX), event ports (Solaris), 或者 IOCP (Windows)來對事件進行監聽。
當操做系統檢測到事件準備好以後,event loop就會調用event所綁定的callback事件,最終執行callback。
相反的,worker pool就真的是保存了要執行的任務隊列,這些任務隊列中的任務由各個worker來執行。當執行完畢以後,Woker將會通知Event Loop該任務已經執行完畢。
由於nodejs中的線程有限,若是某個線程被阻塞,就可能會影響到整個應用程序的執行,因此咱們在程序設計的過程當中,必定要當心的考慮event loop和worker pool,避免阻塞他們。
event loop主要關注的是用戶的鏈接和響應用戶的請求,若是event loop被阻塞,那麼用戶的請求將會得不到及時響應。
由於event loop主要執行的是callback,因此,咱們的callback執行時間必定要短。
時間複雜度通常用在判斷一個算法的運行速度上,這裏咱們也能夠藉助時間複雜度這個概念來分析一下event loop中的callback。
若是全部的callback中的時間複雜度都是一個常量的話,那麼咱們能夠保證全部的callback均可以很公平的被執行。
可是若是有些callback的時間複雜度是變化的,那麼就須要咱們仔細考慮了。
app.get('/constant-time', (req, res) => { res.sendStatus(200); });
先看一個常量時間複雜度的狀況,上面的例子中咱們直接設置了respose的status,是一個常量時間操做。
app.get('/countToN', (req, res) => { let n = req.query.n; // n iterations before giving someone else a turn for (let i = 0; i < n; i++) { console.log(`Iter ${i}`); } res.sendStatus(200); });
上面的例子是一個O(n)的時間複雜度,根據request中傳入的n的不一樣,咱們能夠獲得不一樣的執行時間。
app.get('/countToN2', (req, res) => { let n = req.query.n; // n^2 iterations before giving someone else a turn for (let i = 0; i < n; i++) { for (let j = 0; j < n; j++) { console.log(`Iter ${i}.${j}`); } } res.sendStatus(200); });
上面的例子是一個O(n^2)的時間複雜度。
這種狀況應該怎麼處理呢?首先咱們須要估算出系統可以承受的響應極限值,而且設定用戶傳入的參數極限值,若是用戶傳入的數據太長,超出了咱們的處理範圍,則能夠直接從用戶輸入端進行限制,從而保證咱們的程序的正常運行。
在nodejs中的核心模塊中,有一些方法是同步的阻塞API,使用起來開銷比較大,好比壓縮,加密,同步IO,子進程等等。
這些API的目的是供咱們在REPL環境中使用的,咱們不該該直接在服務器端程序中使用他們。
有哪些不推薦在server端使用的API呢?
crypto.randomBytes (同步版本)
crypto.randomFillSync
crypto.pbkdf2Sync
zlib.inflateSync
zlib.deflateSync
不要使用fs的同步API
child_process.spawnSync
child_process.execSync
child_process.execFileSync
爲了避免阻塞event loop,同時給其餘event一些運行機會,咱們實際上有兩種解決辦法,那就是partitioning和offloading。
partitioning就是分而治之,把一個長的任務,分紅幾塊,每次執行一塊,同時給其餘的event一些運行時間,從而再也不阻塞event loop。
舉個例子:
for (let i = 0; i < n; i++) sum += i; let avg = sum / n; console.log('avg: ' + avg);
好比咱們要計算n個數的平均數。上面的例子中咱們的時間複雜度是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的任務分解成一步一步的。雖然asyncAvg須要執行不少次,可是每一次的event loop均可以保證不被阻塞。
partitioning雖然邏輯簡單,可是對於一些大型的計算任務來講,並不合適。而且partitioning自己仍是運行在event loop中的,它並無享受到多核系統帶來的優點。
這個時候咱們就須要將任務offloading到worker Pool中。
使用Worker Pool有兩種方式,第一種就是使用nodejs自帶的Worker Pool,咱們能夠自行開發C++ addon或者node-webworker-threads。
第二種方式就是自行建立Worker Pool,咱們可使用Child Process 或者 Cluster來實現。
固然offloading也有缺點,它的最大缺點就是和Event Loop的交互損失。
nodejs是運行在V8引擎上的,一般來講V8引擎已經足夠優秀足夠快了,可是仍是存在兩個例外,那就是正則表達式和JSON操做。
正則表達式有什麼問題呢?正則表達式有一個悲觀回溯的問題。
什麼是悲觀回溯呢?
咱們舉個例子,假如你們對正則表達式已經很熟悉了。
假如咱們使用/^(x*)y$/ 來和字符串xxxxxxy來進行匹配。
匹配以後第一個分組(也就是括號裏面的匹配值)是xxxxxx。
若是咱們把正則表達式改寫爲 /^(x*)xy$/ 再來和字符串xxxxxxy來進行匹配。 匹配的結果就是xxxxx。
這個過程是怎麼樣的呢?
首先(x)會盡量的匹配更多的x,知道遇到字符y。 這時候(x)已經匹配了6個x。
接着正則表達式繼續執行(x)以後的xy,發現不能匹配,這時候(x)須要從已經匹配的6個x中,吐出一個x,而後從新執行正則表達式中的xy,發現可以匹配,正則表達式結束。
這個過程就是一個回溯的過程。
若是正則表達式寫的很差,那麼就有可能會出現悲觀回溯。
仍是上面的例子,可是此次咱們用/^(x*)y$/ 來和字符串xxxxxx來進行匹配。
按照上面的流程,咱們知道正則表達式須要進行6次回溯,最後匹配失敗。
考慮一些極端的狀況,可能會致使回溯一個很是大的次數,從而致使CPU佔用率飆升。
咱們稱正則表達式的DOS攻擊爲REDOS。
舉個nodejs中REDOS的例子:
app.get('/redos-me', (req, res) => { let filePath = req.query.filePath; // REDOS if (filePath.match(/(\/.+)+$/)) { console.log('valid path'); } else { console.log('invalid path'); } res.sendStatus(200); });
上面的callback中,咱們本意是想匹配 /a/b/c這樣的路徑。可是若是用戶輸入filePath=///.../n,假若有100個/,最後跟着換行符。
那麼將會致使正則表達式的悲觀回溯。由於.
表示的是匹配除換行符 n 以外的任何單字符。可是咱們只到最後才發現不可以匹配,因此產生了REDOS攻擊。
如何避免REDOS攻擊呢?
一方面有一些現成的正則表達式模塊,咱們能夠直接使用,好比safe-regex,rxxr2和node-re2等。
一方面能夠到www.regexlib.com網站上查找要使用的正則表達式規則,這些規則是通過驗證的,能夠減小本身編寫正則表達式的失誤。
一般咱們會使用JSON.parse 和 JSON.stringify 這兩個JSON經常使用的操做,可是這兩個操做的時間是和輸入的JSON長度相關的。
舉個例子:
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);
上面的例子中咱們對obj進行解析操做,固然這個obj比較簡單,若是用戶傳入了一個超大的json文件,那麼就會致使event loop的阻塞。
解決辦法就是限制用戶的輸入長度。或者使用異步的JSON API:好比JSONStream和Big-Friendly JSON。
nodejs的理念就是用最小的線程來處理最大的客戶鏈接。上面咱們也講過了要把複雜的操做放到Worker Pool中來藉助線程池的優點來運行。
可是線程池中的線程個數也是有限的。若是某一個線程執行了一個long run task,那麼就等於線程池中少了一個worker線程。
惡意攻擊者其實是能夠抓住系統的這個弱點,來實施DOS攻擊。
因此對Worker Pool中long run task的最優解決辦法就是partitioning。從而讓全部的任務都有平等的執行機會。
固然,若是你能夠很清楚的區分short task和long run task,那麼咱們實際上能夠分別構造不一樣的worker Pool來分別爲不一樣的task任務類型服務。
event loop和worker pool是nodejs中兩種不一樣的事件處理機制,咱們須要在程序中根據實際問題來選用。
本文做者:flydean程序那些事本文連接:http://www.flydean.com/nodejs-block-eventloop/
本文來源:flydean的博客
歡迎關注個人公衆號:「程序那些事」最通俗的解讀,最深入的乾貨,最簡潔的教程,衆多你不知道的小技巧等你來發現!