所有你需要知道的關於完全理解 Node.js 事件迴圈及其度量
Node.js 是一個基於事件的平臺。這意味著在 Node 中發生的一切都是基於對事件的反應。通過 Node 的事件處理機制遍歷一系列回撥。
事件的回撥,這一切都由一個名為 libuv 的庫來處理,它提供了一種稱為事件迴圈的機制。
這個事件迴圈可能是平臺中最被誤解的概念。當我們提及事件迴圈監測的主題時,我們花了很多精力來正確地理解我們實際監視的內容。
在本文中,我將帶大家重新認知事件迴圈是如何工作以及它是如何正確地監視。
常見的誤解
Libuv 是向 Node.js 提供事件迴圈的庫。在 libuv 背後的關鍵人物 Bert Belder 的精彩的演講 Node 互動的主題演講 中,演講開頭他使用 Google 影像搜尋展示了各種不同方式描述事件迴圈的圖片,但是他指出大部分圖片描繪的都是錯誤的。
讓我們來看看最流行的誤解。
誤解1:在使用者程式碼中,事件迴圈在單獨的執行緒中執行
誤解
使用者的 JavaScript 程式碼執行在主執行緒上面,而另開一個執行緒執行事件迴圈。每次非同步操作發生時,主執行緒將把工作交給事件迴圈執行緒,一旦完成,事件迴圈執行緒將通知主執行緒執行回撥。
現實
只有一個執行緒執行 JavaScript 程式碼,事件迴圈也執行在這個執行緒上面。回撥的執行(在執行的 Node.js 應用程式中被傳入、後又被呼叫的程式碼都是一個回撥)是由事件迴圈完成地。稍後我們會深入討論。
誤解2:非同步的所有內容都由執行緒池處理
誤解
非同步操作,像操作檔案系統,向外傳送 HTTP 請求以及與資料庫通訊等都是由 libuv 提供的執行緒池處理的。
現實
Libuv 預設使用四個執行緒建立一個執行緒池來完成非同步工作。今天的作業系統已經為許多 I/O 任務提供了非同步介面(例子 AIO on Linux)。
只要有可能,libuv 將使用這些非同步介面,避免使用執行緒池。
這同樣適用於像資料庫這樣的第三方子系統。在這裡,驅動程式的作者寧願使用非同步介面,而不是使用執行緒池。
簡而言之:只有沒有其他方式可以使用時,執行緒池才將會被用於非同步 I/O 。
誤解3:事件迴圈類似棧或佇列
誤解
事件迴圈採用先進先出的方式執行非同步任務,類似於佇列,當一個任務執行完畢後呼叫對應的回撥函式。
現實
雖然涉及到類似佇列的結構,事件迴圈並不是採用棧的方式處理任務。事件迴圈作為一個程式被劃分為多個階段,每個階段處理一些特定任務,各階段輪詢排程。
瞭解事件迴圈週期的階段
為了真正地瞭解事件迴圈,我們必須明白各個階段都完成了哪些工作。 希望 Bert Belder 不介意,我直接拿了他的圖片來說明事件迴圈是如何工作的:
事件迴圈的執行可以分成 5 個階段,讓我們來討論這些階段。更加深入的解釋見 Node.js 官網
計時器
通過 setTimeout() 和 setInterval() 註冊的回撥會在此處處理。
IO 回撥
大部分回撥將在這部分被處理。Node.js 中大多數使用者程式碼都在回撥中處理(例如,對傳入的 http 請求觸發級聯的回撥)。
IO 輪詢
對接著要處理的的事件進行新的輪詢。
Immediate 設定
此處處理所有由 setImmediate() 註冊的回撥。
結束
這裡處理所有‘結束’事件的回撥。
監測事件迴圈
我們看到,事實上在 Node 應用程式中進行的所有事件都將通過事件迴圈執行。這意味著如果我們可以從中獲得指標,相應地我們可以分析出有關應用程式整體執行狀況和效能的寶貴資訊。
沒有現成的 API 可以從事件迴圈中獲取執行時指標,因此每個監控工具都提供自己的指標,讓我們來看看都有些什麼。
記錄頻率
每次的記錄數。
記錄持續時間
一個刻度的時間。
由於我們的代理作為本機模組執行,因此這是比較容易地新增探測器為我們提供這些資訊。
記錄頻率以及記錄持續事件指標
當我們在不同的負載下進行第一次測試時,結果令人驚訝 – 讓我舉例說明一下:
在以下情況下,我正在呼叫一個 express.js 應用程式,對其他 http 伺服器進行外撥呼叫。
有以下 4 中情況:
- Idle
沒有傳入請求
- ab -c 5
使用 apache bench 工具我一次建立了 5 個併發請求
- ab -c 10
一次 10 個併發請求
- ab -c 10 (slow backend)
為了模擬出一個很慢的後端,我們讓被呼叫的 http 伺服器在 1s 後返回資料。這樣造成請求等待後端返回資料,被堆積在 Node 中,產生背壓。
事件迴圈執行階段
如果我們看看得到的圖表,我們可以做一個有趣的觀察:
事件迴圈持續時間和被動態調整頻率
如果應用程式處於空閒狀態,這意味著沒有執行任何任務(定時器、回撥等),此時全速執行這些階段是沒有意義的,事件迴圈就這種情況會在在輪詢階段阻塞一段時間以等待新的外部事件進入。
這也意味著,無負載下的度量(低頻,高持續時間)與在高負載下與慢後端相關的應用程式相似。
我們還看到,該演示應用程式在場景中執行得“最好”的是併發 5 個請求。
因此,標記頻率和標記持續時間需要基於每秒併發請求量進行度量。
雖然這些資料已經為我們提供了一些有價值的見解,但我們仍然不知道在哪個階段花費時間,因此我們進一步研究並提出了另外兩個指標。
工作處理延遲
這個度量衡量執行緒池處理非同步任務所需的時間。
高工作處理的延遲表示一個繁忙/耗盡的執行緒池。
為了測試這個指標,我建立了一個使用 Sharp 的模組來處理影像的 express 路由。 由於影像處理開銷太大,Sharp 利用執行緒池來實現。
通過 Apache bench 發起 5 個併發請求到具有影像處理功能的路由與沒有使用圖片處理的路由有很大不同,可以直接從圖表上可以看到。
事件迴圈延遲
事件迴圈延遲測量在通過 setTimeout(X) 排程的任務真正得到處理之前需要多長時間。
事件迴圈高延遲表示事件迴圈正忙於處理回撥。
為了測試這個指標,我建立了一個 express 路由使用了一個非常低效的演算法來計算斐波那契。
執行具有 5 個併發連線的 Apache bench,具有計算斐波那契功能的路由顯示此刻回撥佇列處於繁忙狀態。
我們清楚地看到,這四個指標可以為我們提供寶貴的見解,並幫助您更好地瞭解 Node.js 的內部工作。
這些需求仍然需要在更大的圖片中去觀察,以使其有意義。因此,我們正在收集資訊以將這些資料納入我們的異常檢測。
回到事件迴圈
當然,在不瞭解如何從可能的行動中解決問題的情況下,衡量標準本身就不會有太大的幫助。當事件迴圈快耗盡時,這裡有幾個提示。
事件迴圈耗盡
利用所有 CPU
Node.js 應用程式在單個執行緒上執行。在多核機器上,這意味著負載不會分佈在所有核心上。使用 Node 附帶的 cluster module 可以輕鬆地為每個 CPU 生成一個子程式。每個子程式維護自己的事件迴圈,主程式在所有子程式之間透明地分配負載。
調整執行緒池
如上所述,libuv 將建立一個大小為 4 的執行緒池。通過設定環境變數 UV_THREADPOOL_SIZE 可以覆蓋執行緒池的預設大小。
雖然這可以解決 I/O 繫結應用程式上的負載問題,我建議多次負載測試,因為較大的執行緒池可能仍然耗盡記憶體或 CPU 。
將任務扔給服務程式
如果 Node.js 花費太多時間參與 CPU 繁重的操作,開一些服務程式處理這些繁重任務或者針對某些特定任務使用其它語言編寫服務也是一個可行的選擇。
總結
我們總結一下我們在這篇文章中學到的內容:
- 事件迴圈是使 Node.js 應用程式執行的原因
- 它的功能經常被誤解 – 它有多個階段組成,各階段處理特定任務,階段間輪詢排程
- 事件迴圈不提供現成的指標,因此收集的指標在 APM 供應商之間是不同的
- 這些指標清楚地提供了有關瓶頸的有價值的見解,但對事件迴圈的深刻理解以及正在執行的程式碼才是關鍵
- 在未來,Dynatrace 將會把事件迴圈新增到第一檢測要素,從而將事件迴圈異常與問題相關聯
對我來說,毫無疑問,我們今天剛剛在市場上構建了最全面的事件迴圈監控解決方案,我非常高興在未來幾個星期內,這個驚人的新功能將推向所有客戶。
最後
我們一流的 Node.js 代理團隊為了做好事件迴圈監控盡了很大努力。這篇部落格文章中提出的大部分發現都是基於他們對 Node.js 內部運作的深入瞭解。 我要感謝 Bernhard Liedl ,Dominik Gruber ,GerhardStöbich 和 Gernot Reisinger 所有的工作和支援。
我希望這篇文章使大家在事件迴圈上有新的認知。請在 Twitter 上關注我 @dkhan。我很樂意回答您在 Twitter 裡或下面評論區中的提出的一切問題。
最後和以往一樣:下載免費試用版去監控您的完整堆疊,包括Node.js。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、React、前端、後端、產品、設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃。