1. 程式人生 > >不要阻塞事件迴圈(或工作池)

不要阻塞事件迴圈(或工作池)

原文: Don’t Block the Event Loop (or the Worker Pool)

你應該閱讀本指南嗎?

如果您編寫比命令列指令碼更復雜的程式,那麼閱讀本文可以幫助您編寫效能更高,更安全的應用程式。

在編寫本文件時,主要是基於Node伺服器。但裡面的原則也適用於其它複雜的Node應用程式。在沒有特別說明作業系統的情況下,預設為Linux。

TL; DR

Node.js在事件迴圈(初始化和回撥)中執行JavaScript程式碼,並提供工作池來處理成本比較高的任務,如檔案I/O。 Node服務節點有很強的擴充套件能力,有時能提供比相對較重的Apache更好的解決方案。關鍵點就在於它使用少量執行緒來處理多客戶端連線。如果Node可以使用更少的執行緒,那麼它可以將更多的系統時間和記憶體用於客戶端,而不是為執行緒(記憶體,上下文切換)佔用額外空間和時間。但也因為Node只有少量的執行緒,因此在構建應用程式時,必須明智地使用它們。

這裡有一些保持Node伺服器快速穩健執行的經驗法則: 當在任何給定時間與每個客戶端關聯的工作“很小”時,Node服務會很快。

這適用於事件迴圈上的回撥和工作池上的任務。

為什麼我要避免阻塞事件迴圈和工作池?

Node使用少量的執行緒來處理多個客戶端連線。在Node中有兩種型別的執行緒:

  • 一個事件迴圈(又稱主迴圈,主執行緒,事件執行緒等);
  • k工作池(也稱為執行緒池)中的工作池

如果一個執行緒需要很長時間來執行回撥(Event Loop)或任務(Worker),我們稱之為“阻塞”。雖然執行緒為處理一個客戶端連線而阻塞,但它無法處理來自任何其他客戶端的請求。這提供了阻止事件迴圈和工作池的兩個動機:

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

快速回顧一下Node

Node使用事件驅動架構:它有一個事件迴圈用於排程 和 一個處理阻塞任務的工作池。

什麼程式碼在事件迴圈上執行?

在開始時,Node應用程式首先完成初始化階段,即require模組和註冊事件的回撥。然後,Node應用程式進入事件迴圈,通過執行相應的回撥來響應傳入的客戶端請求。此回撥同步執行,並在完成後又有可能註冊新的非同步請求。這些新非同步請求的回撥也將在事件迴圈上執行。

事件迴圈中還包含其它一些非阻塞非同步請求(例如,網路I/O)產生的回撥。

總之,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伺服器,您必須確保在良性或惡意輸入上,您的事件迴圈和工作者都不會被阻塞。