不要阻塞事件循環(或工做池)

你應該閱讀本指南嗎?

若是您編寫比命令行腳本更復雜的程序,那麼閱讀本文能夠幫助您編寫性能更高,更安全的應用程序。javascript

在編寫本文檔時,主要是基於Node服務器。但裏面的原則也適用於其它複雜的Node應用程序。在沒有特別說明操做系統的狀況下,默認爲Linux。html

TL; DR

Node.js在事件循環(初始化和回調)中運行JavaScript代碼,並提供工做池來處理成本比較高的任務,如文件I/O。 Node服務節點有很強的擴展能力,有時能提供比相對較重的Apache更好的解決方案。關鍵點就在於它使用少許線程來處理多客戶端鏈接。若是Node可使用更少的線程,那麼它能夠將更多的系統時間和內存用於客戶端,而不是爲線程(內存,上下文切換)佔用額外空間和時間。但也由於Node只有少許的線程,所以在構建應用程序時,必須明智地使用它們。java

這裏有一些保持Node服務器快速穩健運行的經驗法則: 當在任何給定時間與每一個客戶端關聯的工做「很小」時,Node服務會很快。node

這適用於事件循環上的回調和工做池上的任務。git

爲何我要避免阻塞事件循環和工做池?

Node使用少許的線程來處理多個客戶端鏈接。在Node中有兩種類型的線程:github

  • 一個事件循環(又稱主循環,主線程,事件線程等);
  • k工做池(也稱爲線程池)中的工做池

若是一個線程須要很長時間來執行回調(Event Loop)或任務(Worker),咱們稱之爲「阻塞」。雖然線程爲處理一個客戶端鏈接而阻塞,但它沒法處理來自任何其餘客戶端的請求。這提供了阻止事件循環和工做池的兩個動機:web

  1. 性能:若是常常在任一類型的線程上執行重量級活動,則服務器的吞吐量(請求/秒)將受到影響;
  2. 安全性:若是某個輸入可能會阻塞某個線程,則惡意客戶端可能會提交此「惡意輸入」,使線程阻塞,從而阻塞其它客戶端上的處理。這就很方便地的形成了 拒絕服務攻擊

快速回顧一下Node

Node使用事件驅動架構:它有一個事件循環用於調度 和 一個處理阻塞任務的工做池。正則表達式

什麼代碼在事件循環上運行?

在開始時,Node應用程序首先完成初始化階段,即require模塊和註冊事件的回調。而後,Node應用程序進入事件循環,經過執行相應的回調來響應傳入的客戶端請求。此回調同步執行,並在完成後又有可能註冊新的異步請求。這些新異步請求的回調也將在事件循環上執行。數據庫

事件循環中還包含其它一些非阻塞異步請求(例如,網絡I/O)產生的回調。express

總之,Event Loop執行這些註冊爲某些事件的JavaScript回調,而且還負責完成非阻塞異步請求,如網絡I/O.

什麼代碼在線程池(Worker Pool)中運行

Node的線程池經過libuv(docs)實現。libuv暴露出一組任務提交的API。

Node使用線程池(Worker Pool)處理比較費時的任務。例操做系統沒有提供非阻塞版本的I/O, CPU密集型任務等。

會用到線程池的Node模塊:

  • I/O密集型

    • DNS: dns.lookup(), dns.lookupService()
    • fs: 除了fs.FSWatcher()和全部明確同步調用的文件API,剩下的都會用到libuv實現的線程池
  • CPU密集型

    • Crypto: crypto.pbkdf2(), crypto.randomBytes(), crypto.randomFill()
    • Zlib: 除了明確聲明使用同步調用的API,剩下的都會用到libuv的線程池

在大多數Node應用程序中,這些API是Worker Pool的惟一任務源。實際上,使用C++插件的應用程序和模塊也能夠提交任務給工做池。

爲了完整起見,咱們注意到當從事件循環上的回調中調用上述其中一個API時,事件循環會花費一些較小的設置成本。由於須要進入該API相關的C++實現模塊並將任務提交給工做池。與任務的總成本相比,這些成本能夠忽略不計,這就是事件循環將它轉接到C++模塊的緣由。將這些任務之一提交給Worker Pool時,Node會在Node C++綁定中提供指向相應C++函數的指針。

Node如何肯定接下來要運行的代碼?

理論上,Event Loop 和 Worker Pool 分別操做待處理的事件 和 待完成的任務。

實際上,Event Loop並不真正維護隊列。相應的,它有一組文件描述符,這些文件描述符被操做系統使用epoll(Linux),kqueue(OSX),事件端口(Solaris)或IOCP(Windows)等機制進行監視。這些文件描述符對應於網絡套接字,它正在觀看的任何文件,等等。當操做系統說其中一個文件描述符準備就緒時,Event Loop會將其轉換爲相應的事件並調用與該事件關聯的回調。您能夠在此處詳細瞭解此過程。

相反,Worker Pool使用一個真正的隊列,隊列中包含要處理的任務。Worker今後隊列中出棧一個任務並對其進行處理,完成後,Worker會爲事件循環引起「至少一個任務已完成」事件。

這對應用程序設計意味着什麼?

在像Apache這樣的一個線程對應一個客戶端鏈接的系統中,每一個掛起的客戶端都被分配了本身的線程。若是處理一個客戶端的線程阻塞時,操做系統會中斷它並切換到另外一個處理客戶端請求的線程。所以操做系統確保須要少許工做的客戶不會受到須要更多工做的客戶的影響。

由於Node用不多的線程數量處理許多客戶端鏈接,若是一個線程處理一個客戶端的請求時被阻塞,那麼其它被掛起的客戶端請求會一直得不到執行機會,直到該線程完成其回調或任務。 所以,保證客戶端的鏈接都受到公平對待是你編寫程序的工做內容。 這也就是說,在Node 程序中,不該該在任何單個回調或任務中爲任何客戶端作太多比較耗時的工做。

上面說的就是Node爲何能夠很好地擴展的部分緣由,但這也意味着開發者有責任確保公平的調度。接下來的部分將討論如何確保事件循環和工做池的公平調度。

不要阻塞事件循環

事件循環通知每一個新客戶端鏈接並協調對客戶端的響應。也就是說,全部傳入請求和傳出響應都經過事件循環處理。這意味着若是事件循環在任什麼時候候花費的時間太長,全部當前的 以及新進來的客戶端鏈接都不會得到響應機會。

因此,要確保在任什麼時候候都不該該阻塞事件循環。換句話說,每個JavaScript回調應當可以快速完成。這固然也適用於你awaitPromise.then等。

確保這一點的一個好方法是推斷回調的「計算複雜度」。若是你的回調須要必定數量的步驟,不管它的參數是什麼,老是會給每一個鏈接的客戶段提供一個合理的響應。若是回調根據其參數採用不一樣的步驟數,那麼就應該考慮不一樣參數可能致使的計算複雜度。

例子1: 恆定時間的回調

app.get('/constant-time', (req, res) => {
    res.sendStatus(200);
});

例子2: 時間複雜度O(n)。回調運行時間與n成線性關係

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);
});

例子3: 時間複雜度是O(n^2)的例子。當n比較小的時候,回調執行速度沒有太大的影響,若是n比較大,相對O(n)而言,會特別的慢。並且n+1 對 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);
});

如何更當心一點?

Node使用Google V8引擎解析JavaScript,這對於許多常見操做來講很是快。可是有例外:regexp和JSON操做。

對於複雜的任務,應該考慮限制輸入長度並拒絕太長的輸入。這樣,即便回調具備很大的複雜度,經過限制輸入,也能夠確保回調執行時間不會超過最壞狀況下的執行時間。而後,能夠依據此評估​​回調的最壞狀況成本,並肯定其上下文中的運行時間是否可接受。

阻止事件循環: REDOS(Regular expression Denial of Service - ReDoS)

一種比較常見的阻塞事件循環的方式是使用比較「脆弱」的正則表達式。

正則表達式(regexp)將輸入字符串與特定的模式匹配。一般咱們認爲正則表達式只須要匹配一次輸入的字符串----時間複雜度是O(n),n是輸入字符串的長度。在許多狀況下,確實只須要一次即可完成匹配。但在某些狀況下,正則表達式可能須要對傳入的字符串進行屢次匹配----時間複雜度是O(2^n)。指數級增加意味着若是引擎須要x次回溯來肯定匹配,那麼若是咱們在輸入字符串中再添加一個字符,則至少須要2*x次回溯。因爲回溯次數與所需時間成線性關係,所以這種狀況會阻塞事件循環。

