閒談軟體設計:Java virtual thread

閱讀時間約 16 分鐘
Figure 1 — Amazing 20 cores in a PC

Figure 1 — Amazing 20 cores in a PC

引言

當初在《迎接 Java 19: 虛擬執行緒與平台執行緒》[拙譯] 的後記中提到,如果把 virtual thread 套用到閒談軟體設計:Async everything? 裡面的例子,不知道會蹦出甚麼新花樣,隔了半年多,終於把之前的例子拿出來修修改改,一跑下去,還真的跑出非常意外的結果,那就來分享一下 virtual thread 的威力吧!

直接上場

畢竟閒談軟體設計:Async everything?寫在 2019 年,看實驗結果之前,先來聊聊程式進行了那些修改吧!當時使用的還是 Java 8/12,想要使用 virtual thread,需要 Java 19 以上,程式碼勢必要做些調整。

往下閱讀前,建議先到閒談軟體設計:Async everything?複習一下程式邏輯與實驗內容,下面的說明比較簡單扼要一點。

第一個是 client 端,原本是用 ScheduledThreadPoolExecutor,並將要產生的 request 數量當成參數,希望能產生對應數量的執行緒,但 platform thread 的成本很高,這次換成 Executors.newVirtualThreadPerTaskExecutor() 提供的 executor service [註:要用 @SuppressWarnings(“preview”) 是因為 virtual thread 仍在 preview 階段],意思是每個 task 都用單獨的 virtual thread:

回到 server 端,先前的例子中,替 SleepyRepository 提供 countAsync 和 saveAsync 兩個回傳 CompletableFuture 的函式,實作很簡單,只是利用 supplyAsync 和 runAsync 簡單地轉成 async 模式,兩個函式皆能指定執行的 Executor 實體,若無指定,則使用預設的 ForkJoinPool.commonPool(),使用的是 platform thread。這次會方便實驗,修改成根據設定,使用預設的 Executor 或是 Executors.newVirtualThreadPerTaskExecutor()

同樣,在 saveAsync 函式也是根據設定來使用對應的 Executor

然後,在 Spring framework 的 controller 中,有一個 wrapped 的版本,將 sync 版的 hello 轉成 async 版的 helloWrapped,這邊也是一樣,為了實驗方便,用條件判斷要使用哪一種 Executor

到這邊為止,都是 application code 的修改,把原本 sync 轉成 async 的地方套用新的 Executor,但有沒有一種可能,是在框架層就換掉呢?這樣是不是就更完美地不改變 application code 就能切換到 virtual thread?答案是有的,以 Spring framework 來說,文件上說只需要加入以下幾行。

程式客製 Spring framework 的初始化,第一個函式提供 AsyncTaskExecutor 實體,當 Spring framework 發現 controller 回傳值是一個 Future 時,便會將該 Future 丟到我們提供的 Executor 中執行。

第二個函式則是一個回傳一個 TomcatProtocolHandlerCustomer,去客製化底層 Tomcat,當 acceptor 接受一個 request 進來時,用什麼 Executor 執行,交出去後,acceptor thread 就繼續等待下個 request 進來。

最後就是開關面板,將所有的 flag 集中在一起,方便實驗時調整:

就這樣,只調整了這幾行程式,對了,這次的修改一樣有放到 GitHub 上。

這次程式修改其實沒有花太多的時間,反倒是環境設定花比較多的時間,因為 Eclipse 能同時開發多個 maven 專案,並處理彼此的 dependencies,相較 IntelliJ 我仍然比較喜歡使用 Eclipse。雖然 Eclipse 和 Java 一樣,都採取更快的更新頻率,卻還是常常要用 patch 的方式支援最新的 Java 版本,這次也是如此,Eclipse 2023–03 的版本並不支援 Java 20,安裝 patch 後,project 仍然無法切到 Java 20,後來就放棄了,反正 Java 19 也能開啟 virtual thread,但奇摩子就是不太好。

