簡體   English   中英

Node.js 架構和性能

[英]Node js architecture and performance

我有一個關於 Node js 的架構和性能的問題。

我已經閱讀了很多關於這個主題的文章(包括 Stack Overflow),但我仍然有幾個問題。 我想做兩件事:

  1. 半簡潔地總結我從爬取許多不同來源中學到的東西,看看我的結論是否正確。
  2. 問幾個關於 Node 的線程和性能的問題,我無法從我的研究中確定確切的答案。

Node 具有單線程、異步事件處理架構

單線程- 有一個單獨的事件線程調度異步工作(結果通常是 I/O,但可以是計算)並執行回調執行(即處理異步工作結果)。

  • 事件線程在一個無限的“事件循環”中運行,完成上面的 2 個工作; a) 通過分派異步工作來處理請求,以及 b) 注意到先前的異步工作結果已准備就緒並執行回調來處理結果。

  • 這里常見的類比是餐廳點餐員:事件線程是一個超快的服務員,從餐廳接受訂單(服務請求)並將訂單交付到廚房准備(調度異步工作),但也通知當食物准備好(異步結果)並將其送回餐桌(回調執行)。

  • 服務員不做任何食物; 他的工作是盡快從餐廳來回廚房。 如果他在餐廳接受訂單時陷入困境,或者如果他被迫回到廚房准備其中一頓飯,系統就會變得效率低下,系統吞吐量也會受到影響。

異步請求(例如 Web 請求)產生的異步工作流在邏輯上是一個鏈:例如

   FIRST [ASYNC: read a file, figure out what to get from the database] THEN 
   [ASYNC: query the database] THEN 
   [format and return the result].

上面標有“ASYNC”的工作是“廚房工作”,“FIRST []”和“THEN []”代表服務員參與發起回調。

像這樣的鏈以 3 種常見方式以編程方式表示:

  • 嵌套函數/回調

  • 用 .then() 鏈接的 promise

  • 對異步結果進行 await() 的異步方法。

所有這些編碼方法幾乎是等效的,盡管 asynch/await 似乎是最干凈的並且使有關異步編碼的推理更容易。

這是我對正在發生的事情的心理想象……它是正確的嗎? 非常感謝評論!

問題

我的問題涉及操作系統支持的異步操作的使用,誰實際執行異步工作,以及該架構比“每個請求生成一個線程”(即多個廚師)架構的性能更高的方式:

  1. 通過使用跨平台異步庫 libuv,節點庫已被設計為異步的,對嗎? 這里的想法是 libuv 為節點(在所有平台上)提供一致的異步 I/O 接口,然后在引擎蓋下使用依賴於平台的異步 I/O 操作嗎? 在 I/O 請求“一直向下”到 OS 支持的異步操作的情況下,誰在“做”等待 I/O 返回並觸發節點的工作? 它是內核,使用內核線程嗎? 如果不是,是誰? 無論如何,這個實體可以處理多少個請求?

  2. 我讀過 libuv 也在內部使用線程池(通常是 pthreads,每個內核一個?)。 這是為了將不會“一路向下”的操作“包裝”為異步,以便可以使用線程坐下來等待同步操作,從而使 libuv 可以提供異步 API?

  3. 關於性能,用於解釋類似節點的架構可以提供的性能提升的通常說明是:想象(可能更慢和更胖)線程每個請求的方法——產生延遲、CPU 和內存開銷一堆線程只是坐在等待 I/O 完成(即使它們不忙於等待)然后將它們拆除,node 在很大程度上使這種情況消失,因為它使用了一個長期存在的事件線程來將異步 I/O 分派到操作系統/內核,對嗎? 但是在一天結束時,某些東西在互斥鎖上睡覺並在 I/O 准備好時被喚醒……是不是內核比用戶線程更有效率? 最后,請求由libuv的線程池處理的情況如何……除了使用池的效率(避免啟動和拆卸)之外,這似乎類似於每個請求的線程方法,但在這種情況下,當有很多請求並且池有積壓時會發生什么?......延遲增加,現在你的表現比每個請求的線程差,對吧?

這里有關於 SO 的很好的答案,可以讓您更清晰地了解架構。 但是,您有一些可以回答的具體問題。

誰在“做”等待 I/O 返回並觸發節點的工作? 它是內核,使用內核線程嗎? 如果不是,是誰? 無論如何,這個實體可以處理多少個請求?

實際上,線程和異步 I/O 都是在同一個原語之上實現的:操作系統事件隊列。

多任務操作系統的發明是為了允許用戶使用單個 CPU 內核並行執行多個程序。 是的,當時確實存在多核、多線程系統,但它們很大(通常有兩到三間普通卧室的大小)且價格昂貴(通常是一兩間普通房屋的成本)。 這些系統可以在沒有操作系統幫助的情況下並行執行多個操作。 您所需要的只是一個簡單的加載程序(稱為執行程序,一種原始的類似 DOS 的操作系統),您可以在沒有操作系統幫助的情況下直接在程序集中創建線程。

更便宜、更大規模生產的計算機一次只能運行一件事。 長期以來,這是用戶可以接受的。 然而,習慣了分時系統的人希望從他們的計算機中獲得更多。 因此發明了進程和線程。

但是在操作系統級別沒有線程。 操作系統本身提供線程服務(嗯……從技術上講,您可以將線程作為庫來實現,而無需操作系統支持)。 那么操作系統是如何實現線程的呢?

中斷。 它是所有異步處理的核心。

進程或線程只是一個等待 CPU 處理並由操作系統管理的事件。 這是可能的,因為 CPU 硬件支持中斷。 任何等待 I/O 事件(來自鼠標、磁盤、網絡等)的線程或進程都會被停止、暫停並添加到事件隊列中,並且在等待時間內執行其他進程或線程。 CPU 中還內置了一個可以觸發中斷的定時器(令人驚訝的是,這種中斷被稱為定時器中斷)。 這個定時器中斷會觸發操作系統的進程/線程管理系統,這樣即使沒有一個進程在等待 I/O 事件,您仍然可以並行運行多個進程。

這是多任務處理的核心。 除了操作系統設計、嵌入式編程(你經常需要在沒有操作系統的情況下做類似操作系統的事情)和實時編程之外,通常不會教授這種編程(使用定時器和中斷)。

那么,異步 I/O 和進程之間有什么區別?

除了操作系統向程序員公開的 API 之外,它們完全相同:

  • 進程/線程:嘿,程序員,假設您正在為單個 CPU 編寫一個簡單的程序,並假設您可以完全控制 CPU。 來吧,使用我的 I/O。 當我處理並行運行的混亂時,我會保持你控制 CPU 的錯覺。

  • 異步I/O :你認為你比我更了解? 好的,我讓您直接將事件偵聽器添加到我的內部隊列中。 但我不打算處理事件發生時調用哪個函數。 我只是粗魯地喚醒你的過程,你自己處理所有這些。

在多核 CPU 的現代世界中,操作系統仍然執行這種進程管理,因為典型的現代操作系統運行數十個進程,而 PC 通常只有兩個或四個內核。 多核機器還有另一個區別:

  • 進程/線程:由於我正在為您處理進程隊列,我想您不會介意我分散您要求我在多個 CPU 上運行的線程的負載吧? 這樣我會讓硬件並行完成工作。

  • 異步 I/O :抱歉,我無法將所有不同的等待回調分布在不同的 CPU 上,因為我不知道你的代碼到底在做什么。 單核給你!

我讀過 libuv 也在內部使用線程池(通常是 pthreads,每個內核一個?)。 這是為了將不會“一直向下”的操作“包裝”為異步嗎?

是的。

實際上,據我所知,所有操作系統都提供了足夠好的異步 I/O 接口,您不需要線程池。 自 80 年代以來,編程語言Tcl一直在處理異步 I/O 之類的節點,而無需線程池的幫助。 但它非常凌亂,並不那么簡單。 Node 開發人員決定,當涉及到磁盤 I/O 時,他們不想處理這種混亂,而只是將經過充分測試的阻塞文件 API 與線程一起使用。

但是在一天結束時,某些東西正在互斥鎖上睡覺並在 I/O 准備好時被喚醒

我希望我對(1)的回答也能回答這個問題。 但是,如果您想知道那是什么,我建議您閱讀 C 中的select()函數。如果您了解 C 編程,我建議您嘗試使用select()編寫沒有線程的 TCP/IP 程序。 谷歌“選擇c”。 我在另一個答案中更詳細地解釋了這一切是如何在 C 級別工作的: 我知道回調函數異步運行,但為什么呢?

當有很多請求並且池有積壓時會發生什么?......延遲增加,現在你的表現比線程每個請求更糟糕,對吧?

我希望一旦你理解了我對 (1) 的回答,你也會意識到即使你使用線程也無法擺脫積壓。 硬件並不真正支持操作系統級線程。 硬件線程受限於內核數量,因此在硬件級別,CPU 是一個線程池。 單線程和多線程的區別很簡單,多線程程序真正可以在硬件中並行執行多個線程,而單線程程序只能使用單個CPU。

異步 I/O 和傳統多線程程序之間唯一真正的區別是線程創建延遲。 從這個意義上說,像 node.js 這樣的程序比使用像 nginx 和 apache2 這樣的線程池的程序沒有任何優勢。

但是,由於 CGI 的工作方式,像 node.js 這樣的程序仍然具有更高的吞吐量,因為您不必為每個請求重新初始化解釋器和程序中的所有對象。 這就是為什么大多數語言已經轉移到作為 HTTP 服務(如 node 的 Express.js)或 FastCGI 之類的東西運行的 Web 框架。


注意:你真的想知道線程創建延遲有什么大不了的嗎? 在 90 年代末/ 2000 年代初,有一個 Web 服務器基准測試。 Tcl 是一種眾所周知的平均比 C 慢 500% 的語言(因為它基於像 bash 這樣的字符串處理)設法勝過 apache(這是在 apache2 之前並觸發了創建 apache2 的完整重新架構)。 原因很簡單:tcl 有很好的異步 I/O api,所以程序員更有可能使用異步 I/O。 僅此一項就擊敗了用 C 編寫的程序(不是說 C 沒有異步 I/O,畢竟 tcl 是用 C 編寫的)。

node.js 相對於 Java 等語言的核心優勢不在於它具有異步 I/O。 正是異步 I/O 無處不在,而且 API(回調、承諾)易於使用,因此您可以使用異步 I/O 編寫整個程序,而無需下拉到匯編或 C。

如果您認為回調很難使用,我強烈建議您嘗試用 C 編寫基於select()的程序。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM