[譯] Node.js 架構概覽

譯者按:
在 Medium 上看到這篇文章,行文脈絡清晰,闡述簡明利落,果斷點下翻譯按鈕。
第一小節背景鋪陳略囉嗦,能夠略過。剛開始我給這部分留了個 blah blah blah 直接翻後面的,翻完以後回頭看,考慮完整性才把第一節給補上。接下來的內容乾貨滿滿,相信對 Node.js 運行機制有興趣的讀者必定會有所收穫。html

原文:Architecture of Node.js’ Internal Codebase
做者:Aren Linode


首先,說點兒 JavaScript……git

StackOverflow 的聯合創始人 Jeff Atwood 在他著名的編程博客 Coding Horror 上說:github

any application that can be written in JavaScript, will eventually be written in JavaScript.
任何能夠用 JavaScript 寫就的應用程序,最終都會以 JavaScript 寫出來。數據庫

JavaScrit 的邊界和影響力在過去幾年裏迅猛發展,如今已是最流行的編程語言之一。2016 年爆棧網的開發者調查中,JavaScript 在最流行技術最熱門問答兩項排名第一,其餘方面也名列前茅。npm

Node.js 是一個服務器端 JavaScript 執行環境,提供了底層服務器功能環境,包括二進制數據操做、文件系統 I/O、數據庫訪問、網絡訪問等。它獨一無二的特性使其在現存的多種成熟服務器語言中脫穎而出,而且通過了業界領先的科技公司如 Paypal、Tinder、Medium(是的,本文原文的那個博客系統)、LinkedIn 和 Netflex 的實戰應用,甚至這些都發生在 Node.js 發佈 1.0 以前。編程

我最近在 StackOverflow 上回答一個關於 Node.js 內部代碼結構的問題,所以而萌生了寫做本文的念頭。設計模式


Node.js 的官方文檔其實講得並不清楚它是什麼:服務器

一個基於 Chrome V8 引擎的 JavaScript 運行時。Node.js 採用事件驅動、非阻塞 I/O 模型……網絡

要理解這段話和它背後的真正力量,咱們須要把 Node.js 拆分到組件,瞭解它們的關鍵技術,如何交互協做,最終構成了 Node.js 這個強大的運行時環境:

Node.js Architecture (High-Level to Low-Level)

組件和第三方依賴

V8:Google 開源的高性能 JavaScript 引擎,以 C++ 實現。這也是集成在 Chrome 中的 JS 引擎。V8 將你寫的 JavaScript 代碼編譯爲機器碼(因此它超級快)而後執行。V8 有多快?看看這個爆棧網的回答

libuv:提供異步功能的 C 庫。它在運行時負責一個事件循環(Event Loop)、一個線程池、文件系統 I/O、DNS 相關和網絡 I/O,以及一些其餘重要功能。

其餘 C/C++ 組件和庫:如 c-arescrypto (OpenSSL)http-parser 以及 zlib。這些依賴提供了對系統底層功能的訪問,包括網絡、壓縮、加密等。

應用/模塊(Application/Modules):這部分就是全部的 JavaScript 代碼:你的應用程序、Node.js 核心模塊、任何 npm install 的模塊,以及你寫的全部模塊代碼。你花費的主要精力都在這部分。

綁定(Bindings):Node.js 用了這麼多 C/C++ 的代碼和庫,簡單來講,它們性能很好。不過,JavaScript 代碼最後是怎麼跟這些 C/C++ 代碼互相調用的呢?這不是三種不一樣的語言嗎?確實如此,並且一般不一樣語言寫出來的代碼也不能互相溝通,沒有 binding 就不行。Binding 是一些膠水代碼,可以把不一樣語言綁定在一塊兒使其可以互相溝通。在 Node.js 中,binding 所作的就是把 Node.js 那些用 C/C++ 寫的庫接口暴露給 JS 環境。這麼作的目的之一是代碼重用:這些功能已經有現存的成熟實現,不必只是由於換個語言環境就重寫一遍,若是橋接調用一下就足夠的話。另外一個緣由是性能:C/C++ 這樣的系統編程語言一般都比其餘高階語言(Python、JavaScript、Ruby 等等)性能更高,因此把主要消耗 CPU 的操做以 C/C++ 代碼來執行更加明智。

C/C++ Addons:Binding 僅橋接 Node.js 核心庫的一些依賴,zlib、OpenSSL、c-ares、http-parser 等。若是你想在應用程序中包含其餘第三方或者你本身的 C/C++ 庫的話,須要本身完成這部分膠水代碼。你寫的這部分膠水代碼就稱爲 Addon。能夠把 Binding 和 Addon 視爲鏈接 JavaScript 代碼和 C/C++ 代碼的橋樑。

術語

I/O:輸入/輸出(Input/Output)的縮寫,基本上代指那些主要由計算機 I/O 子系統處理的操做。重 I/O 操做(I/O-bound operations)一般會牽涉到磁盤或驅動器訪問,例如數據庫訪問或文件系統相關操做。相似的概念還有重 CPU 操做(CPU-bound)、重內存操做(Memory-bound)等等。它們的區分是根據系統哪部分性能對這個操做有最大的影響。好比對於某項操做而言,CPU 運算能力提升能夠帶來最大的提高,這項操做就屬於重 CPU 操做。

非阻塞/異步:當一項請求發來,應用程序會處理這個請求,其餘操做須要等這個請求處理完成才能執行。這個流程的問題是:當大量請求併發時每一個請求都須要等待前一個完成,也就是說每一個請求都會阻塞後面的全部請求,最糟糕的是若是前一個請求花了很長時間(好比從數據庫讀取 3GB 的數據)後面全部請求都跟着悲劇了。解決辦法能夠是引入多處理器和(或)多線程架構,這些辦法各有優劣。Node.js 採用了另外一種方式,再也不爲每一個請求開啓一個新的線程,而是全部請求都在單一的主線程中處理,也只作這麼一件事情:處理請求——請求中包含的 I/O 操做如文件系統訪問、數據庫讀寫等,都會轉發給由 libuv 管理的工做線程去執行。也就是說,請求中的 I/O 操做是異步處理的,而非在主線程上進行。這個辦法就使得主線程從不會阻塞,由於全部耗時的任務都分配到了別處。你須要面對的只有惟一的主線程,全部 libuv 管理的工做線程都與你隔離開來,無需操心,Node.js 會處理好那部分。在這個架構之上重 I/O 操做變得格外高效,那些重 CPU、重內存的也同樣。Node.js 提供了開箱即用的異步 I/O 調度,還有一些針對重 CPU 執行的處理,不過這已經超出本文話題範疇了。

事件驅動:基本上,全部現代系統都是主程序啓動完畢以後,對每一個收到的請求開啓一個進程,接下來根據不一樣技術有不一樣的處理方式,有時差別會截然不同。典型的實現是:針對一個請求開啓一個線程,一步接一步執行任務操做,若是某個操做執行緩慢,這個線程上的後續操做都會隨之掛起,直到全部操做完成,返回結果。而在 Node.js 中,全部的操做都註冊爲一個事件,等待主程序或者外部請求來觸發。

(系統)運行時:Node.js 運行時是指全部這些代碼(上述全部組件,包括底層和上層)提供給 Node.js 應用程序執行的環境。

合體

咱們已經瞭解 Node.js 頂層組件各自的概貌,如今看看它們組合在一塊兒的工做流程,能夠更透徹地理解總體架構以及各部分如何協做交互。

一個 Node.js 應用啓動時,V8 引擎會執行你寫的應用代碼,保持一份觀察者(註冊在事件上的處理函數)列表。當事件發生時,它的處理函數會被加進一個事件隊列。只要這個隊列還有等待執行的事件,事件循環就會持續把事件從隊列中拿出,放進調用堆棧。須要注意的是,只有當前一個事件處理完畢(調用堆棧也已經清空),事件循環纔會把下一個事件放進調用堆棧。

在調用堆棧中,全部的 I/O 請求都會轉發給 libuv 處理。libuv 會維持一個線程池,包含四個工做線程(這是默認數量,也能夠修改配置增長更多工做線程)。文件系統 I/O 請求和 DNS 相關請求都會放進這個線程池處理;其餘的請求,如網絡、平臺特性相關的請求會分發給相應的系統處理單元(參見 libuv 設計概覽)。

安排給線程池的這些 I/O 操做由 Node.js 的底層庫執行,完成以後 libuv 把此事件放回事件隊列,等待主線程執行後續操做。在 libuv 處理這些異步 I/O 操做期間,主線程不會等待處理結果,而是繼續忙其餘事情,只有當事件循環把 libuv 返回的事件放進調用堆棧以後,主線程纔會繼續處理這個事件的後續操做。這就是一個事件在 Node.js 中執行的整個生命週期。

mbp 曾經作過一個巧妙的比喻,把 Node.js 當作一家餐廳。我在此借用下他的例子,稍做修改來闡述下 Node.js 的執行狀況:

把 Node.js 應用程序想象成一家星巴克,一個訓練有素的前臺服務生(惟一的主線程)在櫃檯前接受訂單。當不少顧客同時光臨的時候,他們排隊(進入事件隊列)等候接待;每當服務生接待一位顧客,服務生會把訂單告知給經理(libuv),經理安排相應的專職人員去烹製咖啡(工做線程或者系統特性)。這個專職人員會使用不一樣的原料和咖啡機(底層 C/C++ 組件)按訂單要求製做咖啡或甜點,一般會有四個這樣的專職人員保持在崗待命(線程池),高峯期的時候也能夠安排更多(不過須要在一早就安排人員來上班,而不能中午臨時通知)。服務生把訂單轉交給經理以後不須要等着咖啡製做完成,而是直接開始接待下一位顧客(事件循環放進調用堆棧的另外一個事件),你能夠把當前調用堆棧裏的事件當作是站在櫃檯前正在接受服務的顧客。

當咖啡完成時,會被髮送到顧客隊列的最後位置,等它移動到櫃檯前服務生會叫相應顧客的名字,顧客就來取走咖啡(最後這部分在真實生活中聽起來有點怪,不過你從程序執行的角度理解就比較合乎情理了)。


以上就是 Node.js 的內部頂層組件架構概覽,以及它的事件循環機制。本文依然是很是精簡歸納,還有不少問題和細節沒有展開,如重 CPU 操做的處理、Node.js 設計模式等,將來會有更多文章闡述這些內容(譯註:在 Aren Li 的 Medium 專欄 Yet Another Node.js Blog 裏)。

相關文章
相關標籤/搜索