結果揭曉

當初寫閒談軟體設計:Async everything?的時候,用的電腦已經退役了,新組的電腦在各方面都遠比舊電腦要好很多,例如 CPU 核心數從 4 核心變成 Intel i5 13500 的 6 大核加 8 小核,加上大核支援 hyper threading,對作業系統來說就好像有 20 個核心 (Figure 1),因此無法跟之前的數據比較。

這裡補充一下這次實驗使用的環境,先前用了 12 年的桌機陣亡後,其實曾經一度想買 Mac Studio,但偶爾還是想玩個遊戲,Mac 系統要玩遊戲不是不行,有點麻煩,效能也不太理想,所以在小年夜那一天組裝新桌機,CPU 是 Intel i5 13500,用小機殼與 ITX 的主機板,所以直上兩條 16GB DDR 5 記憶體,合計 32 GB,顯卡是 AMD Radeon RX 6600,一顆 PCI Gen 4 M.2 1TB 的SSD 做系統碟,並把前一台主機的 SATA 1TB SSD 做為資料碟,作業系統則是 Windows 11,這也是我首次買盒裝 Windows (笑)。

作為對照組,即在不開啟任何 virtual threads,Figure 2 是 sync、async 和 wrapped 的執行結果,畢竟核心數這麼多,直接測 2000 個 requests 開始。

Figure 2 — new baseline

Figure 2 — new baseline

真不愧是 20 個核心,不論是 sync、async 還是 wrapped,2000 個 requests 都沒有失敗,雖然 response time 表現還是不理想,sync 模式,等最久的要 20 秒,async 模式是 31 秒,wrapped 模式則是 32 秒。

Figure 3 — Initial NIO-threads

Figure 3 — Initial NIO-threads

NIO thread 和之前的測試結果一樣,一開始會有 10 個 (Figure 3),最多曾經到 200 個,但等到測試結束一段時間後會降至 10 個。worker thread 和上次不同,最多到 19 個 (Figure 4)。

Figure 4 — Worker threads (ForkJoinPool)

Figure 4 — Worker threads (ForkJoinPool)

程式調整後,共有四個開關,在取得對照組後,要先嘗試哪個呢?在《迎接 Java 19: 虛擬執行緒與平台執行緒》中有句話很有趣:

虛擬執行緒真正擅長的是等待。

所以我們先調整 Tomcat,讓每個 request 都用一個 virtual thread 去等,讓我們看一下 sync、async 和 wrapped 的執行結果。

Figure 5 — Virtual threads enabled for Tomcat

Figure 5 — Virtual threads enabled for Tomcat

驚豔的 Sync 模式

sync 讓人十分意外,幾乎每個 request 都只需要兩秒多一點點就回傳結果,處理完所有 requests 的總時間也只需 3.5 秒,這和對照組相比,只用了 17% 的時間就跑完了。可是,有趣的是 async 和 wrapped 卻完全沒有改善。

用 debugger 觀察,會發現執行過程中 NIO executor threads 數量都是 0,因為已經換成 Executors.newVirtualThreadPerTaskExecutor(),然後會看到超多 thread 被啟動,很快又被終止掉,直等所有 requests 處理完,都沒有任何 NIO executor threads 被建立。

Figure 6 — No NIO executor threads

Figure 6 — No NIO executor threads

另一個有趣的點是,雖然 debugger 看不到 worker threads,但從 server 的 log 可以看到,有 19 個 worker threads 被建立,因此 Spring framework 仍然使用數量有限的 platform thread 執行交付的 async task,如果有 task 卡住了,會讓後面的 task 等待有限的 threads。

... do sleep at VirtualThread[#5990]/runnable@ForkJoinPool-1-worker-19

所以該怎麼改善呢?我心中大概有答案,直接做實驗驗證吧,下面為了呈現方便,把四個開關分別用 T (on/off) 代表 Tomcat Handler;A (on/off) 代表 Spring AsyncTask Executor;W (on/off) 代表 Wrapper 及 R (on/off) 代表 Repository 是否開啟 virtual thread。

Async 模式

針對 Async 模式,可能影響的開關有兩個 A 與 R,組合起來就是四種,當兩種都關閉時,就是 Figure 5 的 async 欄,剩下三種結果如 Figure 7 左側,可以看到真正有作用的是 Repository 開啟 virtual thread,Spring framework 的 async task executor 開或關都沒有甚麼影響,甚至從 server 的 log 也看不出 thread 的建立與管理邏輯,感覺似乎還有待 Spring framework 優化?

這邊可以注意到,virtual threads 被建立了 6000 個,然後很快地又消滅了,但為什麼是 6000 個?因為 Tomcat 為每個 request 建立 2000 個,等到呼叫 count 與 save 時,又各別建立 2000 個,總計就有 6000 個 virtual thread 被建立。那共用會比較好嗎?
Figure 7 — Async mode with different flags

Figure 7 — Async mode with different flags

Wrapped 模式

同樣,針對 wrapped 模式,影響的開關是 A 與 W,兩個都是關閉時結果就是 Figure 5 的 wrapped 欄,剩下的結果如 Figure 8 左側。很明顯可以看到 Spring framework 的 async task executor 並沒有作用,重點是 wrapped 的 task 要能在 virtual thread 中執行。

Figure 8 — Wrapped mode with different flags

Figure 8 — Wrapped mode with different flags

如果是這樣,即使關閉 Tomcat 的 virtual thread 支援,只要在對的地方開啟 virtual thread 就該有改善的效果?看實驗結果,在 Figure 7 與 Figure 8 最右側一欄,便是 T (off) + A (off) + R (on) 以及 T (off) + A (off) + W (on) 的結果,確實如預期般,獲得顯著改善。唯一的差別是 NIO Executor Threads 在最高峰的時期會增加到 200 個。

分析結果

這邊先為實驗結果先小節一下,Figure 9 把整個 request 從進來到 repository 的過程,用的是什麼 thread 給整理出來,先看 sync 模式,若沒開啟 Tomcat 的 virtual thread,整段都是靠 NIO Executor Thread (圖中縮寫成 NIO),開啟後就整段用 virtual thread (縮寫成 VT),這也是為什麼開啟後 sync 模式立即有顯著改善。

Figure 9 — Threads running in each component

Figure 9 — Threads running in each component

那 Async 模式呢?沒開啟 Tomcat 的 virtual thread,一開始使用 NIO,後段使用 Worker thread (縮寫 Worker),開啟 Repository 的 virtual thread 後,會在最後一段進入 VT。比較特別的是,一旦為 Tomact 開啟 virtual thread 後,debugger 會看不到 worker threads,可是 log 仍能看到 worker thread 的名字,這也許是 JVM 保留的,圖中是以 PT (platform thread) 標註,從圖中可以看出,只要最後會呼叫 sleep 的 repository 不是以 VT 執行,即便前面可能有使用 VT,也無法得顯著的改善。Wrapped 模式也可以看到一樣的結果。

總結

最後作個總結,這次的 Java Virtual Thread 讓我非常喜歡,原因不完全只是效能上顯著的改善,而是不用為了改善效能,硬是要把程式碼改成違反心智模型的寫法,是的,我就是在指 Reactive 或 Promise 等寫法。

Virtual Thread 提供和 Platform Thread 一樣的抽象,因此既有的專案,可以快速且少量的修改就能立即使用。只是在使用時,要注意的是,virtual thread 要用在刀口上,也就是真正會需要等待的地方,否則無法發揮效果。

對我來說,因為無法保證第三方的 Reactive 或 Promise 是用什麼方式實作,最單純的寫法就是應用程式的部分都是以 sync 的方式撰寫,也只使用 sync 的第三方套件,只要在最外層 (Tomcat) 開啟 virtual thread 就能獲得顯著的改善。

這也是我覺得好架構迷人的地方,以下純粹是我個人想法,不代表 Clean Architecture 的立場。在使用 Clean Architecture 時,越是核心的地方,越應該是單純,只有 business logic 與 application logic,盡可能沒有與框架或是第三方套件的相依,這些相依應該都推到外層。

為什麼?因為框架或第三方套件都是可以換的,現在用 Spring framework,未來想使用 Play framework 可不可以?可以,只要換掉外層的 controller,但核心呢?business logic不應該因為換框架而變動。

如果,第三方套件用了 Promise 或是 Reactive,導致所有 business logic 都要做調整,這就違反「只能有對內的相依方向」的原則。business logic 大多數情況下與效能優化無關,通常需要優化的是 I/O 的存取,這些既然都在外層,就應該在外層做優化,外層的優化不該影響核心,這才是好架構。


後記

這次修改除了使用 virtual thread 外,也用 Java 11 引入的 HttpClient 取代 OkHttp 的套件,讓 dependency 少了一個,使用上也蠻容易的,至於為什麼當時沒有考慮使用呢?嗯... 老實說我忘了。不過,因為真的可以建立非常多 virtual thread,當超過 1000 個 virtual threads 時,只用一個 client 實體發送請求時會遇到 ConnectException 或是 ConnectionClosedException,最後變成每一個 request 都是搭配一個獨立的 clinet 實體才解決問題。

53會員
104內容數
這是從 Medium 開始的一個專題,主要是想用輕鬆閒談的方式,分享這幾年軟體開發的心得,原本比較侷限於軟體架構,但這幾年的文章不僅限於架構,也聊不少流程相關的心得,所以趁換平台,順勢換成閒談軟體設計。
留言0
查看全部
發表第一個留言支持創作者!
Spirit的沙龍 的其他內容
如果您以為上一篇 已經是所有需要考慮的眉角,那可就錯了,實作 offline first 不是只有 client 要注意,server 也需要下功夫的。
任何語言特性用與不用,其實要看是否提升了生產力?是否提升可讀性?是否提升可維護性?這些都是在三個月甚至半年後回來修改程式時,才能明顯感受到的,而不是寫程式的當下。Java 8 的 CompletableFuture、Stream 和 Optional 都很好,但用的不好反而畫蛇添足又沒提高可讀性。
不管用哪種語言開發軟體,除非是那種一個 function 寫個幾萬行的人 (來人啊,把這種人拖去砍了),不然,一般都會根據某些因素,切割成模組或是特定功能的區塊 (一個 class 或是一個 function),但要完成一個特定功能,這些模組或區塊勢必要一起合作,因此這些模組與區塊就發生了關係。
Offline first 的設計最近有越來越多的感覺,但好的 Offline first 設計要解決蠻多的問題,是否使用 offline first 設計真的需要好好思考,不然可能得不到好處,反而還引起一堆 bug,本篇先探討在 client 端可能會遇到的問題與一些可能的解法。
如果您以為上一篇 已經是所有需要考慮的眉角,那可就錯了,實作 offline first 不是只有 client 要注意,server 也需要下功夫的。
任何語言特性用與不用,其實要看是否提升了生產力?是否提升可讀性?是否提升可維護性?這些都是在三個月甚至半年後回來修改程式時,才能明顯感受到的,而不是寫程式的當下。Java 8 的 CompletableFuture、Stream 和 Optional 都很好,但用的不好反而畫蛇添足又沒提高可讀性。
不管用哪種語言開發軟體,除非是那種一個 function 寫個幾萬行的人 (來人啊,把這種人拖去砍了),不然,一般都會根據某些因素,切割成模組或是特定功能的區塊 (一個 class 或是一個 function),但要完成一個特定功能,這些模組或區塊勢必要一起合作,因此這些模組與區塊就發生了關係。
Offline first 的設計最近有越來越多的感覺,但好的 Offline first 設計要解決蠻多的問題,是否使用 offline first 設計真的需要好好思考,不然可能得不到好處,反而還引起一堆 bug,本篇先探討在 client 端可能會遇到的問題與一些可能的解法。
你可能也想看
Google News 追蹤
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
11/20日NVDA即將公布最新一期的財報, 今天Sell Side的分析師, 開始調高目標價, 市場的股價也開始反應, 未來一週NVDA將重新回到美股市場的焦點, 今天我們要分析NVDA Sell Side怎麼看待這次NVDA的財報預測, 以及實際上Buy Side的倉位及操作, 從
Thumbnail
Hi 大家好,我是Ethan😊 相近大家都知道保濕是皮膚保養中最基本,也是最重要的一步。無論是在畫室裡長時間對著畫布,還是在旅途中面對各種氣候變化,保持皮膚的水分平衡對我來說至關重要。保濕化妝水不僅能迅速為皮膚補水,還能提升後續保養品的吸收效率。 曾經,我的保養程序簡單到只包括清潔和隨意上乳液
Thumbnail
這篇文章探討了在軟體開發中的技術債可能來自哪些原因,以及如何自動化偵測與修復技術債。作者透過分享不同情境下的技術債選擇,提供了對於技術債的思考與建議,針對開發人員在需要做出無奈的技術決策時,提供了一些建議。此外,還提供了一些在做出技術決策時的方法,如保留抽象層和避免vendor lock-in。
Thumbnail
今天來聊個最近很夯的主題 DDD,但不是 DDD 的本尊 Domain Driven Design,而是無所不在的 Database Driven Design,Database Driven Design 不是不好,只是你的模型容易變成貧血模型,邏輯都集中在 service 層等等。
Thumbnail
有趣的是,Model 其實沒什麼嚴格的定義,所以每個人對 Model 的解讀也不盡相同,有人覺得資料怎麼儲存屬於 Model 的一部份 (受 ORM 工具的影響),有人覺得工作流程 (workflow) 是 Model 的一部份,我個人也有自己的想法,而且隨專案的規模和特性,也不是總是一樣的。
Thumbnail
起源是當時 Facebook 有篇文章討論不少人分不清楚上述二者的差別,當時寫了首部曲《閒談軟體設計:API Naming Style》,接著是《閒談軟體設計:內部函式庫》,但始終沒談到 library 和 framework 的差別,主要是沒有好的例子,這次這例子還蠻不錯的。
Thumbnail
我自己偏好用 Repository 搭配 decorator 來管理 cache,而不是在 controller 層或是到處都有快取的邏輯,如果程式都是透過 Repository 更新資料,Repository 就會是一個不錯的地方更新快取,邏輯也就不會散亂在各處了。
Thumbnail
最近身旁有幾位正在懷孕、或剛生產完的朋友,讓我想起自己在懷孕期間印象最深刻的三件「怪事」,其中又以第三件事最誇張。
Thumbnail
不知道大家在買房之前是不是都會參考親朋好友的意見,或是上網看一些買房注意事項,有時候考慮了這塊就忘了那塊,考慮的那塊又忘了這塊.......
Thumbnail
塔西佗陷阱(Tacitus Trap),一個得名於古羅馬歷史學家塔西佗的政治學理論,意指倘若公權力失去其公信力,無論如何發言或是處事,社會均將給予其負面評價。 當然信著恆信、不信者恆不信,這就是真實的人生。
Thumbnail
關於片名   台灣片名《花漾女子》,原文片名《Promising Young Woman》,台灣譯名將時間定格在悲劇發生前,而原文片名則進一步帶我們看見另一個可能性結果
Thumbnail
前幾年因為身體的關係,當了幾年的律師逃兵,當時開了之前的事務所以後,一時間也沒有特別想要做甚麼事情,所以就邊讀一點書、早晚運動一下,剛好聽到當年同梯朋友進去金融業工作,因此也抱著嘗(ㄊㄠˊ)試(ㄅㄧˋ)的心態,找了份銀行法令遵循的工作
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
11/20日NVDA即將公布最新一期的財報, 今天Sell Side的分析師, 開始調高目標價, 市場的股價也開始反應, 未來一週NVDA將重新回到美股市場的焦點, 今天我們要分析NVDA Sell Side怎麼看待這次NVDA的財報預測, 以及實際上Buy Side的倉位及操作, 從
Thumbnail
Hi 大家好,我是Ethan😊 相近大家都知道保濕是皮膚保養中最基本,也是最重要的一步。無論是在畫室裡長時間對著畫布,還是在旅途中面對各種氣候變化,保持皮膚的水分平衡對我來說至關重要。保濕化妝水不僅能迅速為皮膚補水,還能提升後續保養品的吸收效率。 曾經,我的保養程序簡單到只包括清潔和隨意上乳液
Thumbnail
這篇文章探討了在軟體開發中的技術債可能來自哪些原因,以及如何自動化偵測與修復技術債。作者透過分享不同情境下的技術債選擇,提供了對於技術債的思考與建議,針對開發人員在需要做出無奈的技術決策時,提供了一些建議。此外,還提供了一些在做出技術決策時的方法,如保留抽象層和避免vendor lock-in。
Thumbnail
今天來聊個最近很夯的主題 DDD,但不是 DDD 的本尊 Domain Driven Design,而是無所不在的 Database Driven Design,Database Driven Design 不是不好,只是你的模型容易變成貧血模型,邏輯都集中在 service 層等等。
Thumbnail
有趣的是,Model 其實沒什麼嚴格的定義,所以每個人對 Model 的解讀也不盡相同,有人覺得資料怎麼儲存屬於 Model 的一部份 (受 ORM 工具的影響),有人覺得工作流程 (workflow) 是 Model 的一部份,我個人也有自己的想法,而且隨專案的規模和特性,也不是總是一樣的。
Thumbnail
起源是當時 Facebook 有篇文章討論不少人分不清楚上述二者的差別,當時寫了首部曲《閒談軟體設計:API Naming Style》,接著是《閒談軟體設計:內部函式庫》,但始終沒談到 library 和 framework 的差別,主要是沒有好的例子,這次這例子還蠻不錯的。
Thumbnail
我自己偏好用 Repository 搭配 decorator 來管理 cache,而不是在 controller 層或是到處都有快取的邏輯,如果程式都是透過 Repository 更新資料,Repository 就會是一個不錯的地方更新快取,邏輯也就不會散亂在各處了。
Thumbnail
最近身旁有幾位正在懷孕、或剛生產完的朋友,讓我想起自己在懷孕期間印象最深刻的三件「怪事」,其中又以第三件事最誇張。
Thumbnail
不知道大家在買房之前是不是都會參考親朋好友的意見,或是上網看一些買房注意事項,有時候考慮了這塊就忘了那塊,考慮的那塊又忘了這塊.......
Thumbnail
塔西佗陷阱(Tacitus Trap),一個得名於古羅馬歷史學家塔西佗的政治學理論,意指倘若公權力失去其公信力,無論如何發言或是處事,社會均將給予其負面評價。 當然信著恆信、不信者恆不信,這就是真實的人生。
Thumbnail
關於片名   台灣片名《花漾女子》,原文片名《Promising Young Woman》,台灣譯名將時間定格在悲劇發生前,而原文片名則進一步帶我們看見另一個可能性結果
Thumbnail
前幾年因為身體的關係,當了幾年的律師逃兵,當時開了之前的事務所以後,一時間也沒有特別想要做甚麼事情,所以就邊讀一點書、早晚運動一下,剛好聽到當年同梯朋友進去金融業工作,因此也抱著嘗(ㄊㄠˊ)試(ㄅㄧˋ)的心態,找了份銀行法令遵循的工作