成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

不要阻塞事件循環(huán)(或工作池)

widuu / 1016人閱讀

摘要:接下來的部分將討論如何確保事件循環(huán)和工作池的公平調(diào)度。不要阻塞事件循環(huán)事件循環(huán)通知每個(gè)新客戶端連接并協(xié)調(diào)對客戶端的響應(yīng)。

你應(yīng)該閱讀本指南嗎?

如果您編寫比命令行腳本更復(fù)雜的程序,那么閱讀本文可以幫助您編寫性能更高,更安全的應(yīng)用程序。

在編寫本文檔時(shí),主要是基于Node服務(wù)器。但里面的原則也適用于其它復(fù)雜的Node應(yīng)用程序。在沒有特別說明操作系統(tǒng)的情況下,默認(rèn)為Linux。

TL; DR

Node.js在事件循環(huán)(初始化和回調(diào))中運(yùn)行JavaScript代碼,并提供工作池來處理成本比較高的任務(wù),如文件I/O。 Node服務(wù)節(jié)點(diǎn)有很強(qiáng)的擴(kuò)展能力,有時(shí)能提供比相對較重的Apache更好的解決方案。關(guān)鍵點(diǎn)就在于它使用少量線程來處理多客戶端連接。如果Node可以使用更少的線程,那么它可以將更多的系統(tǒng)時(shí)間和內(nèi)存用于客戶端,而不是為線程(內(nèi)存,上下文切換)占用額外空間和時(shí)間。但也因?yàn)镹ode只有少量的線程,因此在構(gòu)建應(yīng)用程序時(shí),必須明智地使用它們。

這里有一些保持Node服務(wù)器快速穩(wěn)健運(yùn)行的經(jīng)驗(yàn)法則: 當(dāng)在任何給定時(shí)間與每個(gè)客戶端關(guān)聯(lián)的工作“很小”時(shí),Node服務(wù)會很快。

這適用于事件循環(huán)上的回調(diào)和工作池上的任務(wù)。

為什么我要避免阻塞事件循環(huán)和工作池?

Node使用少量的線程來處理多個(gè)客戶端連接。在Node中有兩種類型的線程:

一個(gè)事件循環(huán)(又稱主循環(huán),主線程,事件線程等);

k工作池(也稱為線程池)中的工作池

如果一個(gè)線程需要很長時(shí)間來執(zhí)行回調(diào)(Event Loop)或任務(wù)(Worker),我們稱之為“阻塞”。雖然線程為處理一個(gè)客戶端連接而阻塞,但它無法處理來自任何其他客戶端的請求。這提供了阻止事件循環(huán)和工作池的兩個(gè)動(dòng)機(jī):

性能:如果經(jīng)常在任一類型的線程上執(zhí)行重量級活動(dòng),則服務(wù)器的吞吐量(請求/秒)將受到影響;

安全性:如果某個(gè)輸入可能會阻塞某個(gè)線程,則惡意客戶端可能會提交此“惡意輸入”,使線程阻塞,從而阻塞其它客戶端上的處理。這就很方便地的造成了 拒絕服務(wù)攻擊。

快速回顧一下Node

Node使用事件驅(qū)動(dòng)架構(gòu):它有一個(gè)事件循環(huán)用于調(diào)度 和 一個(gè)處理阻塞任務(wù)的工作池。

什么代碼在事件循環(huán)上運(yùn)行?

在開始時(shí),Node應(yīng)用程序首先完成初始化階段,即require模塊和注冊事件的回調(diào)。然后,Node應(yīng)用程序進(jìn)入事件循環(huán),通過執(zhí)行相應(yīng)的回調(diào)來響應(yīng)傳入的客戶端請求。此回調(diào)同步執(zhí)行,并在完成后又有可能注冊新的異步請求。這些新異步請求的回調(diào)也將在事件循環(huán)上執(zhí)行。

事件循環(huán)中還包含其它一些非阻塞異步請求(例如,網(wǎng)絡(luò)I/O)產(chǎn)生的回調(diào)。

總之,Event Loop執(zhí)行這些注冊為某些事件的JavaScript回調(diào),并且還負(fù)責(zé)完成非阻塞異步請求,如網(wǎng)絡(luò)I/O.

什么代碼在線程池(Worker Pool)中運(yùn)行

Node的線程池通過libuv(docs)實(shí)現(xiàn)。libuv暴露出一組任務(wù)提交的API。

Node使用線程池(Worker Pool)處理比較費(fèi)時(shí)的任務(wù)。例操作系統(tǒng)沒有提供非阻塞版本的I/O, CPU密集型任務(wù)等。

會用到線程池的Node模塊:

I/O密集型

DNS: dns.lookup(), dns.lookupService()

fs: 除了fs.FSWatcher()和所有明確同步調(diào)用的文件API,剩下的都會用到libuv實(shí)現(xiàn)的線程池

CPU密集型

Crypto: crypto.pbkdf2(), crypto.randomBytes(), crypto.randomFill()

Zlib: 除了明確聲明使用同步調(diào)用的API,剩下的都會用到libuv的線程池

在大多數(shù)Node應(yīng)用程序中,這些API是Worker Pool的唯一任務(wù)源。實(shí)際上,使用C++插件的應(yīng)用程序和模塊也可以提交任務(wù)給工作池。

為了完整起見,我們注意到當(dāng)從事件循環(huán)上的回調(diào)中調(diào)用上述其中一個(gè)API時(shí),事件循環(huán)會花費(fèi)一些較小的設(shè)置成本。因?yàn)樾枰M(jìn)入該API相關(guān)的C++實(shí)現(xiàn)模塊并將任務(wù)提交給工作池。與任務(wù)的總成本相比,這些成本可以忽略不計(jì),這就是事件循環(huán)將它轉(zhuǎn)接到C++模塊的原因。將這些任務(wù)之一提交給Worker Pool時(shí),Node會在Node C++綁定中提供指向相應(yīng)C++函數(shù)的指針。

Node如何確定接下來要運(yùn)行的代碼?

理論上,Event Loop 和 Worker Pool 分別操作待處理的事件 和 待完成的任務(wù)。

實(shí)際上,Event Loop并不真正維護(hù)隊(duì)列。相應(yīng)的,它有一組文件描述符,這些文件描述符被操作系統(tǒng)使用epoll(Linux),kqueue(OSX),事件端口(Solaris)或IOCP(Windows)等機(jī)制進(jìn)行監(jiān)視。這些文件描述符對應(yīng)于網(wǎng)絡(luò)套接字,它正在觀看的任何文件,等等。當(dāng)操作系統(tǒng)說其中一個(gè)文件描述符準(zhǔn)備就緒時(shí),Event Loop會將其轉(zhuǎn)換為相應(yīng)的事件并調(diào)用與該事件關(guān)聯(lián)的回調(diào)。您可以在此處詳細(xì)了解此過程。

相反,Worker Pool使用一個(gè)真正的隊(duì)列,隊(duì)列中包含要處理的任務(wù)。Worker從此隊(duì)列中出棧一個(gè)任務(wù)并對其進(jìn)行處理,完成后,Worker會為事件循環(huán)引發(fā)“至少一個(gè)任務(wù)已完成”事件。

這對應(yīng)用程序設(shè)計(jì)意味著什么?

在像Apache這樣的一個(gè)線程對應(yīng)一個(gè)客戶端連接的系統(tǒng)中,每個(gè)掛起的客戶端都被分配了自己的線程。如果處理一個(gè)客戶端的線程阻塞時(shí),操作系統(tǒng)會中斷它并切換到另一個(gè)處理客戶端請求的線程。因此操作系統(tǒng)確保需要少量工作的客戶不會受到需要更多工作的客戶的影響。

因?yàn)镹ode用很少的線程數(shù)量處理許多客戶端連接,如果一個(gè)線程處理一個(gè)客戶端的請求時(shí)被阻塞,那么其它被掛起的客戶端請求會一直得不到執(zhí)行機(jī)會,直到該線程完成其回調(diào)或任務(wù)。 因此,保證客戶端的連接都受到公平對待是你編寫程序的工作內(nèi)容。 這也就是說,在Node 程序中,不應(yīng)該在任何單個(gè)回調(diào)或任務(wù)中為任何客戶端做太多比較耗時(shí)的工作。

上面說的就是Node為什么可以很好地?cái)U(kuò)展的部分原因,但這也意味著開發(fā)者有責(zé)任確保公平的調(diào)度。接下來的部分將討論如何確保事件循環(huán)和工作池的公平調(diào)度。

不要阻塞事件循環(huán)

事件循環(huán)通知每個(gè)新客戶端連接并協(xié)調(diào)對客戶端的響應(yīng)。也就是說,所有傳入請求和傳出響應(yīng)都通過事件循環(huán)處理。這意味著如果事件循環(huán)在任何時(shí)候花費(fèi)的時(shí)間太長,所有當(dāng)前的 以及新進(jìn)來的客戶端連接都不會獲得響應(yīng)機(jī)會。

