摘要:為什么要避免阻塞事件循環(huán)和工作池使用少量線程來處理許多客戶端,在中有兩種類型的線程一個事件循環(huán)又稱主循環(huán)主線程事件線程等,以及一個工作池也稱為線程池中的個的池。
不要阻塞事件循環(huán)(或工作池) 你應該閱讀這本指南嗎?
如果你編寫的內(nèi)容比簡短的命令行腳本更復雜,那么閱讀本文應該可以幫助你編寫性能更高、更安全的應用程序。
本文檔是在考慮Node服務器的情況下編寫的,但這些概念也適用于復雜的Node應用程序,在特定于操作系統(tǒng)的細節(jié)有所不同,本文檔以Linux為中心。
TL; DRNode.js在事件循環(huán)(初始化和回調(diào))中運行JavaScript代碼,并提供一個工作池來處理如文件I/O之類昂貴的任務,Node可以很好地擴展,有時比Apache等更重量級的方法更好,Node可擴展性的秘訣在于它使用少量線程來處理許多客戶端。如果Node可以使用更少的線程,那么它可以將更多的系統(tǒng)時間和內(nèi)存用于客戶端,而不是為線程支付空間和時間開銷(內(nèi)存,上下文切換),但由于Node只有幾個線程,因此你必須明智地使用它們來構建應用程序。
這是保持Node服務器快速的一個很好的經(jīng)驗法則:當在任何給定時間與每個客戶端相關的工作“很小”時,Node很快。
這適用于事件循環(huán)上的回調(diào)和工作池上的任務。
為什么要避免阻塞事件循環(huán)和工作池?Node使用少量線程來處理許多客戶端,在Node中有兩種類型的線程:一個事件循環(huán)(又稱主循環(huán)、主線程、事件線程等),以及一個工作池(也稱為線程池)中的k個Worker的池。
如果一個線程需要很長時間來執(zhí)行回調(diào)(事件循環(huán))或任務(Worker),我們稱之為“阻塞”,雖然線程被阻塞代表一個客戶端工作,但它無法處理來自任何其他客戶端的請求,這提供了阻塞事件循環(huán)和工作池的兩個動機:
性能:如果你經(jīng)常在任一類型的線程上執(zhí)行重量級活動,則服務器的吞吐量(請求/秒)將受到影響。
安全:如果某個輸入可能會阻塞某個線程,則惡意客戶端可能會提交此“惡意輸入”,使你的線程阻塞,并阻止他們?yōu)槠渌蛻艄ぷ鳎@將是拒絕服務攻擊。
快速回顧一下NodeNode使用事件驅(qū)動架構:它有一個用于協(xié)調(diào)的事件循環(huán)和一個用于昂貴任務的工作池。
什么代碼在事件循環(huán)上運行?當它們開始時,Node應用程序首先完成初始化階段,require模塊并注冊事件的回調(diào),然后,Node應用程序進入事件循環(huán),通過執(zhí)行適當?shù)幕卣{(diào)來響應傳入的客戶端請求,此回調(diào)同步執(zhí)行,并可以注冊異步請求以在完成后繼續(xù)處理,這些異步請求的回調(diào)也將在事件循環(huán)上執(zhí)行。
事件循環(huán)還將完成其回調(diào)(例如,網(wǎng)絡I/O)所產(chǎn)生的非阻塞異步請求。
總之,事件循環(huán)執(zhí)行為事件注冊的JavaScript回調(diào),并且還負責完成非阻塞異步請求,如網(wǎng)絡I/O。
什么代碼在工作池上運行?Node的工作池在libuv(docs)中實現(xiàn),它公開了通用任務提交API。
Node使用工作池來處理“昂貴”的任務,這包括操作系統(tǒng)不提供非阻塞版本的I/O,以及特別是CPU密集型任務。
這些是使用此工作池的Node模塊API:
I/O密集型
DNS:dns.lookup()、dns.lookupService()。
文件系統(tǒng):除fs.FSWatcher()之外的所有文件系統(tǒng)API和明確同步的API都使用libuv的線程池。
CPU密集型
Crypto:crypto.pbkdf2()、crypto.randomBytes()、crypto.randomFill()。
Zlib:除明確同步的那些之外的所有zlib API都使用libuv的線程池。
在許多Node應用程序中,這些API是工作池的唯一任務源,使用C++插件的應用程序和模塊可以將其他任務提交給工作池。
為了完整起見,我們注意到當你從事件循環(huán)上的回調(diào)中調(diào)用其中一個API時,事件循環(huán)花費一些較小的設置成本,因為它進入該API的Node C++綁定并將任務提交給工作池,與任務的總成本相比,這些成本可以忽略不計,這就是事件循環(huán)卸載它的原因。將這些任務之一提交給工作池時,Node會在Node C++綁定中提供指向相應C++函數(shù)的指針。
Node如何確定接下來要運行的代碼?抽象地說,事件循環(huán)和工作池分別維護待處理事件和待處理任務的隊列。
實際上,事件循環(huán)實際上并不維護隊列,相反,它有一組文件描述符,它要求操作系統(tǒng)使用epoll(Linux)、kqueue(OSX)、事件端口(Solaris)或IOCP(Windows)等機制進行監(jiān)控。這些文件描述符對應于網(wǎng)絡sockets、它正在監(jiān)視的任何文件,等等,當操作系統(tǒng)說其中一個文件描述符準備就緒時,事件循環(huán)會將其轉(zhuǎn)換為相應的事件并調(diào)用與該事件關聯(lián)的回調(diào),你可以在這里了解更多關于此過程的信息。
相反,工作池使用一個真正的隊列,其條目是要處理的任務,一個Worker從此隊列中彈出一個任務并對其進行處理,完成后,Worker會為事件循環(huán)引發(fā)“至少一個任務已完成”事件。
這對于應用程序設計意味著什么?在像Apache這樣的每個客戶端一個線程的系統(tǒng)中,每個掛起的客戶端都被分配了自己的線程,如果處理一個客戶端的線程阻塞,操作系統(tǒng)將中斷它并給另一個客戶端一個機會,因此,操作系統(tǒng)確保需要少量工作的客戶端不會被需要更多工作的客戶端造成不利。
因為Node使用很少的線程處理許多客戶端,如果一個線程阻塞處理一個客戶端的請求,那么待處理的客戶端請求可能不會輪到,直到線程完成其回調(diào)或任務。因此,公平對待客戶端是你應用程序的職責,這意味著你不應該在任何單個回調(diào)或任務中為任何客戶端做太多工作。
這是Node可以很好地擴展的部分原因,但這也意味著你有責任確保公平的調(diào)度,接下來的部分將討論如何確保事件循環(huán)和工作池的公平調(diào)度。
不要阻塞事件循環(huán)事件循環(huán)通知每個新客戶端連接并協(xié)調(diào)響應的生成,所有傳入請求和傳出響應都通過事件循環(huán)傳遞,這意味著如果事件循環(huán)在任何時候花費的時間太長,所有當前和新客戶端都不會獲得機會。
你應該確保永遠不會阻塞事件循環(huán),換句話說,每個JavaScript回調(diào)都應該快速完成,這當然也適用于你的await、你的Promise.then等等。
確保這一點的一個好方法是考慮回調(diào)的“計算復雜性”,如果你的回調(diào)無論參數(shù)是什么,都采取一定數(shù)量的步驟,那么你將始終公平地對待每個掛起的客戶端,如果你的回調(diào)根據(jù)其參數(shù)采用不同的步驟數(shù),那么你應該考慮參數(shù)可能有多長。
示例1:一個固定時間的回調(diào)。
app.get("/constant-time", (req, res) => { res.sendStatus(200); });
示例2:O(n)回調(diào),對于小n,此回調(diào)將快速運行,對于大n,此回調(diào)將緩慢運行。
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)回調(diào),對于小n,此回調(diào)仍將快速運行,但對于大n,它將比前一個O(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,這對于許多常見操作來說非常快,此規(guī)則的例外是正則表達式和JSON操作,如下所述。
但是,對于復雜的任務,你應該考慮限制輸入并拒絕太長的輸入,這樣,即使你的回調(diào)具有很大的復雜性,通過限制輸入,你可以確保回調(diào)不會超過最長可接受輸入的最壞情況時間,然后,你可以評估此??回調(diào)的最壞情況成本,并確定其上下文中的運行時間是否可接受。
阻塞事件循環(huán):REDOS阻塞事件循環(huán)災難性的一種常見方法是使用“易受攻擊”的正則表達式。
避免易受攻擊的正則表達式正則表達式(regexp)將輸入字符串與模式匹配,我們通常認為正則表達式匹配需要單次通過輸入字符串 — O(n)時間,其中n是輸入字符串的長度,在許多情況下,確實單次通過。
不幸的是,在某些情況下,正則表達式匹配可能需要通過輸入字符串的指數(shù)次數(shù) — O(2^n)時間,指數(shù)次數(shù)意味著如果引擎需要x次以確定匹配,如果我們只在輸入字符串中添加一個字符,它將需要2*x次,由于次數(shù)與所需時間成線性關系,因此該評估的效果將是阻塞事件循環(huán)。
一個易受攻擊的正則表達式可能會使你的正則表達式引擎花費指數(shù)級的時間,使你暴露在“惡意輸入”上的REDOS中。你的正則表達式模式是否易受攻擊(即正則表達式引擎可能需要指數(shù)時間)實際上是一個難以回答的問題,并取決于你使用的是Perl、Python、Ruby、Java、JavaScript等,但是這里有一些適用于所有這些語言的經(jīng)驗法則:
避免嵌套量詞,如(a+)*,Node的regexp引擎可以快速處理其中的一些,但其他引擎容易受到攻擊。
避免使用帶有重疊子句的OR,如(a|a)*,同樣,這些有時是快速的。
避免使用反向引用,例如(a.*) 1,沒有正則表達式引擎可以保證在線性時間內(nèi)評估它們。
如果你正在進行簡單的字符串匹配,請使用indexOf或本地等效項,它會更便宜,永遠不會超過O(n)。
如果你不確定你的正則表達式是否容易受到攻擊,請記住,Node通常不會遇到報告匹配的問題,即使是易受攻擊的正則表達式和長輸入字符串,當存在不匹配時觸發(fā)指數(shù)行為,但是在嘗試通過輸入字符串的許多路徑之前,Node無法確定。
一個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”,它很危險,因為它違反了規(guī)則1:它有一個雙重嵌套的量詞。
如果客戶端使用filePath ///.../ 查詢(100個/后跟換行符,正則表達式的“.”不會匹配),那么事件循環(huán)將永遠有效,阻塞事件循環(huán),此客戶端的REDOS攻擊導致所有其他客戶端在正則表達式匹配完成之前不會輪到。
因此,你應該謹慎使用復雜的正則表達式來驗證用戶輸入。
Anti-REDOS資源有一些工具可以檢查你的正則表達式是否安全,比如
safe-regex
rxxr2,然而,這些都不能捕獲所有易受攻擊的正則表達式。
另一種方法是使用不同的正則表達式引擎,你可以使用node-re2模塊,該模塊使用Google超快的RE2正則表達式引擎,但請注意,RE2與Node的正則表達式不是100%兼容,因此如果你交換node-re2模塊來處理你的正則表達式,請回歸檢查,并且node-re2不支持特別復雜的正則表達式。
如果你正在嘗試匹配“明顯”的內(nèi)容,例如URL或文件路徑,請在正則表達式庫中查找示例或使用npm模塊,例如:ip-regex。
阻塞事件循環(huán):Node核心模塊幾個Node核心模塊具有同步昂貴的API,包括:
Encryption
Compression
File system
Child process
這些API很昂貴,因為它們涉及大量計算(加密、壓縮),需要I/O(文件I/O),或者可能兩者(子進程),這些API旨在方便腳本,但不打算在服務器上下文中使用,如果在事件循環(huán)上執(zhí)行它們,它們將比典型的JavaScript指令花費更長的時間來完成,從而阻塞事件循環(huán)。
在服務器中,你不應使用以下模塊中的以下同步API:
Encryption:
crypto.randomBytes(同步版本)
crypto.randomFillSync
crypto.pbkdf2Sync
你還應該小心為加密和解密例程提供大量輸入。
Compression:
zlib.inflateSync
zlib.deflateSync
File system:
不要使用同步文件系統(tǒng)API,例如,如果你訪問的文件位于NFS等分布式文件系統(tǒng)中,則訪問時間可能會有很大差異。
Child process:
child_process.spawnSync
child_process.execSync
child_process.execFileSync
從Node v9開始,此列表相當完整。
阻塞事件循環(huán):JSON DOSJSON.parse和JSON.stringify是其他可能很昂貴的操作,雖然這些在輸入的長度上是O(n),但對于大的n,它們可能花費驚人的長。
如果你的服務器操縱JSON對象,特別是來自客戶端的JSON對象,你應該對在事件循環(huán)上使用的對象或字符串的大小保持謹慎。
示例:JSON阻塞,我們創(chuàng)建一個大小為2^21的對象obj并且JSON.stringify它,在字符串上運行indexOf,然后JSON.parse它,JSON.stringify的字符串是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 < niter; i++) { obj = { obj1: obj, obj2: obj }; // Doubles in size each iter } before = process.hrtime(); res = JSON.stringify(obj); took = process.hrtime(before); console.log("JSON.stringify took " + took); before = process.hrtime(); res = 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);
有npm模塊提供異步JSON API,例如:
JSONStream,具有流API。
Big-Friendly JSON,它具有流API以及標準JSON API的異步版本,使用下面概述的事件循環(huán)分區(qū)范例。
不阻塞事件循環(huán)的復雜計算假設你想在JavaScript中執(zhí)行復雜計算而不阻塞事件循環(huán),你有兩種選擇:分區(qū)或卸載。
分區(qū)你可以對計算進行分區(qū),以便每個計算都在事件循環(huán)上運行,但會定期產(chǎn)生(轉(zhuǎn)向)其他待處理事件,在JavaScript中,很容易在閉包中保存正在進行的任務的狀態(tài),如下面的示例2所示。
舉一個簡單的例子,假設你想要計算數(shù)字1到n的平均值。
示例1:未分區(qū)求平均值,花費O(n)。
for (let i = 0; i < n; i++) sum += i; let avg = sum / n; console.log("avg: " + avg);
示例2:分區(qū)求平均值,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); });
你可以將此原則應用于數(shù)組迭代等。
卸載如果你需要做一些更復雜的事情,分區(qū)不是一個好選擇,這是因為分區(qū)僅使用事件循環(huán),你幾乎無法在計算機上使用多個核心,請記住,事件循環(huán)應該協(xié)調(diào)客戶端請求,而不是自己完成它們,對于復雜的任務,將工作循環(huán)的工作移到工??作池上。
對于要卸載工作的目標工作線池,你有兩個選項。
你可以通過開發(fā)C++插件來使用內(nèi)置的Node工作池,在舊版本的Node上,使用NAN構建C++插件,在較新版本上使用N-API,node-webworker-threads提供了一種訪問Node的工作池的JavaScript方法。
你可以創(chuàng)建和管理專用于計算的工作池,而不是Node的I/O主題工作池,最直接的方法是使用子進程或群集。
你不應該只是為每個客戶創(chuàng)建一個子進程,你可以比創(chuàng)建和管理子進程更快地接收客戶機請求,你的服務器可能會成為一個fork炸彈。
卸載方法的缺點是它會以通信成本的形式產(chǎn)生開銷,只允許事件循環(huán)查看應用程序的“namespace”(JavaScript狀態(tài)),從Worker中,你無法在事件循環(huán)的命名空間中操作JavaScript對象,相反,你必須序列化和反序列化你希望共享的任何對象,然后,Worker可以對它自己的這些對象的副本進行操作,并將修改后的對象(或“補丁”)返回給事件循環(huán)。
有關序列化問題,請參閱有關JSON DOS的部分。
一些卸載的建議你可能希望區(qū)分CPU密集型和I/O密集型任務,因為它們具有明顯不同的特征。
CPU密集型任務僅在調(diào)度其Worker時進行,并且必須將Worker調(diào)度到計算機的一個邏輯核心上,如果你有4個邏輯核心和5個Worker,則其中一個Worker無法進行,因此,你為此Worker支付了開銷(內(nèi)存和調(diào)度成本),并且沒有獲得任何回報。
I/O密集型任務涉及查詢外部服務提供者(DNS,文件系統(tǒng)等)并等待其響應,雖然具有I/O密集型任務的Worker正在等待其響應,但它沒有其他任何操作可以由操作系統(tǒng)取消調(diào)度,從而使另一個Worker有機會提交其請求,因此,即使關聯(lián)的線程未運行,I/O密集型任務也將進行。數(shù)據(jù)庫和文件系統(tǒng)等外部服務提供者已經(jīng)過高度優(yōu)化,可以同時處理許多待處理的請求,例如,文件系統(tǒng)將檢查大量待處理的寫入和讀取請求,以合并沖突的更新并以最佳順序檢索文件(例如,參見這些幻燈片)。
如果你只依賴一個工作池,例如Node工作器池,然后CPU綁定和I/O綁定工作的不同特性可能會損害你的應用程序的性能。
因此,你可能希望維護一個多帶帶的計算工作池。
卸載:結論對于簡單的任務,例如迭代任意長數(shù)組的元素,分區(qū)可能是一個不錯的選擇,如果你的計算更復雜,卸載是一種更好的方法:通信成本,即在事件循環(huán)和工作池之間傳遞序列化對象的開銷,被使用多個核心的好處所抵消。
如果你采用卸載方法,請參閱有關不阻塞工作池的部分。
不要阻塞工作池Node有一個由k個Worker組成的工作池,如果你使用上面討論的卸載范例,你可能會有一個多帶帶的計算工作池,相同的原則適用于此。在任何一種情況下,我們假設k遠小于你可能同時處理的客戶端數(shù)量,這與Node的“一個線程用于許多客戶端”理念保持一致,這是其可擴展性的秘訣。
如上所述,每個Worker在繼續(xù)執(zhí)行工作池隊列中的下一個任務之前完成其當前任務。
現(xiàn)在,處理客戶端請求所需的任務成本會有所不同,某些任務可以快速完成(例如,讀取短文件或緩存文件,或產(chǎn)生少量隨機字節(jié)),而其他任務則需要更長時間(例如讀取較大或未緩存的文件,或生成更多隨機字節(jié)),你的目標應該是最小化任務時間的變化,你應該使用任務分區(qū)來完成此任務。
最小化任務時間的變化如果Worker的當前任務比其他任務昂貴得多,那么它將無法用于其他待處理的任務,換句話說,每個相對較長的任務有效地將工作池的大小減小,直到它完成。這是不可取的,因為在某種程度上,工作者池中的工作者越多,工作者池吞吐量(任務/秒)越大,因此服務器吞吐量越大(客戶端請求/秒),具有相對昂貴的任務的一個客戶端將降低工作池的吞吐量,從而降低服務器的吞吐量。
為避免這種情況,你應該盡量減少提交給工作池的任務長度的變化,雖然將I/O請求(DB,F(xiàn)S等)訪問的外部系統(tǒng)視為黑盒是合適的,你應該知道這些I/O請求的相對成本,并且應該避免提交你可能預期特別長的請求。
兩個例子可以說明任務時間的可能變化。
變化示例:長時間運行的文件系統(tǒng)讀取假設你的服務器必須讀取文件以處理某些客戶端請求,在咨詢了Node的文件系統(tǒng)API之后,為了簡單起見,你選擇使用fs.readFile(),但是fs.readFile()(當前)未分區(qū):它提交一個跨越整個文件的fs.read()任務,如果為某些用戶讀取較短的文件,為其他用戶讀取較長的文件,fs.readFile()可能會導致任務長度的顯著變化,從而損害工作者池的吞吐量。
對于最壞的情況,假設攻擊者可以說服你的服務器讀取任意文件(這是目錄遍歷漏洞),如果你的服務器運行的是Linux,攻擊者可以命名一個速度極慢的文件:/dev/random,出于所有實際目的,/dev/random是無限慢的,每個Worker要求從/dev/random讀取將永遠不會完成該任務,然后,攻擊者提交k個請求,每個Worker一個請求,并且使用工作池的其他客戶機請求不會取得進展。
變化示例:長時間運行的加密操作假設你的服務器使用crypto.randomBytes()生成加密安全隨機字節(jié),crypto.randomBytes()沒有被分區(qū):它創(chuàng)建一個randomBytes()任務來生成所請求的字節(jié)數(shù),如果為某些用戶創(chuàng)建更少的字節(jié),為其他用戶創(chuàng)建更多字節(jié),則crypto.randomBytes()是任務長度的另一個變化來源。
任務分區(qū)具有可變時間成本的任務可能會損害工作池的吞吐量,為了盡量減少任務時間的變化,你應盡可能將每個任務劃分為可比較的子任務,當每個子任務完成時,它應該提交下一個子任務,并且當最后的子任務完成時,它應該通知提交者。
要繼續(xù)fs.readFile()示例,你應該使用fs.read()(手動分區(qū))或ReadStream(自動分區(qū))。
同樣的原則適用于CPU綁定任務,asyncAvg示例可能不適合事件循環(huán),但它非常適合工作池。
將任務劃分為子任務時,較短的任務會擴展為少量的子任務,較長的任務會擴展為更多的子任務,在較長任務的每個子任務之間,分配給它的Worker可以從另一個較短的任務處理子任務,從而提高工作池的整體任務吞吐量。
請注意,已完成的子任務數(shù)量對于工作池的吞吐量而言并不是一個有用的指標,相反,請關注完成的任務數(shù)量。
避免任務分區(qū)回想一下,任務分區(qū)的目的是最小化任務時間的變化,如果你可以區(qū)分較短的任務和較長的任務(例如,匯總數(shù)組與排序數(shù)組),你可以為每個任務類創(chuàng)建一個工作池,將較短的任務和較長的任務路由到多帶帶的工作池是另一種最小化任務時間變化的方法。
支持這種方法,分區(qū)任務會產(chǎn)生開銷(創(chuàng)建工作池任務表示和操作工作池隊列的成本),并且避免分區(qū)可以節(jié)省額外訪問工作池的成本,它還可以防止你在分區(qū)任務時出錯。
這種方法的缺點是,所有這些工作池中的Worker都會產(chǎn)生空間和時間開銷,并且會相互競爭CPU時間,請記住,每個受CPU限制的任務僅在調(diào)度時才進行,因此,你應該在仔細分析后才考慮這種方法。
工作池:結論無論你是僅使用Node工作池還是維護多帶帶的工作池,你都應該優(yōu)化池的任務吞吐量,為此,請使用任務分區(qū)最小化任務時間的變化。
npm模塊的風險雖然Node核心模塊為各種應用程序提供了構建塊,但有時需要更多的東西,Node開發(fā)人員從npm生態(tài)系統(tǒng)中獲益匪淺,數(shù)十萬個模塊提供了加速開發(fā)過程的功能。
但請記住,大多數(shù)這些模塊都是由第三方開發(fā)人員編寫的,并且通常只發(fā)布盡力而為的保證,使用npm模塊的開發(fā)人員應該關注兩件事,盡管后者經(jīng)常被遺忘。
它是否遵循其API?
它的API可能會阻塞事件循環(huán)或Worker嗎?許多模塊都沒有努力表明其API的成本,這對社區(qū)不利。
對于簡單的API,你可以估算API的成本,字符串操作的成本并不難理解,但在許多情況下,尚不清楚API可能會花費多少。
如果你正在調(diào)用可能會執(zhí)行昂貴操作的API,請仔細檢查成本,要求開發(fā)人員記錄它,或者自己檢查源代碼(并提交記錄成本的PR)。
請記住,即使API是異步的,你也不知道它可能花費多少時間在Worker或每個分區(qū)的事件循環(huán)上。例如,假設在上面給出的asyncAvg示例中,對helper函數(shù)的每次調(diào)用將一半的數(shù)字相加而不是其中一個,那么這個函數(shù)仍然是異步的,但是每個分區(qū)的成本都是O(n),而不是O(1),這使得用于任意n值的安全性要低得多。
結論Node有兩種類型的線程:一個事件循環(huán)和k個Worker,事件循環(huán)負責JavaScript回調(diào)和非阻塞I/O,并且Worker執(zhí)行與完成異步請求的C++代碼相對應的任務,包括阻塞I/O和CPU密集型工作,兩種類型的線程一次只能處理一個活動,如果任何回調(diào)或任務需要很長時間,則運行它的線程將被阻塞。如果你的應用程序進行阻塞回調(diào)或任務,則可能導致吞吐量(客戶端/秒)降級最多,并且最壞情況下會導致完全拒絕服務。
要編寫高吞吐量、更多防DoS的Web服務器,你必須確保在良性和惡意輸入上,你的事件循環(huán)和Worker都不會阻塞。
上一篇:Node.js事件循環(huán)、定時器和process.nextTick() 下一篇:Node.js中的定時器文章版權歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/100216.html
摘要:接下來的部分將討論如何確保事件循環(huán)和工作池的公平調(diào)度。不要阻塞事件循環(huán)事件循環(huán)通知每個新客戶端連接并協(xié)調(diào)對客戶端的響應。 你應該閱讀本指南嗎? 如果您編寫比命令行腳本更復雜的程序,那么閱讀本文可以幫助您編寫性能更高,更安全的應用程序。 在編寫本文檔時,主要是基于Node服務器。但里面的原則也適用于其它復雜的Node應用程序。在沒有特別說明操作系統(tǒng)的情況下,默認為Linux。 TL; D...
摘要:中的定時器中的模塊包含在一段時間后執(zhí)行代碼的函數(shù),定時器不需要通過導入,因為所有方法都可以在全局范圍內(nèi)模擬瀏覽器,要完全了解何時執(zhí)行定時器功能,最好先閱讀事件循環(huán)。 Node.js中的定時器 Node.js中的Timers模塊包含在一段時間后執(zhí)行代碼的函數(shù),定時器不需要通過require()導入,因為所有方法都可以在全局范圍內(nèi)模擬瀏覽器JavaScript API,要完全了解何時執(zhí)行定...
Node.js 指南 Node.js?是基于Chrome的V8 JavaScript引擎構建的JavaScript運行時。 常規(guī) 關于Node.js 入門指南 輕松分析Node.js應用程序 Docker化Node.js Web應用程序 遷移到安全的Buffer構造函數(shù) Node.js核心概念 阻塞與非阻塞概述 Node.js事件循環(huán)、定時器和process.nextTick() 不要阻塞事...
摘要:檢索新的事件執(zhí)行與相關的回調(diào)幾乎所有,除了由定時器調(diào)度的一些和將在適當?shù)臅r候在這里阻塞。在事件循環(huán)的每次運行之間,檢查它是否在等待任何異步或定時器,如果沒有,則徹底關閉。 Node.js事件循環(huán)、定時器和process.nextTick() 什么是事件循環(huán)? 事件循環(huán)允許Node.js執(zhí)行非阻塞I/O操作 — 盡管JavaScript是單線程的 — 通過盡可能將操作卸載到系統(tǒng)內(nèi)核。 ...
原文 先說1.1總攬: Reactor模式 Reactor模式中的協(xié)調(diào)機制Event Loop Reactor模式中的事件分離器Event Demultiplexer 一些Event Demultiplexer處理不了的復雜I/O接口比如File I/O、DNS等 復雜I/O的解決方案 未完待續(xù) 前言 nodejs和其他編程平臺的區(qū)別在于如何去處理I/O接口,我們聽一個人介紹nodejs,總是...
閱讀 3646·2021-11-19 09:40
閱讀 3101·2019-08-30 15:54
閱讀 2320·2019-08-30 15:44
閱讀 3199·2019-08-29 15:35
閱讀 3337·2019-08-29 12:22
閱讀 2867·2019-08-28 18:01
閱讀 3148·2019-08-26 13:54
閱讀 910·2019-08-26 12:24