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