所以,要確保在任何時(shí)候都不應(yīng)該阻塞事件循環(huán)。換句話說,每一個(gè)JavaScript回調(diào)應(yīng)當(dāng)能夠快速完成。這當(dāng)然也適用于你await,Promise.then等。

確保這一點(diǎn)的一個(gè)好方法是推斷回調(diào)的“計(jì)算復(fù)雜度”。如果你的回調(diào)需要一定數(shù)量的步驟,無論它的參數(shù)是什么,總是會給每個(gè)連接的客戶段提供一個(gè)合理的響應(yīng)。如果回調(diào)根據(jù)其參數(shù)采用不同的步驟數(shù),那么就應(yīng)該考慮不同參數(shù)可能導(dǎo)致的計(jì)算復(fù)雜度。

例子1: 恒定時(shí)間的回調(diào)

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

例子2: 時(shí)間復(fù)雜度O(n)?;卣{(diào)運(yùn)行時(shí)間與n成線性關(guā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: 時(shí)間復(fù)雜度是O(n^2)的例子。當(dāng)n比較小的時(shí)候,回調(diào)執(zhí)行速度沒有太大的影響,如果n比較大,相對O(n)而言,會特別的慢。而且n+1 對 n而言,執(zhí)行時(shí)間也會增長很多。是指數(shù)級別的。

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);
});
如何更小心一點(diǎn)?

Node使用Google V8引擎解析JavaScript,這對于許多常見操作來說非???。但是有例外:regexp和JSON操作。

對于復(fù)雜的任務(wù),應(yīng)該考慮限制輸入長度并拒絕太長的輸入。這樣,即使回調(diào)具有很大的復(fù)雜度,通過限制輸入,也可以確?;卣{(diào)執(zhí)行時(shí)間不會超過最壞情況下的執(zhí)行時(shí)間。然后,可以依據(jù)此評估??回調(diào)的最壞情況成本,并確定其上下文中的運(yùn)行時(shí)間是否可接受。

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

一種比較常見的阻塞事件循環(huán)的方式是使用比較“脆弱”的正則表達(dá)式。

正則表達(dá)式(regexp)將輸入字符串與特定的模式匹配。通常我們認(rèn)為正則表達(dá)式只需要匹配一次輸入的字符串----時(shí)間復(fù)雜度是O(n),n是輸入字符串的長度。在許多情況下,確實(shí)只需要一次便可完成匹配。但在某些情況下,正則表達(dá)式可能需要對傳入的字符串進(jìn)行多次匹配----時(shí)間復(fù)雜度是O(2^n)。指數(shù)級增長意味著如果引擎需要x次回溯來確定匹配,那么如果我們在輸入字符串中再添加一個(gè)字符,則至少需要2*x次回溯。由于回溯次數(shù)與所需時(shí)間成線性關(guān)系,因此這種情況會阻塞事件循環(huán)。

一個(gè)“脆弱”的正則表達(dá)式在你的正則匹配引擎上運(yùn)行可能需要指數(shù)時(shí)間,導(dǎo)致你可能遭受REDOS(Regular expression Denial of Service - ReDoS)的“邪惡輸入”。但是正則表達(dá)式模式是否易受攻擊(即正則表達(dá)式引擎可能需要指數(shù)時(shí)間)實(shí)際上是一個(gè)難以回答的問題,并且取決于您使用的是Perl,Python,Ruby,Java,JavaScript等。但有一些經(jīng)驗(yàn)法則是適用于所有語言的:

避免使用嵌套量詞(a+)*。Node的regexp引擎可能可以快速處理其中的一些,但其他引擎容易受到攻擊。

避免使用帶有重疊子句的OR,例如(a|a)*。同樣,這種情況有時(shí)是快速的。

避免使用反向引用,例如(a.*) 1。沒有正則表達(dá)式引擎可以確保在線性時(shí)間內(nèi)匹配它們。

如果您正在進(jìn)行簡單的字符串匹配,請使用indexOf或其它本身替代方法。它會更輕量且永遠(yuǎn)不會超過O(n)。

如果您不確定您的正則表達(dá)式是否容易受到攻擊,但你需要明確的是即使易受攻擊的正則表達(dá)式和長輸入字符串,Node通常無法報(bào)告匹配項(xiàng)。當(dāng)不匹配時(shí), Node在嘗試匹配的輸入字符串的許多路徑之前,是無法確定是否會觸發(fā)指數(shù)級的時(shí)間長度。

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

以下是將其服務(wù)器暴露給REDOS的示例易受攻擊的正則表達(dá)式:

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

這個(gè)例子中易受攻擊的正則表達(dá)式是一種(糟糕的)方法來檢查Linux上的有效路徑。它匹配以“/”作為分隔符的字符串,如“/a/b/c”。它很危險(xiǎn),因?yàn)樗`反了規(guī)則1:它有一個(gè)雙重嵌套的量詞。

如果客戶端使用filePath查詢///.../n(100 / s后跟換行符“?!睂⒉黄ヅ涞膿Q行符),那么事件循環(huán)將永遠(yuǎn)有效,阻塞事件循環(huán)。此客戶端的REDOS攻擊導(dǎo)致所有其他客戶端在regexp匹配完成之前不會響應(yīng)。

因此,您應(yīng)該謹(jǐn)慎使用復(fù)雜的正則表達(dá)式來驗(yàn)證用戶輸入。

反REDOS資源

有一些工具可以檢查你的regexp是否安全,比如

safe-regex

rxxr2。

但是,它們并不能保證識別所有易受攻擊的正則表達(dá)式。

另一種方法是使用不同的正則表達(dá)式引擎。您可以使用node-re2模塊,該模塊使用Google非?;馃岬腞E2 regexp引擎。但是要注意,RE2與Node的regexp不是100%兼容,因此如果你使用node-re2模塊來處理你的regexp,請檢查回歸。node-re2不支持特別復(fù)雜的regexp。

如果您正在嘗試匹配一些特別常見的內(nèi)容,例如URL或文件路徑,請?jiān)趓egexp庫中查找示例或使用npm模塊,例如ip-regex。

阻塞事件循環(huán): Node核心模塊

Node里有一些核心模塊,包含一些比較耗時(shí)的同步API:

Encryption(加密)

Compression(壓縮)

File system(文件操作)

Child process(子進(jìn)程)

這些模塊中的一些API比較耗時(shí),主要是因?yàn)樾枰罅康挠?jì)算(encryption, compression),I/O操作(file I/O)或者兩者都有(child process)。 這些API旨在方便編寫腳本,但是在服務(wù)端也許并不適用。如果在事件循環(huán)中調(diào)用這些API,將會花費(fèi)更多的時(shí)間,從而導(dǎo)致事件循環(huán)阻塞。

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

加密:

crypto.randomBytes (同步版)

crypto.randomFillSync

crypto.pbkdf2Sync

您還應(yīng)該注意為加密和解密例程提供大量輸入。

壓縮:

zlib.inflateSync

zlib.deflateSync

文件系統(tǒng)

不要使用同步文件系統(tǒng)API。例如,如果您訪問的文件位于NFS等分布式文件系統(tǒng)中,則訪問時(shí)間可能會有很大差異。

child process(子進(jìn)程)

child_process.spawnSync

child_process.execSync

child_process.execFileSync

從Node V9開始,這個(gè)列表已經(jīng)比較完善了。

阻塞事件循環(huán): JSON DOS

JSON.parseJSON.stringify 是另外兩種比較耗時(shí)的操作。 盡管他們的時(shí)間復(fù)雜度是O(n),但是如果n比較大的話,也會花費(fèi)相當(dāng)多的操作時(shí)間。

如果你的服務(wù)程序操作對象主要是JSON,特別是這些JSON來自客戶端,那么你需要特別注意JSON對象的大小 或者 字符串的長度。

JSON 阻塞示例:我們創(chuàng)建一個(gè)大小為2 ^ 21 的obj對象,然后在字符串上JSON.stringify運(yùn)行indexOf,然后運(yùn)行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以及標(biāo)準(zhǔn)JSON API的異步版本,使用下面概述的事件循環(huán)分區(qū)。

復(fù)雜計(jì)算而不阻塞事件循環(huán)

假設(shè)您想在JavaScript中執(zhí)行復(fù)雜計(jì)算而不阻塞事件循環(huán)。您有兩種選擇:partitioning切割或offloading轉(zhuǎn)嫁。

partitioning切割
您可以對計(jì)算進(jìn)行分區(qū),以便每個(gè)計(jì)算都在事件循環(huán)上運(yùn)行,但會定期產(chǎn)生(轉(zhuǎn)向)其他待處理事件。在JavaScript中,很容易在閉包中保存正在進(jìn)行的任務(wù)的狀態(tài),如下面的示例2所示。

舉個(gè)簡單的例子,假設(shè)你想要的數(shù)字的平均計(jì)算1到n。

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

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

示例2:分割求平均值,每個(gè)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);
});

您可以將此原則應(yīng)用于數(shù)組迭代等。

offloading
如果您需要做一些更復(fù)雜的事情,partitioning也許不是一個(gè)好選擇。這是因?yàn)閜artitioning僅借助于事件循環(huán)。而您幾乎無法使用多核系統(tǒng)。 請記住,事件循環(huán)應(yīng)該是調(diào)度客戶端請求,而不是自己完成它們。 對于復(fù)雜的任務(wù),可將工作的轉(zhuǎn)嫁到工??作池上。

How to offloading
對于要卸載工作的目標(biāo)工作線池,您有兩個(gè)選項(xiàng)。

您可以通過開發(fā)C++插件來使用內(nèi)置的Node Worker Pool 。在舊版本的Node上,使用NAN構(gòu)建C++插件,在較新版本上使用N-API。node-webworker-threads提供了一種訪問Node的Worker Pool的JavaScript方法。

您可以創(chuàng)建和管理專用于計(jì)算的工作池,而不是Node的I/O主題工作池。最直接的方法是使用子進(jìn)程或群集。你應(yīng)該不是簡單地創(chuàng)建一個(gè)子進(jìn)程為每個(gè)客戶端。您可以比創(chuàng)建和管理子項(xiàng)更快地接收客戶端請求,并且您的服務(wù)器可能會成為一個(gè)分叉炸彈。

offloading的缺點(diǎn)
offloading方法的缺點(diǎn)是它會產(chǎn)生通信成本。只允許Event Loop查看應(yīng)用程序的“namespace”(JavaScript狀態(tài))。從Worker中,您無法在Event Loop的命名空間中操作JavaScript對象。相反,您必須序列化和反序列化您希望共享的任何對象。然后,Worker可以對它們自己的這些對象的副本進(jìn)行操作,并將修改后的對象(或“補(bǔ)丁”)返回給事件循環(huán)。

有關(guān)序列化問題,請參閱有關(guān)JSON DOS的部分。

一些卸載的建議
您需要區(qū)分CPU密集型和I/O密集型任務(wù),因?yàn)樗鼈兙哂忻黠@不同的特征。

CPU密集型任務(wù)僅在調(diào)度其Worker時(shí)進(jìn)行,并且必須將Worker調(diào)度到計(jì)算機(jī)的一個(gè)邏輯核心上。如果您有4個(gè)邏輯核心和5個(gè)工作線程,則其中一個(gè)工作線程會被掛起。所以,您需要為此Worker支付開銷(內(nèi)存和調(diào)度成本),并且沒有獲得任何回報(bào)。

I/O密集型任務(wù)涉及查詢外部服務(wù)提供商(DNS,文件系統(tǒng)等)并等待其響應(yīng)。雖然具有I/O密集型任務(wù)的Worker正在等待其響應(yīng),因?yàn)樗鼪]有任何其他事情可做從而被操作系統(tǒng)掛起。這就使另一個(gè)Worker有機(jī)會提交其請求。因此,即使關(guān)聯(lián)的線程未運(yùn)行,I/O密集型任務(wù)也將取得進(jìn)展。數(shù)據(jù)庫和文件系統(tǒng)等外部服務(wù)提供商已經(jīng)過高度優(yōu)化,可以同時(shí)處理許多待處理的請求。例如,文件系統(tǒng)將檢查大量待處理的寫入和讀取請求,以合并沖突的更新并以最佳順序檢索文件(詳情可以參閱此處)。

如果您只依賴一個(gè)工作池,例如Node Worker Pool,那么CPU綁定和I/O綁定工作的不同特性可能會損害您的應(yīng)用程序的性能。

因此,您可能希望維護(hù)一個(gè)多帶帶的Computation Worker Pool。

offloadin結(jié)論
對于簡單的任務(wù),例如迭代任意長數(shù)組的元素,partitioning可能是一個(gè)不錯(cuò)的選擇。如果您的計(jì)算更復(fù)雜,則offloading是一種更好的方法。雖然會有通信成本,但在事件循環(huán)和工作池之間傳遞序列化對象的開銷,會被使用多個(gè)核心的好處所抵消。

但是,如果您的服務(wù)器在很大程度上依賴于復(fù)雜的計(jì)算,那么您應(yīng)該考慮Node是否真的適合。Node擅長I/O操作相關(guān)的工作,但對于復(fù)雜的計(jì)算,它可能不是最好的選擇。

如果您采用offloading方法,請參閱有關(guān) 不要阻塞工作池的部分。

不要阻塞工作池

Node有一個(gè)由kWorkers 組成的Worker Pool 。如果您使用上面討論的Offloading范例,您可能有一個(gè)多帶帶的計(jì)算工作池適用上述原則。在任何一種情況下,我們假設(shè)它k比可能同時(shí)處理的客戶端數(shù)量小得多。這與Node的“一個(gè)線程對應(yīng)多個(gè)客戶端”的理念保持一致,這是其具有高可擴(kuò)展性的關(guān)鍵點(diǎn)。

如上所述,每個(gè)Worker在繼續(xù)執(zhí)行Worker Pool隊(duì)列中的下一個(gè)Task之前,會先完成當(dāng)前Task。

現(xiàn)在,處理客戶請求所需的任務(wù)成本會有所不同。某些任務(wù)可以快速完成(例如,讀取短文件或緩存文件,或產(chǎn)生少量隨機(jī)字節(jié));而其他任務(wù)則需要更長時(shí)間(例如,讀取較大或未緩存的文件,或生成更多隨機(jī)字節(jié))。您的目標(biāo)應(yīng)該是最小化任務(wù)時(shí)間的變化,可以通過區(qū)分不同任務(wù)分區(qū)來達(dá)成上述目標(biāo)。

最小化任務(wù)時(shí)間變化

如果Worker的當(dāng)前處理的任務(wù)比其他任務(wù)耗費(fèi)資源比較多,那么它將無法用于其他待處理的任務(wù)。換句話說,每個(gè)相對較長的任務(wù)會減小工作池的大小直到完成。這是不可取的,因?yàn)樵谀撤N程度上,工作者池中的工作者越多,工作者池吞吐量(任務(wù)/秒)就越大,因此服務(wù)器吞吐量(客戶端請求/秒)就越大。耗時(shí)較長的任務(wù)將降低工作池的吞吐量,從而降低服務(wù)器的吞吐量。

為避免這種情況,您應(yīng)該盡量減少提交給工作池的任務(wù)長度的變化。雖然將I/O請求(DB,F(xiàn)S等)訪問的外部系統(tǒng)視為黑盒是合適的,但您應(yīng)該知道這些I/O請求的相對成本,并且應(yīng)該避免提交可能耗時(shí)比較長的請求。

下面兩個(gè)例子應(yīng)該可以說明任務(wù)時(shí)間的可能變化。

時(shí)間變化示例一:長時(shí)間的文件讀取

假設(shè)您的服務(wù)器必須讀取文件以處理某些客戶端請求。在咨詢Node的文件系統(tǒng) API之后,您選擇使用fs.readFile()以簡化操作。但是,fs.readFile()(當(dāng)前)未分區(qū):它提交fs.read()跨越整個(gè)文件的單個(gè)任務(wù)。如果您為某些用戶閱讀較短的文件而為其他用戶閱讀較長的文件,則fs.readFile()可能會導(dǎo)致任務(wù)長度的顯著變化,從而損害工作人員池的吞吐量。

對于最壞的情況,假設(shè)攻擊者可以讓服務(wù)器讀取任意文件(這是一個(gè)目錄遍歷漏洞)。如果您的服務(wù)器運(yùn)行Linux,攻擊者可以命名一個(gè)非常慢的文件:/dev/random。出于所有實(shí)際目的,它/dev/random是無限慢的,并且每個(gè)工作人員要求閱讀/dev/random將永遠(yuǎn)不會完成該任務(wù)。然后k工作池提交攻擊者的請求。每個(gè)工作一個(gè)請求,并且沒有其他客戶端請求使用工作池將取得進(jìn)展。

時(shí)間變化示例二:長時(shí)間運(yùn)行的加密操作時(shí)間變化示例

假設(shè)您的服務(wù)器使用生成加密安全隨機(jī)字節(jié)crypto.randomBytes()。 crypto.randomBytes()未分區(qū):它創(chuàng)建一個(gè)randomBytes()Task來生成所請求的字節(jié)數(shù)。如果為某些用戶創(chuàng)建更少的字節(jié),為其他用戶創(chuàng)建更多字節(jié),則crypto.randomBytes()是任務(wù)時(shí)間長度變化的另一個(gè)來源。

任務(wù)拆分

具有可變時(shí)間成本的任務(wù)可能會損害工作池的吞吐量。為了盡量減少任務(wù)時(shí)間的變化,您應(yīng)盡可能將每個(gè)任務(wù)劃分為時(shí)間可較少的子任務(wù)。當(dāng)每個(gè)子任務(wù)完成時(shí),它應(yīng)該提交下一個(gè)子任務(wù),并且當(dāng)最后的子任務(wù)完成時(shí),它應(yīng)該通知提交者。

繼續(xù)說上面fs.readFile()的例子,您應(yīng)該使用fs.read()(手動(dòng)分區(qū))或ReadStream(自動(dòng)分區(qū))。

同樣的原則適用于CPU綁定任務(wù); 該asyncAvg示例可能不適合事件循環(huán),但它非常適合工作池。

將任務(wù)劃分為子任務(wù)時(shí),較短的任務(wù)會擴(kuò)展為少量的子任務(wù),較長的任務(wù)會擴(kuò)展為更多的子任務(wù)。在較長任務(wù)的每個(gè)子任務(wù)之間,分配給它的工作者可以處理另一個(gè)較短的任務(wù)的子任務(wù),從而提高工作池的整體任務(wù)吞吐量。

請注意,已完成的子任務(wù)數(shù)量對于工作線程池的吞吐量而言并不是一個(gè)有用的度量標(biāo)準(zhǔn)。相反,最終完成任務(wù)的數(shù)量才是關(guān)注點(diǎn)。

不需要做任務(wù)拆分的任務(wù)

回想一下,任務(wù)分區(qū)的目的是最小化任務(wù)時(shí)間的變化。如果您可以區(qū)分較短的任務(wù)和較長的任務(wù)(例如,對數(shù)組進(jìn)行求和與對數(shù)組進(jìn)行排序),則可以為每個(gè)任務(wù)類創(chuàng)建一個(gè)工作池。將較短的任務(wù)和較長的任務(wù)路由到多帶帶的工作池是另一種最小化任務(wù)時(shí)間變化的方法。

之所以要支持這種方法,是因?yàn)榍懈畹娜蝿?wù)會產(chǎn)生額外開銷(創(chuàng)建工作池任務(wù)表示和操作工作池隊(duì)列的成本)。并且這樣還可以避免不必要的任務(wù)拆分,從而節(jié)省額外的訪問工作池的成本。它還可以防止您在分區(qū)任務(wù)時(shí)出錯(cuò)。

這種方法的缺點(diǎn)是所有這些工作池中的worker都會產(chǎn)生空間和時(shí)間開銷,并且會相互競爭CPU時(shí)間。請記住,每個(gè)受CPU限制的任務(wù)僅在計(jì)劃時(shí)才進(jìn)行。因此,您應(yīng)該在仔細(xì)分析后才考慮這種方法。

Worker Pool:結(jié)論

無論您是僅使用Node工作池還是維護(hù)多帶帶的工作池,您都應(yīng)該優(yōu)化池的任務(wù)吞吐量。

為此,請使用任務(wù)拆分 以最小化任務(wù)時(shí)間的變化。

npm模塊帶來的風(fēng)險(xiǎn)

雖然Node核心模塊為各種應(yīng)用程序提供了構(gòu)建塊,但有時(shí)需要更多的東西。Node開發(fā)人員從npm生態(tài)系統(tǒng)中獲益匪淺,數(shù)十萬個(gè)模塊提供了加速開發(fā)過程的功能。

但請記住,大多數(shù)這些模塊都是由第三方開發(fā)人員編寫的,并且通常只發(fā)布盡力而為的保證。使用npm模塊的開發(fā)人員應(yīng)該關(guān)注兩件事,盡管后者經(jīng)常被遺忘。

Does it honor its APIs?

它的API可能會阻塞事件循環(huán)或工作者嗎?許多模塊都沒有努力表明其API的成本,這對社區(qū)不利。

對于簡單的API,您可以估算API的成本, 例如字符串操作的成本并不難理解。但在許多情況下,很難搞清楚API可能會花費(fèi)多少成本。

如果您正在調(diào)用可能會執(zhí)行昂貴操作的API,請仔細(xì)檢查成本。要求開發(fā)人員記錄它,或者自己檢查源代碼(并提交記錄成本的PR)。

請記住,即使API是異步的,您也不知道它可能花費(fèi)多少時(shí)間在Worker或每個(gè)分區(qū)的Event Loop上。例如,假設(shè)在asyncAvg上面給出的示例中,對助手函數(shù)的每次調(diào)用將一半的數(shù)字相加而不是其中一個(gè)。那么這個(gè)函數(shù)仍然是異步的,但每個(gè)拆分的任務(wù)時(shí)間復(fù)雜度仍然是O(n),而不是O(1)。所以在使用任意值的n時(shí),會使安全性降低很多。

結(jié)論

Node有兩種類型的線程:一個(gè)Event Loopk Workers。Event Loop負(fù)責(zé)JavaScript回調(diào)和非阻塞I/O,并且Worker執(zhí)行與完成異步請求的C++代碼相對應(yīng)的任務(wù),包括阻止I/O和CPU密集型工作。兩種類型的線程一次只能處理一個(gè)活動(dòng)。如果任何回調(diào)或任務(wù)需要很長時(shí)間,則運(yùn)行它的線程將被阻止。如果您的應(yīng)用程序進(jìn)行阻塞回調(diào)或任務(wù),則可能導(dǎo)致吞吐量(客戶端/秒)降級最多,并且最壞情況下會導(dǎo)致完全拒絕服務(wù)。

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

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/96914.html

相關(guān)文章

  • Node.js 指南(不要阻塞事件循環(huán)工作

    摘要:為什么要避免阻塞事件循環(huán)和工作池使用少量線程來處理許多客戶端,在中有兩種類型的線程一個(gè)事件循環(huán)又稱主循環(huán)主線程事件線程等,以及一個(gè)工作池也稱為線程池中的個(gè)的池。 不要阻塞事件循環(huán)(或工作池) 你應(yīng)該閱讀這本指南嗎? 如果你編寫的內(nèi)容比簡短的命令行腳本更復(fù)雜,那么閱讀本文應(yīng)該可以幫助你編寫性能更高、更安全的應(yīng)用程序。 本文檔是在考慮Node服務(wù)器的情況下編寫的,但這些概念也適用于復(fù)雜的N...

    hatlonely 評論0 收藏0
  • 【譯】node js event loop part 1.1

    原文 先說1.1總攬: Reactor模式 Reactor模式中的協(xié)調(diào)機(jī)制Event Loop Reactor模式中的事件分離器Event Demultiplexer 一些Event Demultiplexer處理不了的復(fù)雜I/O接口比如File I/O、DNS等 復(fù)雜I/O的解決方案 未完待續(xù) 前言 nodejs和其他編程平臺的區(qū)別在于如何去處理I/O接口,我們聽一個(gè)人介紹nodejs,總是...

    macg0406 評論0 收藏0
  • Node中的事件循環(huán)

    摘要:的事件循環(huán)一個(gè)線程有唯一的一個(gè)事件循環(huán)。索引就是指否還有需要執(zhí)行的事件,是否還有請求,關(guān)閉事件循環(huán)的請求等等。先來看一下定義的定義是在事件循環(huán)的下一個(gè)階段之前執(zhí)行對應(yīng)的回調(diào)。雖然是這樣定義的,但是它并不是為了在事件循環(huán)的每個(gè)階段去執(zhí)行的。 Node中的事件循環(huán) 如果對前端瀏覽器的時(shí)間循環(huán)不太清楚,請看這篇文章。那么node中的事件循環(huán)是什么樣子呢?其實(shí)官方文檔有很清楚的解釋,本文先從n...

    lwx12525 評論0 收藏0
  • NodeJS架構(gòu) - 單線程事件循環(huán)模型

    摘要:客戶端可能需要等待服務(wù)器釋放可用的線程去處理其請求處理阻塞式的任務(wù)時(shí)浪費(fèi)時(shí)間的架構(gòu)單線程事件循環(huán)不遵循請求響應(yīng)多線程無狀態(tài)模型。它采用單線程與事件循環(huán)模型。 showImg(https://segmentfault.com/img/remote/1460000017402136); 這篇譯章探究了NodeJS的架構(gòu)和單線程事件循環(huán)模型。我們將在本文中討論NodeJS如何在底層工作,它遵...

    leap_frog 評論0 收藏0
  • Node - 異步IO和事件循環(huán)

    摘要:它是在的基礎(chǔ)上改進(jìn)的一種方案,通過對文件描述符上的事件狀態(tài)進(jìn)行判斷。檢索新的事件執(zhí)行與相關(guān)的回調(diào)幾乎所有情況下,除了關(guān)閉的回調(diào)函數(shù),它們由計(jì)時(shí)器和排定的之外,其余情況將在此處阻塞。執(zhí)行事件的,例如或者。 前言 學(xué)習(xí)Node就繞不開異步IO, 異步IO又與事件循環(huán)息息相關(guān), 而關(guān)于這一塊一直沒有仔細(xì)去了解整理過, 剛好最近在做項(xiàng)目的時(shí)候, 有了一些思考就記錄了下來, 希望能盡量將這一塊的...

    MyFaith 評論0 收藏0

發(fā)表評論

0條評論

最新活動(dòng)
閱讀需要支付1元查看
<