關於 Spring-WebFlux 的一些想法

乾貨滿滿張雜湊發表於2022-01-03

本文是本人在知乎提問 spring webflux現在看來是否成功? 下的回答,其他回答也很精彩,如果感興趣可以檢視

現在基於 spring web 的同步微服務有一個非常大的缺陷就是:相對於基於 spring-webflux 的非同步微服務,基於 spring-web 的同步微服務沒有很好的處理客戶端有請求超時配置的情況。當客戶端請求超時時,客戶端會直接返回超時異常,但是呼叫的服務端任務,在基於 spring-web 的同步微服務並沒有被取消,基於 spring-webflux 的非同步微服務是會被取消的。目前,還沒有很好的辦法在同步環境中可以取消這些已經超時的任務。

Spring-weflux 目前最大的問題,在於很多框架,包括 JDK 本身,有很多基於 Thread 的 Context,例如 Thread local 這種。還有就是 Java Log 框架的 MDC 的實現,一般都是基於 ThreadLocal 的 Map.還有就是像 redisson 的分散式鎖,它讓每個執行緒生成唯一id並和執行緒繫結,解鎖的時候會檢查。 但是這種設計,與 Spring-Webflux 的 Context 很難相容。可以看看 Spring cloud sleuth 在 Spring-Webflux 中加入鏈路資訊上下文,並保持,有多麻煩,而且,還有不少的 bug 和漏掉的點,參考:

還有一點比較麻煩,就是和現有的各種阻塞鎖的設計,不相容,因為響應式程式設計需要非阻塞。這需要重構成佇列排序消費來解決併發競爭,工作量也很大。

然後就是官方資料庫 IO 庫,不是 NIO 這個問題。不論是Java自帶的Future框架,還是 Spring WebFlux,還是 Vert.x,他們都是一種非阻塞的基於Ractor模型的框架(後兩個框架都是利用netty實現)。在阻塞程式設計模式裡,任何一個請求,都需要一個執行緒去處理,如果io阻塞了,那麼這個執行緒也會阻塞在那。但是在非阻塞程式設計裡面,基於響應式的程式設計,執行緒不會被阻塞,還可以處理其他請求。舉一個簡單例子:假設只有一個執行緒池,請求來的時候,執行緒池處理,需要讀取資料庫 IO,這個 IO 是 NIO 非阻塞 IO,那麼就將請求資料寫入資料庫連線,直接返回。之後資料庫返回資料,這個連結的 Selector 會有 Read 事件準備就緒,這時候,再通過這個執行緒池去讀取資料處理(相當於回撥),這時候用的執行緒和之前不一定是同一個執行緒。這樣的話,執行緒就不用等待資料庫返回,而是直接處理其他請求。這樣情況下,即使某個業務 SQL 的執行時間長,也不會影響其他業務的執行。但是,這一切的基礎,是 IO 必須是非阻塞 IO,也就是 NIO(或者 AIO)。官方JDBC沒有 NIO,只有 BIO 實現。這樣無法讓執行緒將請求寫入連結之後直接返回,必須等待響應。但是也就解決方案,就是通過其他執行緒池,專門處理資料庫請求並等待返回進行回撥,也就是業務執行緒池 A 將資料庫 BIO 請求交給執行緒池B處理,讀取完資料之後,再交給 A 執行剩下的業務邏輯。這樣A也不用阻塞,可以處理其他請求。但是,這樣還是有因為某個業務 SQL 的執行時間長,導致B所有執行緒被阻塞住佇列也滿了從而A的請求也被阻塞的情況,這是不完美的實現。真正完美的,需要 JDBC 實現 NIO。當然,也可以使用其他非同步響應式的三方庫,但是非官方的,相容性以及是否更新及時,還有使用限制什麼的也很麻煩。

最後,提一下 Java 本身的 Project Loom,我簡單研究過他的實現原理:

簡單總結即:在虛擬執行緒中執行的 Java 同步網路 API 會將底層原生 Socket 切換到非阻塞模式。當 Java 程式碼啟用一個 I/O 請求並且這個請求沒有立即完成(原生 socket 返回 EAGAIN - 代表"未就緒"/"會阻塞")的時候,這個底層 socket 會被註冊到一個 JVM 內部事件通知機制(Poller),並且虛擬執行緒會被 parked。當底層 I/O 操作就緒的時候(有相關事件會到達 Poller),虛擬執行緒會 unparked 並且底層的 Socket 操作會被重試處理。同步 Java 網路 API 已經被重新實現,相關的 JEP 包括 JEP 353 和 JEP 373. 在虛擬執行緒中執行時,不能立即完成的 I/O 操作將導致虛擬執行緒被 parked 。當 I/O 就緒時,虛擬執行緒將被 unparked。這個實現相對於當前的非同步非阻塞 I/O 實現程式碼來看,更加簡單易用,隱藏了很多業務不關心的實現細節。

Project Loom 解決了主要的網路 IO 阻塞問題,並且基本不用改現有程式碼就能實現纖程,用阻塞的程式碼風格實現非阻塞的程式碼(而且和現在的基於 Thread 的上下文框架相容)。是目前的 Java 架構師 Goetz 最看重的特性之一,目前 Java 17 的很多新特性其實就是為這個 Project Loom 的釋出鋪路,可以看看 Nicolai 和 Goetz 大神的這個視訊,從 19:17 秒開始:

Brian Goetz: "I think Project Loom is going to kill Reactive Programming"(哈哈,有點過於樂觀帶節奏了,不過值得觀望)

不過,雖然期望中是僅需要下面這種程式碼就可以把現有的執行緒和執行緒池替換成虛擬執行緒:

Thread thread = Thread.ofVirtual().name("duke").unstarted(runnable);
ThreadFactory factory = Thread.ofVirtual().factory();

ExecutorService b = Executors.newVirtualThreadPool();

但是還有很多問題需要解決:

  1. ThreadLocal 相關的類,由於虛擬執行緒會無限制的生成,ThreadLocal 的生成也需要重新設計:首先是很多 JDK 中的框架基於 ThreadLocal 的 Probe 實現,例如 ThreadLocalRandom 的初始 Seed。第二是 ThreadLocal 的使用可能會導致 GC 壓力增大,因為虛擬執行緒可以無限制的生成。
  2. 依然阻塞實際執行緒的地方:在同步鎖的阻塞還是會阻塞實際的執行緒,還有檔案 IO 等。
  3. 修改以上帶來的 bug 以及安全問題,由於這些修改動了 JDK 的一些框架的根本,沒有經過實際線上應用之前,僅憑單元測試和壓測可能很難發現一些細節問題。

不過,現在的 Java 已經在為 Project loom 鋪路了,例如:

微信搜尋“我的程式設計喵”關注公眾號,每日一刷,輕鬆提升技術,斬獲各種offer

相關文章