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