一個「脆弱」的正則表達式在你的正則匹配引擎上運行可能須要指數時間,致使你可能遭受REDOS(Regular expression Denial of Service - ReDoS)的「邪惡輸入」。可是正則表達式模式是否易受攻擊(即正則表達式引擎可能須要指數時間)其實是一個難以回答的問題,而且取決於您使用的是Perl,Python,Ruby,Java,JavaScript等。但有一些經驗法則是適用於全部語言的:

  1. 避免使用嵌套量詞(a+)*。Node的regexp引擎可能能夠快速處理其中的一些,但其餘引擎容易受到攻擊。
  2. 避免使用帶有重疊子句的OR,例如(a|a)*。一樣,這種狀況有時是快速的。
  3. 避免使用反向引用,例如(a.*) 1。沒有正則表達式引擎能夠確保在線性時間內匹配它們。
  4. 若是您正在進行簡單的字符串匹配,請使用indexOf或其它自己替代方法。它會更輕量且永遠不會超過O(n)。

若是您不肯定您的正則表達式是否容易受到攻擊,但你須要明確的是即便易受攻擊的正則表達式和長輸入字符串,Node一般沒法報告匹配項。當不匹配時, Node在嘗試匹配的輸入字符串的許多路徑以前,是沒法肯定是否會觸發指數級的時間長度。

一個REDOS(Regular expression Denial of Service - ReDoS) 例子

如下是將其服務器暴露給REDOS的示例易受攻擊的正則表達式:

app.get('/redos-me', (req, res) => {
    let filePath = req.query.filePath;

    // REDOS
    if (fileName.match(/(\/.+)+$/)) {
        console.log('valid path');
    }
    else {
        console.log('invalid path');
    }

    res.sendStatus(200);
});

這個例子中易受攻擊的正則表達式是一種(糟糕的)方法來檢查Linux上的有效路徑。它匹配以「/」做爲分隔符的字符串,如「/a/b/c」。它很危險,由於它違反了規則1:它有一個雙重嵌套的量詞。

若是客戶端使用filePath查詢///.../n(100 / s後跟換行符「。」將不匹配的換行符),那麼事件循環將永遠有效,阻塞事件循環。此客戶端的REDOS攻擊致使全部其餘客戶端在regexp匹配完成以前不會響應。

所以,您應該謹慎使用複雜的正則表達式來驗證用戶輸入。

反REDOS資源

有一些工具能夠檢查你的regexp是否安全,好比

可是,它們並不能保證識別全部易受攻擊的正則表達式。

另外一種方法是使用不一樣的正則表達式引擎。您可使用node-re2模塊,該模塊使用Google很是火熱的RE2 regexp引擎。可是要注意,RE2與Node的regexp不是100%兼容,所以若是你使用node-re2模塊來處理你的regexp,請檢查迴歸。node-re2不支持特別複雜的regexp。

若是您正在嘗試匹配一些特別常見的內容,例如URL或文件路徑,請在regexp庫中查找示例或使用npm模塊,例如ip-regex

阻塞事件循環: Node核心模塊

Node裏有一些核心模塊,包含一些比較耗時的同步API:

這些模塊中的一些API比較耗時,主要是由於須要大量的計算(encryption, compression),I/O操做(file I/O)或者二者都有(child process)。 這些API旨在方便編寫腳本,可是在服務端也許並不適用。若是在事件循環中調用這些API,將會花費更多的時間,從而致使事件循環阻塞。

在服務端程序中,注意一下同步API的使用。

  • 加密:

    • crypto.randomBytes (同步版)
    • crypto.randomFillSync
    • crypto.pbkdf2Sync
    • 您還應該注意爲加密和解密例程提供大量輸入。
  • 壓縮:

    • zlib.inflateSync
    • zlib.deflateSync
  • 文件系統

    • 不要使用同步文件系統API。例如,若是您訪問的文件位於NFS等分佈式文件系統中,則訪問時間可能會有很大差別。
  • child process(子進程)

    • child_process.spawnSync
    • child_process.execSync
    • child_process.execFileSync

從Node V9開始,這個列表已經比較完善了。

阻塞事件循環: JSON DOS

JSON.parseJSON.stringify 是另外兩種比較耗時的操做。 儘管他們的時間複雜度是O(n),可是若是n比較大的話,也會花費至關多的操做時間。

若是你的服務程序操做對象主要是JSON,特別是這些JSON來自客戶端,那麼你須要特別注意JSON對象的大小 或者 字符串的長度。

JSON 阻塞示例:咱們建立一個大小爲2 ^ 21 的obj對象,而後在字符串上JSON.stringify運行indexOf,而後運行JSON.parse。該JSON.stringify「d字符串爲50MB。字符串化對象須要0.7秒,對50MB字符串的indexOf須要0.03秒,解析字符串須要1.3秒。

var obj = { a: 1 };
var niter = 20;

var before, res, took;

for (var i = 0; i < len; i++) {
  obj = { obj1: obj, obj2: obj }; // Doubles in size each iter
}

before = process.hrtime();
res = JSON.stringify(obj);
took = process.hrtime(n);
console.log('JSON.stringify took ' + took);

before = process.hrtime();
res = str.indexOf('nomatch');
took = process.hrtime(n);
console.log('Pure indexof took ' + took);

before = process.hrtime();
res = JSON.parse(str);
took = process.hrtime(n);
console.log('JSON.parse took ' + took);

有一些npm模塊提供異步JSON API。參見例如:

  • 具備流APIJSONStream
  • Big-Friendly JSON,它具備流API以及標準JSON API的異步版本,使用下面概述的事件循環分區。

複雜計算而不阻塞事件循環

假設您想在JavaScript中執行復雜計算而不阻塞事件循環。您有兩種選擇:partitioning切割或offloading轉嫁。

partitioning切割
您能夠對計算進行分區,以便每一個計算都在事件循環上運行,但會按期產生(轉向)其餘待處理事件。在JavaScript中,很容易在閉包中保存正在進行的任務的狀態,以下面的示例2所示。

舉個簡單的例子,假設你想要的數字的平均計算1到n。

示例1:未作分割的狀況,平均成本 O(n):

for (let i = 0; i < n; i++)
  sum += i;
let avg = sum / n;
console.log('avg: ' + avg);

示例2:分割求平均值,每一個n異步步驟的成本O(1)。

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);
});

您能夠將此原則應用於數組迭代等。

offloading
若是您須要作一些更復雜的事情,partitioning也許不是一個好選擇。這是由於partitioning僅藉助於事件循環。而您幾乎沒法使用多核系統。 請記住,事件循環應該是調度客戶端請求,而不是本身完成它們。 對於複雜的任務,可將工做的轉嫁到工​​做池上。

How to offloading
對於要卸載工做的目標工做線池,您有兩個選項。

  • 您能夠經過開發C++插件來使用內置的Node Worker Pool 。在舊版本的Node上,使用NAN構建C++插件,在較新版本上使用N-API。node-webworker-threads提供了一種訪問Node的Worker Pool的JavaScript方法。
  • 您能夠建立和管理專用於計算的工做池,而不是Node的I/O主題工做池。最直接的方法是使用子進程或羣集。你應該不是簡單地建立一個子進程爲每一個客戶端。您能夠比建立和管理子項更快地接收客戶端請求,而且您的服務器可能會成爲一個分叉炸彈。

offloading的缺點
offloading方法的缺點是它會產生通訊成本。只容許Event Loop查看應用程序的「namespace」(JavaScript狀態)。從Worker中,您沒法在Event Loop的命名空間中操做JavaScript對象。相反,您必須序列化和反序列化您但願共享的任何對象。而後,Worker能夠對它們本身的這些對象的副本進行操做,並將修改後的對象(或「補丁」)返回給事件循環。

有關序列化問題,請參閱有關JSON DOS的部分。

一些卸載的建議
您須要區分CPU密集型和I/O密集型任務,由於它們具備明顯不一樣的特徵。

CPU密集型任務僅在調度其Worker時進行,而且必須將Worker調度到計算機的一個邏輯核心上。若是您有4個邏輯核心和5個工做線程,則其中一個工做線程會被掛起。因此,您須要爲此Worker支付開銷(內存和調度成本),而且沒有得到任何回報。

I/O密集型任務涉及查詢外部服務提供商(DNS,文件系統等)並等待其響應。雖然具備I/O密集型任務的Worker正在等待其響應,由於它沒有任何其餘事情可作從而被操做系統掛起。這就使另外一個Worker有機會提交其請求。所以,即便關聯的線程未運行,I/O密集型任務也將取得進展。數據庫和文件系統等外部服務提供商已通過高度優化,能夠同時處理許多待處理的請求。例如,文件系統將檢查大量待處理的寫入和讀取請求,以合併衝突的更新並以最佳順序檢索文件(詳情能夠參閱此處)。

若是您只依賴一個工做池,例如Node Worker Pool,那麼CPU綁定和I/O綁定工做的不一樣特性可能會損害您的應用程序的性能。

所以,您可能但願維護一個單獨的Computation Worker Pool。

offloadin結論
對於簡單的任務,例如迭代任意長數組的元素,partitioning多是一個不錯的選擇。若是您的計算更復雜,則offloading是一種更好的方法。雖然會有通訊成本,但在事件循環和工做池之間傳遞序列化對象的開銷,會被使用多個核心的好處所抵消。

可是,若是您的服務器在很大程度上依賴於複雜的計算,那麼您應該考慮Node是否真的適合。Node擅長I/O操做相關的工做,但對於複雜的計算,它可能不是最好的選擇。

若是您採用offloading方法,請參閱有關 不要阻塞工做池的部分。

不要阻塞工做池

Node有一個由kWorkers 組成的Worker Pool 。若是您使用上面討論的Offloading範例,您可能有一個單獨的計算工做池適用上述原則。在任何一種狀況下,咱們假設它k比可能同時處理的客戶端數量小得多。這與Node的「一個線程對應多個客戶端」的理念保持一致,這是其具備高可擴展性的關鍵點。

如上所述,每一個Worker在繼續執行Worker Pool隊列中的下一個Task以前,會先完成當前Task。

如今,處理客戶請求所需的任務成本會有所不一樣。某些任務能夠快速完成(例如,讀取短文件或緩存文件,或產生少許隨機字節);而其餘任務則須要更長時間(例如,讀取較大或未緩存的文件,或生成更多隨機字節)。您的目標應該是最小化任務時間的變化,能夠經過區分不一樣任務分區來達成上述目標。

最小化任務時間變化

若是Worker的當前處理的任務比其餘任務耗費資源比較多,那麼它將沒法用於其餘待處理的任務。換句話說,每一個相對較長的任務會減少工做池的大小直到完成。這是不可取的,由於在某種程度上,工做者池中的工做者越多,工做者池吞吐量(任務/秒)就越大,所以服務器吞吐量(客戶端請求/秒)就越大。耗時較長的任務將下降工做池的吞吐量,從而下降服務器的吞吐量。

爲避免這種狀況,您應該儘可能減小提交給工做池的任務長度的變化。雖然將I/O請求(DB,FS等)訪問的外部系統視爲黑盒是合適的,但您應該知道這些I/O請求的相對成本,而且應該避免提交可能耗時比較長的請求。

下面兩個例子應該能夠說明任務時間的可能變化。

時間變化示例一:長時間的文件讀取

假設您的服務器必須讀取文件以處理某些客戶端請求。在諮詢Node的文件系統 API以後,您選擇使用fs.readFile()以簡化操做。可是,fs.readFile()(當前)未分區:它提交fs.read()跨越整個文件的單個任務。若是您爲某些用戶閱讀較短的文件而爲其餘用戶閱讀較長的文件,則fs.readFile()可能會致使任務長度的顯着變化,從而損害工做人員池的吞吐量。

對於最壞的狀況,假設攻擊者可讓服務器讀取任意文件(這是一個目錄遍歷漏洞)。若是您的服務器運行Linux,攻擊者能夠命名一個很是慢的文件:/dev/random。出於全部實際目的,它/dev/random是無限慢的,而且每一個工做人員要求閱讀/dev/random將永遠不會完成該任務。而後k工做池提交攻擊者的請求。每一個工做一個請求,而且沒有其餘客戶端請求使用工做池將取得進展。

時間變化示例二:長時間運行的加密操做時間變化示例

假設您的服務器使用生成加密安全隨機字節crypto.randomBytes()。 crypto.randomBytes()未分區:它建立一個randomBytes()Task來生成所請求的字節數。若是爲某些用戶建立更少的字節,爲其餘用戶建立更多字節,則crypto.randomBytes()是任務時間長度變化的另外一個來源。

任務拆分

具備可變時間成本的任務可能會損害工做池的吞吐量。爲了儘可能減小任務時間的變化,您應儘量將每一個任務劃分爲時間可較少的子任務。當每一個子任務完成時,它應該提交下一個子任務,而且當最後的子任務完成時,它應該通知提交者。

繼續說上面fs.readFile()的例子,您應該使用fs.read()(手動分區)或ReadStream(自動分區)。

一樣的原則適用於CPU綁定任務; 該asyncAvg示例可能不適合事件循環,但它很是適合工做池。

將任務劃分爲子任務時,較短的任務會擴展爲少許的子任務,較長的任務會擴展爲更多的子任務。在較長任務的每一個子任務之間,分配給它的工做者能夠處理另外一個較短的任務的子任務,從而提升工做池的總體任務吞吐量。

請注意,已完成的子任務數量對於工做線程池的吞吐量而言並非一個有用的度量標準。相反,最終完成任務的數量纔是關注點。

不須要作任務拆分的任務

回想一下,任務分區的目的是最小化任務時間的變化。若是您能夠區分較短的任務和較長的任務(例如,對數組進行求和與對數組進行排序),則能夠爲每一個任務類建立一個工做池。將較短的任務和較長的任務路由到單獨的工做池是另外一種最小化任務時間變化的方法。

之因此要支持這種方法,是由於切割的任務會產生額外開銷(建立工做池任務表示和操做工做池隊列的成本)。而且這樣還能夠避免沒必要要的任務拆分,從而節省額外的訪問工做池的成本。它還能夠防止您在分區任務時出錯。

這種方法的缺點是全部這些工做池中的worker都會產生空間和時間開銷,而且會相互競爭CPU時間。請記住,每一個受CPU限制的任務僅在計劃時才進行。所以,您應該在仔細分析後才考慮這種方法。

Worker Pool:結論

不管您是僅使用Node工做池仍是維護單獨的工做池,您都應該優化池的任務吞吐量。

爲此,請使用任務拆分 以最小化任務時間的變化。

npm模塊帶來的風險

雖然Node核心模塊爲各類應用程序提供了構建塊,但有時須要更多的東西。Node開發人員從npm生態系統中獲益匪淺,數十萬個模塊提供了加速開發過程的功能。

但請記住,大多數這些模塊都是由第三方開發人員編寫的,而且一般只發布盡力而爲的保證。使用npm模塊的開發人員應該關注兩件事,儘管後者常常被遺忘。

  1. Does it honor its APIs?
  2. 它的API可能會阻塞事件循環或工做者嗎?許多模塊都沒有努力代表其API的成本,這對社區不利。

對於簡單的API,您能夠估算API的成本, 例如字符串操做的成本並不難理解。但在許多狀況下,很難搞清楚API可能會花費多少成本。

若是您正在調用可能會執行昂貴操做的API,請仔細檢查成本。要求開發人員記錄它,或者本身檢查源代碼(並提交記錄成本的PR)。

請記住,即便API是異步的,您也不知道它可能花費多少時間在Worker或每一個分區的Event Loop上。例如,假設在asyncAvg上面給出的示例中,對助手函數的每次調用將一半的數字相加而不是其中一個。那麼這個函數仍然是異步的,但每一個拆分的任務時間複雜度仍然是O(n),而不是O(1)。因此在使用任意值的n時,會使安全性下降不少。

結論

Node有兩種類型的線程:一個Event Loopk Workers。Event Loop負責JavaScript回調和非阻塞I/O,而且Worker執行與完成異步請求的C++代碼相對應的任務,包括阻止I/O和CPU密集型工做。兩種類型的線程一次只能處理一個活動。若是任何回調或任務須要很長時間,則運行它的線程將被阻止。若是您的應用程序進行阻塞回調或任務,則可能致使吞吐量(客戶端/秒)降級最多,而且最壞狀況下會致使徹底拒絕服務。

要編寫高吞吐量,更多防DoS的Web服務器,您必須確保在良性或惡意輸入上,您的事件循環和工做者都不會被阻塞。

相關文章
相關標籤/搜索