iT邦幫忙

2026 iThome 鐵人賽

0
Software Development

Spring Boot + Kotlin 協程高併發,後端開發新選擇系列 第 31 篇

Day 30:完賽總結,Kotlin 協程適合與不適合的情境

  • 分享至 

  • xImage
  •  

Day 29:全系列整合驗收,跑一次完整的高併發情境 結尾留下一個問題:這趟旅程累積了大量具體的知識與實作經驗,但退一步問,這一切最初是為了回答什麼問題?答案要回到系列第一天。今天,也是這個系列的最後一天,就從那裡開始。

回顧,那個會卡住的 API,還記得嗎

Day 01:為什麼高併發後端需要協程,從一個會卡住的 API 說起 開場描述過一個畫面。一個電商後端,有一支平常反應飛快的 API,使用者按下確認結帳,後端查詢訂單狀態、確認庫存、扣減庫存、寫回資料庫,幾十毫秒內完成,快到你不會特別去關注它的效能報表。

促銷活動開賣那一刻,畫面整個變了。倒數計時歸零,大量使用者同時湧入,回應時間從幾十毫秒爬升到幾百毫秒,再到一兩秒,接著開始有請求直接逾時。前端轉圈圈轉到使用者放棄重新整理,客服後台湧入抱怨,有人說按了結帳沒反應,有人說同一筆訂單被扣了兩次庫存。監控面板上 CPU 使用率其實沒有衝到滿載,資料庫也還撐得住,系統卻就是變得又慢又不穩定。

當時那篇文章留下兩個懸念沒有給答案。為什麼會這樣,加機器到底能不能解決,這兩個問題在 29 天前只有一個方向性的猜測,關鍵可能在執行緒被閒置等待的方式,而非硬體數量不夠。方向猜對了,但當時說不出完整的機制,也說不出真正該怎麼解。今天要把這筆帳算清楚。

完整回答,那個症狀背後真正發生的事

先回到症狀本身。Day 02:Thread-per-Request 模型的極限,搞懂你原本在用什麼 把這個機制拆解得很仔細:Spring Boot 預設的 Thread-per-Request 模型,一個請求進來就配一條執行緒陪它從頭走到尾,執行緒池的容量固定,而請求處理過程中,真正花在運算的時間占比很低,絕大部分時間都花在等待資料庫回應、等待內部服務確認庫存、等待寫入確認完成。等待期間執行緒被白白佔著,動彈不得,這正是那個「阻塞」的精確定義。促銷活動一來,同時湧入的請求遠超過執行緒池的上限,多出來的請求只能在容器層排隊,隊伍越積越長,回應時間自然一路推高到逾時。加機器確實能撐住一段時間,但代價會隨著機器數量攀升,遲早撞到現實的天花板,這一點在系列一開始就已經誠實點出來過。

如果問題出在執行緒被閒置等待占著,答案的方向自然就是讓執行緒在等待時能被讓出去。Day 03:第一個 suspend function,協程到底暫停了什麼 給出了第一塊積木,suspend function 讓函式在等待某個操作完成時,把控制權交還出去,執行緒被釋放去處理其他請求,等結果真正回來,協程才被接回某條可用的執行緒上繼續往下跑。暫停不是阻塞,這是那篇文章最重要的一句話,也是這整套模型能夠成立的根基。

光有一個會暫停的函式還不夠,真實的請求往往牽涉多個步驟、甚至多個並行進行的子任務,這時候就需要一套規則管理這些協程彼此的關係。Day 05:Coroutine Scope,協程住在哪裡、活多久 定義了協程的生命週期容器,[[Day 06:Structured Concurrency,為什麼協程不能亂長亂放]] 確保父子協程之間有明確的收斂規則,父範圍結束或取消時子協程必須一併結束,不會有協程遊蕩在外、失去管控。[[Day 07:Dispatchers,協程實際跑在哪條執行緒上]] 決定這些協程實際被排程到哪一種執行緒資源,[[Day 08:Coroutine Context 與例外處理,協程出錯了誰負責]] 補齊了協程攜帶的上下文與例外傳播機制。這四件事合在一起,讓「執行緒被讓出去」這件事有了明確邊界,成為一套能被追蹤、能被信任的結構,而非失控的混亂。

執行緒能不能真正被讓出去,取決於整條請求處理鏈路上有沒有任何一環還是傳統的阻塞式呼叫,光靠協程本身撐不起這件事。Day 14:小結:三種技術選型的判斷依據,MVC、WebFlux 各用在哪 收斂了選型的判斷依據,搭配 WebFlux 反應式模型與 R2DBC 非阻塞資料庫存取,讓從接收請求到資料庫存取,整條鏈路都維持非阻塞,協程的優勢才真正發揮到底,不會卡在某個環節依然乖乖排隊等待。

這一切也並非憑空宣稱的效益。Day 20:實測結果解讀,協程贏在哪裡、代價又是什麼 用 k6 實際量測過,協程模型在高併發、等待密集的情境下,回應時間增長更平緩,能撐住的吞吐量也更高,這個結論有數據驗證態度撐著,不是紙上談兵。

把這些串起來,29 天前那個模糊的疑問,現在有了完整的因果鏈可以回答。那支會卡住的 API,真正的病灶在於 Thread-per-Request 模型讓執行緒在等待外部回應時被白白占著,執行緒池很快被打滿,後面的請求只能排隊,流量本身反而只是誘發這個病灶的導火線。解法的方向也就跟著清楚了:讓執行緒在等待期間能被釋放出來,比無止盡地加機器更貼近問題核心,這正是 suspend function 的暫停機制在做的事,搭配 Coroutine Scope、Structured Concurrency、Dispatchers、Coroutine Context 這套結構化的管理機制,確保這些協程不會失控,再搭配 WebFlux 與 R2DBC 讓整條鏈路都維持非阻塞,才能把這個優勢真正落實到底,而這套組合的效益也經過實際量測驗證。這就是完整的答案。

誠實地說,協程不是任何情境都該用

讀到這裡,很容易產生一種錯覺,協程模型解決了問題,數據也支持它,那是不是任何後端服務都該導入協程。這個結論太快了,也正是這個系列從第一天就想避免的態度。接下來要誠實列出三個協程不適合、或不必要的情境,這是三個值得認真放進判斷標準裡的論證,而非形式上的免責聲明。

第一個情境,純 CPU 密集運算。前面整套因果鏈的起點,是執行緒在等待外部回應時被閒置占用,協程解決的正是這個閒置問題。但如果一個服務的工作內容主要是大量運算,例如複雜的數學模型計算、影像處理、大規模資料轉換,執行緒本來就一直在忙著運算,沒有閒置等待這回事,協程要解決的核心問題在這種服務裡根本不存在。這種情境下引入協程,不會帶來 Day 20 示意數據呈現的那種效益,因為執行緒的瓶頸在運算本身,不在等待。更麻煩的是,協程的排程本身有它自己的開銷,Coroutine Scope 的管理、Dispatcher 的排程決策,這些機制都有成本,Day 20 那篇的示意數據裡,低併發、運算為主的情境下,協程版本的表現甚至可能略遜於單純的阻塞式寫法,這件事當時已經誠實揭露過,這裡再次確認同一個結論:協程不會讓運算本身變快,它調節的是執行緒資源怎麼被使用,運算密集型服務沒有多少閒置時間可以被協程省下來。

第二個情境,團隊對協程除錯經驗不足。Day 6 與 Day 8 建立的結構化並發與例外傳播機制,設計精良,確保了協程之間的父子關係與例外能夠被正確收斂,但這套機制本身有它的學習曲線。一個跨越多個協程的例外,堆疊追蹤看起來跟傳統同步程式碼不太一樣,某個協程被取消時連帶影響了哪些子協程,這些行為對熟悉的人來說是設計精良的保障,對不熟悉的人來說卻可能是半夜出狀況時看不懂發生了什麼事的來源。癥結不在協程本身,而在團隊的除錯直覺長年建立在 Thread-per-Request 那種一條執行緒從頭走到尾、堆疊追蹤一目瞭然的模型上,換一套心智模型需要時間累積。如果團隊沒有足夠的時間投入學習,也還沒有機會累積協程情境下的除錯經驗,貿然把正式環境的核心服務整套換成協程與非阻塞架構,風險不只在架構本身能不能撐住流量,更在於出狀況時團隊能不能在合理時間內定位問題。效能數字再好看,也彌補不了這項成本,選型時這件事該被認真放進權衡,而不是等出事才發現。

第三個情境,既有系統流量不高。如果一個服務本身規模不大,日常流量始終遠低於執行緒池的上限,Thread-per-Request 模型在這種情境下運作得相當好,簡單直觀,維護成本也低,這件事 Day 2 那篇已經先誠實提醒過,Thread-per-Request 並不是設計上的失誤,也稱不上過時的技術,只是在高併發、等待密集的場景下缺點才會被放大。如果你的服務沒有促銷尖峰那種瞬間湧入的流量,執行緒從來沒有被逼到打滿的邊緣,協程能省下的等待成本本來就沒有多少發揮空間,為了追求技術上看起來比較先進,把整套架構換成協程與非阻塞模型,換來的效益可能相當有限,付出的學習成本與除錯門檻卻是實實在在的。技術選型從來不是非黑即白的事,一個工具好不好,永遠要放回它要解決的問題規模裡看,脫離了問題規模談優劣,容易變成單純的技術信仰。

這三個情境合在一起,其實指向同一件事。協程解決的是一個很具體的問題,執行緒在等待外部回應時被閒置占用,這個問題不存在,或是規模小到不構成困擾,或是團隊還沒準備好承擔它的學習成本,這個當下優先導入協程都稱不上是好決定。把這三個情境放進判斷標準裡,比起記住協程有多少優點,更能幫助你在真正面對一個技術選型時做出站得住腳的決定。

這趟旅程留下的,不只是協程知識

看完這一系列文章,如果對怎麼使用 Kotlin 寫 Spring Boot 專案有興趣,歡迎訂閱電子報

https://laravel.reccachao.net/newsletter/spring-boot

這個系列一開始就說過,核心目標不是背熟一份協程 API 的速查手冊,而是建立一套判斷力,看到一個高併發情境時,知道該用哪個機制、為什麼用它、不用會有什麼代價。走過這 30 天,從那支會卡住的 API,到 Coroutine Scope 與 Structured Concurrency 的心智模型,到 WebFlux 與 R2DBC 的選型判斷,到 Semaphore、逾時取消、背壓這些高併發控制手段,再到具名專案裡的監控、壓測、部署檢查清單,這套判斷力大概已經在不知不覺間建立起來了。

如果接下來要在自己的專案裡評估要不要導入協程,希望帶走的不是「協程比較先進所以該用」這種結論。更值得帶走的,是回頭問問這個服務的瓶頸到底在哪裡,執行緒是不是真的被閒置等待占用著,團隊有沒有餘裕承擔學習曲線,流量規模是不是真的逼近了原本模型的極限。這些問題問完,答案有時候是協程,有時候不是,兩種答案都不代表這 30 天的學習白費了,反而正是這趟旅程真正想留下的東西。

系列到這裡完結,謝謝一路讀到這裡的每一位讀者,也謝謝那支曾經卡住的 API,讓這整套故事有了一個具體的起點。


上一篇
Day 29:全系列整合驗收,跑一次完整的高併發情境
系列文
Spring Boot + Kotlin 協程高併發,後端開發新選擇 共 31 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言