極簡 Node.js 入門系列教程:https://www.yuque.com/sunluyong/node
本文更佳閱讀體驗:https://www.yuque.com/sunluyong/node/what-is-node
定義
Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine.
現在 Node.js 官網的定義就這么簡單,但也可以看出幾個最重要的特征
- Node.js 不是一門語言,是一個運行時,和瀏覽器更像,只不過運行在服務端
- 這個運行時的方言是 JavaScript(不包含 BOM、DOM API,增加了 Stream、網絡等 API)
- Node.js 是靠 Chrome V8 引擎運行 JavaScript
對應到 Java 我們可以理解 Node.js 是 JDK,裝上就能在服務端跑 JavaScript 代碼了。
Chrome 和 Node.js 同樣是 JavaScript 運行時,都使用了 V8 引擎,主要區別在於 V8 只實現了 ECMAScript 的數據類型、對象和方法,Chrome 運行時提供了 Window、DOM、BOM,而 Node.js 運行時提供了global、 Buffer、net 等模塊
下面內容需要一些計算機基礎知識,但看不懂並不影響 Node.js 的學習
事件驅動 & 非阻塞 I/O 是什么
在 Node.js 才誕生的時候大家總是充滿了好奇,早期官網上的介紹要更多一些,主要說了 Node 最核心的兩個特性:事件驅動、非阻塞 I/O
Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient. Node.js' package ecosystem, npm, is the largest ecosystem of open source libraries in the world.
舉個例子理解 Node.js 和之前大部分 web 應用編程區別,當讀取數據庫的時候會寫出這樣的代碼
var result = db.query('select * from...');
I/O 是一個相對耗時較長的工作(這是后面討論的前提),I/O 任務主要由 CPU 分發給 DMA 執行,等待數據庫查詢結果的時候進程在做什么?大部分時候就是單純在等着而已
不同硬件設備 I/O 操作所花費 CPU cycles
Action Cost (CPU cycles)
L1 Cache* 3
L2 Cache* 14
RAM* 250
Disk 41,000,000
Network 240,000,000
這明顯是在浪費 CPU,所以有了多線程的性能優化手段,但學習操作系統的時候我們就知道
- 操作系統創建線程和切換多和線程上下文需要一定的開銷
- 因為多線程帶來的執行堆棧是要占用內存的
- 多線程變成面對的死鎖、狀態同步等問題會增加使用的復雜性
上面的代碼要么阻塞整個進程,要么使用了多線程,如果進程不等待 I/O 結果,直接處理后續任務就是非阻塞 I/O,這樣可以不用浪費 CPU
db.query('select * from...', function (result) {
// 消費 result
});
在 Promise、async|await 沒有的年代,回調是異步的通用處理方式
進程如何獲知異步 I/O 調用完成,觸發回調函數呢?這就要靠 Event Loop 實現,也就是上面提到的事件驅動
這個圖看起來非常復雜,有幾個要點可以幫助理解
- 在 Node.js 中所有操作稱之為事件,客戶端的請求也是事件,所有事件維護在圖中最左側的事件隊列中
- Node.js 主線程也就是圖中間的循環就是 Event Loop,主要作用是輪訓事件隊列中是否存在事件
- 有非阻塞事件,按照先進先出原則依次調用處理
- 有阻塞事件,交給圖中最右側的 C++ 線程池處理,線程池處理完成后把結果通過 Event Loop 返回給事件隊列
- 進行下一次循環
- 一個請求所有事件都被處理,把響應結果發給客戶端,完成一次請求
這樣一個請求 - 響應模型就完成了,如果在 Event Loop 中包含同步的 CPU 密集操作,就會阻塞主線程
Node.js 性能真的高嗎?
要回答這個問題首先需要了解幾個基本常識
- CPU 運算遠遠快於 I/O 操作
- Web 是典型的 I/O 密集場景
- JavaScript 是單線程,但 JavaScript 的 runtime Node.js 並不是,負責 Event Loop 的 libuv 用 C 和 C++ 編寫
很多語言是依賴的多線程解決高並發,一個線程處理一條用戶請求,處理完成了釋放線程,在阻塞 I/O 模型下, I/O 期間該用戶線程所占用的 CPU 資源(雖然十分微量,大部分交給了 DMA)什么都不做,等待 I/O,然后響應用戶,而且開啟多個進程/線程 CPU 切換 Context 的時間也十分可觀
就像飯店的服務員只負責點菜,如果給每個廚師都配一個服務員,服務員把客人菜單給大廚后就玩手機等着一樣,你是老板你也生氣,況且不同於飯店大廚工資高於服務員,在計算機世界,CPU 資源比 I/O 寶貴的多
說 Node.js 在高並發、I/O 密集場景性能高,也就是 Web 場景性能高主要也是解決這個問題,沒必要一個廚師配一個服務員,整個飯店說不定一個服務員就夠了,剩下的錢可以隨便做其它事情
用戶請求來了, CPU 的部分做完不用等待 I/O,交給底層完成,然后可以接着處理下一個請求了,快就快在
- 非阻塞 I/O
- Web 場景 I/O 密集
- 沒多線程 Context 切換開銷,多出來的開銷是維護 EventLoop
其它場景 NodeJS 性能確實不高,甚至非常低下,感興趣可以看一下 Apache(多進程) 和 Nginx(事件驅動) 對比,現在大型 web 應用普遍是 Nginx 在最前面做負載均衡服務器、靜態資源服務器,Apache 在下一層做實際 Web Server,響應動態請求
因此 Node.js 在 I/O 密集的 Web 場景相對於使用多進程模型語言有性能優勢,這個優勢不是來源於語言,而是操作系統實現,Java 按照這種模型實現性能一樣很高
得益於 V8 的優化和 C/C++ 拓展,Node.js 執行 CPU 密集任務性能並不差,但如果長時間進行 CPU 運算會阻塞后續 I/O 任務發起,用 Java 實現非阻塞模型也會遇到一樣問題
參考
- Node.js 作者 Ryan Dahl 介紹 Node.jsRyan Dahl 2009 JSconf - Node.js.pdf
- NGINX 如何實現高性能和可擴展性
- 深入理解 JavaScript Event Loop