在過去的五個月里,我一直在我當前的項目中使用 Spring Webflux,我還編寫了很多 Nodejs 應用程序,并且使用 Promise 樣式編碼(async/await)的方式也幾乎相同,而 Webflux 使用 Mono/Flux。
Nodejs 和 Spring Webflux 之間的區別是什么,因為它們都在解決同一個問題,專注于事件循環。
Node.js 是一個構建基于 Chrome V8 JAVAScript 引擎的事件驅動服務器應用程序的平臺。
讓我們看看基于以下幾點的比較,了解設計方法和內部發生了什么不同之處?
線程模型
在決定任何框架之前,需要了解線程建模。
Nodejs:
- Node.js并不是單純的單線程,它用主線程處理所有請求,然后對I/O操作進行異步處理,交給其他線程去執行,避免了頻繁創建、銷毀和上下文切換帶來的系統開銷。下面來看Node.js的工作原理。
從左到右,從上到下,Node.js 被分為了四層,分別是 應用層、V8引擎層、Node API層 和 LIBUV層。
應用層: 即 JavaScript 交互層,常見的就是 Node.js 的模塊,比如 http,fs
V8引擎層: 即利用 V8 引擎來解析JavaScript 語法,進而和下層 API 交互
NodeAPI層: 為上層模塊提供系統調用,一般是由 C 語言來實現,和操作系統進行交互
LIBUV層: 是跨平臺的底層封裝,實現了 事件循環、文件操作等,是 Node.js 實現異步的核心
Node.js在主線程維護了一個事件隊列,接收到請求后,就將該請求作為一個事件放入Event Queue中,然后繼續接受其他請求,當主線程空閑(沒有請求接收) 的時候,就開始輪詢事件隊列
Webflux:
- 接收客戶端連接是一個獨立的線程池。Acceptor接收到客戶端TCP連接請求處理完成后(可能包含接入認證等),將新創建的SocketChannel注冊到I/O線程池(sub reactor線程池)的某個I/O線程上,由它負責SocketChannel的讀寫和編解碼工作。
- Acceptor線程池只用于客戶端的登錄、握手和安全認證,一旦鏈路建立成功,就將鏈路注冊到后端subReactor線程池的I/O線程上,有I/O線程負責后續的I/O操作。
異步操作
Nodejs:
- Nodejs 內部使用 Libuv 來處理異步任務。它將現代內核所能做的盡可能多的調度到操作系統內核。
- 如果 Libuv 無法將任務委托給內核,則它使用其創建的線程池(默認 4 個線程)來處理工作。
Webflux:
- 在 Netty 4 中,所有 I/O 操作和事件都由分配給事件循環的同一線程處理。
- 而在 Netty 3 中,入站事件有一個單獨的事件循環,在 I/O 線程池中處理,出站事件可能在 I/O 線程池或另一個池中。
事件循環結構
Nodejs:
- Event Loop 有多個階段來處理事件,它們是計時器、掛起回調、空閑和準備、輪詢、檢查和關閉回調。
Webflux:
- 事件循環有它的任務隊列。
- 每當應用程序收到新請求時,它都會存儲在 Java 堆中,其?中一個事件循環將從 Java 堆中選擇它并進行處理。
事件循環中的任務調度
Nodejs:
- 所有通過 setTimeout() 或 setInterval() 調度的內容都將在事件循環的計時器階段進行處理。
Webflux:
- 我們可以使用事件循環來調度任務。這里的事件循環繼承ScheduledExecutorService執行線程池管理。
- 如果我們直接使用 ScheduledExecutorService,那么在高負載下,這會帶來性能成本,并且如果任務被頻繁調度,可能會成為瓶頸。
CPU 利用率
Nodejs:
- Node.js 應用程序在單個線程上運行。在多核機器上,這意味著負載不會分布在所有內核上。
- 使用 Node 附帶的集群模塊,可以很容易地為每個 CPU 生成一個子進程。
- 每個子進程都維護自己的事件循環,主進程透明地在所有子進程之間分配負載。
Webflux:
- 如果我們需要運行一個長時間運行的任務,那么最好創建一個單獨的線程執行器池并在那里處理它。事件循環稍后可以選擇返回的結果,避免事件循環解除對長任務的阻塞。
- 我們還可以增加事件循環實例來提高 CPU 利用率。
調整線程池
Nodejs:
- Libuv 會創建一個大小為 4 的線程池。
- 可以通過設置環境變量 UV_THREADPOOL_SIZE 來覆蓋池的默認大小。
Webflux:
- 事件循環線程池的默認大小是可用處理器的兩倍。
- 可以根據需要修改池大小。
處理背壓問題
軟件系統中的背壓是使流量通信過載的能力。換句話說,信息流的生產速度超過消費速度。
Nodejs:
- 被調用的 HTTP 服務器在 1s 后返回數據以模擬慢速后端。當等待后端返回的請求在 Node 內部堆積時,可能會導致背壓。
- 為了在流中實現背壓,我們可以使用具有高水位標記的可讀可寫流來有效地處理數據生產者和消費者之間的背壓。
webflux:
- 背壓的責任由 Project Reactor 管理。它在內部使用 Flux 功能控制發射器產生的事件。
- Webflux 使用 TCP 流量控制來調節背壓。
- Flux 中提供了三種方法,我們可以使用它們來控制背壓。
- 選項 1:使用request(),消費者可以控制讓發布者等到它收到新事件的請求。簡而言之,消費者訂閱事件并根據需求進行處理。
- 選項 2:使用limitRange(),我們正在設置一次預取的項目數。即使消費者請求處理更多事件,該限制也適用。發布者將事件分成塊,避免消耗超過每個請求的限制。
- 選項 3:使用cancel(),消費者可以隨時取消要接收的事件。我們可以取消訂閱并稍后再次訂閱以繼續接收下一個事件。
- 為了處理客戶端和服務器之間的背壓,我們可以Channel.isWritable() 通過調用 Channel.write()來檢查是等待還是發送下一個事件,或者我們也可以列出fireChannelWritabilityChanged事件來決定何時向通道發送更多數據.
基于以上幾點,我認為沒有一個比另一個更好,因為兩者都有一些優點和缺點。但在大多數方面,它們在性能方面是相同的。因此,可以根據技能可用性、團隊技術方向、項目生態系統等來做出決定。