極簡 Node.js 入門 - 1.1 Node.js 是什么、性能有優勢?


極簡 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 官網的定義就這么簡單,但也可以看出幾個最重要的特征

  1. Node.js 不是一門語言,是一個運行時,和瀏覽器更像,只不過運行在服務端
  2. 這個運行時的方言是 JavaScript(不包含 BOM、DOM API,增加了 Stream、網絡等 API)
  3. 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,所以有了多線程的性能優化手段,但學習操作系統的時候我們就知道

  1. 操作系統創建線程和切換多和線程上下文需要一定的開銷
  2. 因為多線程帶來的執行堆棧是要占用內存的
  3. 多線程變成面對的死鎖、狀態同步等問題會增加使用的復雜性

上面的代碼要么阻塞整個進程,要么使用了多線程,如果進程不等待 I/O 結果,直接處理后續任務就是非阻塞 I/O,這樣可以不用浪費 CPU

db.query('select * from...', function (result) {
	// 消費 result
});

在 Promise、async|await 沒有的年代,回調是異步的通用處理方式

進程如何獲知異步 I/O 調用完成,觸發回調函數呢?這就要靠 Event Loop 實現,也就是上面提到的事件驅動
image.png

這個圖看起來非常復雜,有幾個要點可以幫助理解

  1. 在 Node.js 中所有操作稱之為事件,客戶端的請求也是事件,所有事件維護在圖中最左側的事件隊列中
  2. Node.js 主線程也就是圖中間的循環就是 Event Loop,主要作用是輪訓事件隊列中是否存在事件
    1. 有非阻塞事件,按照先進先出原則依次調用處理
    2. 有阻塞事件,交給圖中最右側的 C++ 線程池處理,線程池處理完成后把結果通過 Event Loop 返回給事件隊列
    3. 進行下一次循環
  3. 一個請求所有事件都被處理,把響應結果發給客戶端,完成一次請求

這樣一個請求 - 響應模型就完成了,如果在 Event Loop 中包含同步的 CPU 密集操作,就會阻塞主線程

Node.js 性能真的高嗎?

要回答這個問題首先需要了解幾個基本常識

  1. CPU 運算遠遠快於 I/O 操作
  2. Web 是典型的 I/O 密集場景
  3. 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,交給底層完成,然后可以接着處理下一個請求了,快就快在

  1. 非阻塞 I/O
  2. Web 場景 I/O 密集
  3. 沒多線程 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 實現非阻塞模型也會遇到一樣問題

參考

  1. Node.js 作者 Ryan Dahl 介紹 Node.jsRyan Dahl 2009 JSconf - Node.js.pdf
  2. NGINX 如何實現高性能和可擴展性
  3. 深入理解 JavaScript Event Loop


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM