Day 13:協程搭配訊息佇列與外部 API 呼叫的常見模式 結尾把問題攤開來了:這幾天累積的技術選型各自看起來都理解了,但實務上該根據什麼判斷依據去選擇,還沒有一個統一的答案。今天要把這張選型地圖畫出來。
四天之內,這個系列陸續走過三個技術選項。
Day 10:在 Spring MVC 裡寫 suspend function,能動但有代價 定案了協程與 Spring MVC 整合模式,語法上可行,但底層仍是 Thread-per-Request 容器骨架。
Day 11:WebFlux 是什麼,協程與反應式模型的分工 定案了 WebFlux 反應式模型,少數事件循環執行緒服務大量連線,與協程分屬不同層次,可以互補搭配。
Day 12:R2DBC,讓資料庫存取也不阻塞執行緒 定案了 R2DBC,把非阻塞這件事延伸到資料庫存取這一層,全系列資料庫技術棧也在那天定案為 PostgreSQL 搭配 R2DBC。
今天不會重新解釋這三者各自怎麼運作,那些定義已經定案,直接沿用即可。今天要處理的問題更實際:這三者不是互斥的三選一,而是分屬不同層次的選擇。Web 框架這個層級,要在 Spring MVC 與 WebFlux 之間選一個;資料庫存取這個層級,則有 R2DBC 這個非阻塞選項可以搭配。把這兩層分開看,選型才不會混成一團,本篇就是要建立這個清楚的判斷框架。
選型這件事,很容易被簡化成「哪個技術比較先進、比較該選」。但實務上,第一個要面對的問題從來不是這個,而是團隊手上現在有什麼、熟悉什麼。
如果團隊已經有大量既有的 Spring MVC 專案,維運經驗也都建立在這套框架之上,短期內沒有更換底層框架的迫切需求,Day 10 提到的「在 Spring MVC 裡寫 suspend function」就是務實的起點。
這個選擇不會讓系統立刻擺脫 Thread-per-Request 執行緒池上限的限制,但可以先讓新寫的業務邏輯採用協程風格,享受程式碼一致性帶來的維護效益,之後如果真的評估要往非阻塞架構遷移,業務邏輯層已經是協程寫法,遷移阻力會小很多。這是一條漸進路線,不是終點,但很多團隊的現實條件就是只能走漸進路線。
如果是全新專案,或者專案雖然還小,但已經明確預期會面對高併發流量情境,情況就不一樣了。
這種時候與其先走過渡方案再回頭遷移,不如直接採用 WebFlux 搭配 R2DBC,從底層就是非阻塞的組合,長期來看能更完整發揮協程與非阻塞模型疊加起來的效益。少了既有系統的包袱,一開始就照最終想要的架構去搭,通常比中途轉向划算。
團隊熟悉度也是同一個維度底下的變數。
WebFlux 搭配 R2DBC 的除錯經驗與傳統 Spring MVC 相比,思維方式差異不小。事件循環模型下的堆疊追蹤與例外處理,不像 Thread-per-Request 模型那樣直覺。
如果團隊完全沒有反應式程式設計的經驗,貿然把正式環境的核心服務整套換成 WebFlux,風險不只在架構本身,也在團隊是否有能力在半夜出狀況時看懂發生了什麼事。這一點不會讓答案反轉,但會影響選型時該投入多少學習與觀察的緩衝期。
現況與熟悉度講完了,換一個角度看,從技術特性本身出發,兩個變數會直接影響選型帶來的效益差異有多明顯:併發規模,以及請求處理過程中等待外部回應的密集程度。
先看併發規模不大的情境。
回到 Day 02:Thread-per-Request 模型的極限,搞懂你原本在用什麼 與 Day 04:小結:把 Thread 模型與協程模型放在一起比一次 已經建立的對照,如果應用程式面對的同時請求數量本來就不多,執行緒池不太可能被打滿,Thread-per-Request 模型底下即使不特別採用協程或 WebFlux,也不一定會遇到明顯瓶頸。
同樣地,如果請求處理過程中大部分時間都花在運算本身,而不是在等資料庫或外部服務回應,非阻塞帶來的效益也有限,畢竟非阻塞解決的是「等待期間執行緒被閒置」這個問題,運算密集的請求本來就沒有多少空等的時間可以省。這種情境下,選型的急迫性偏低,先求穩、先求團隊上手,是合理的優先順序。
再看另一端。回到 Day 11:WebFlux 是什麼,協程與反應式模型的分工 建立的事件循環模型,如果應用程式面對的是高併發流量,而且請求處理過程中大部分時間是在等待資料庫或外部服務回應,這正是 WebFlux 搭配協程與 R2DBC 這個組合最能發揮優勢的情境。示範情境訂單查詢與扣庫存服務就是典型例子,查詢訂單要等資料庫回應,扣庫存要等交易完成,Day 13:協程搭配訊息佇列與外部 API 呼叫的常見模式 提到的確認優惠資格與發送通知,也都是在等外部服務的回應。整個請求生命週期裡,真正在做運算的時間占比很低,絕大部分時間都在等待,這正是少數事件循環執行緒能夠發揮槓桿效益的情境,一條執行緒不需要為了陪一個請求空等,可以轉頭去處理其他連線上真正有事情發生的事件。
把這兩個變數放在一起看,會發現它們往往同時出現,也往往同時缺席。等待密集型的服務,例如電商後端這種大量依賴資料庫與外部服務的系統,通常也正是併發規模容易在促銷活動期間急遽上升的服務。而運算密集型的服務,例如影像處理或複雜的批次計算,就算併發數量高,非阻塞架構能省下的等待時間也相對有限,這種情境下花力氣換架構,投資報酬率不見得划算。
前面兩節分別從現況面與技術特性面拆解了判斷依據,這裡把它們收斂成一張表,方便日後查閱與引用。
| 既有系統狀況 | 團隊熟悉度 | 併發規模與等待密集程度 | 推薦技術組合 |
|---|---|---|---|
| 大量既有 Spring MVC 專案,短期不換框架 | 熟悉傳統 Servlet 開發,反應式經驗有限 | 併發規模不大,或請求以運算為主 | Spring MVC 搭配 suspend function 過渡方案 |
| 大量既有 Spring MVC 專案,但計畫逐步遷移 | 團隊願意投入學習,先從業務邏輯層開始鋪路 | 目前規模尚可,但預期未來會升高 | Spring MVC 搭配 suspend function,業務邏輯先協程化,作為遷移前置準備 |
| 全新專案,沒有既有框架包袱 | 具備或願意建立反應式除錯經驗 | 高併發,且請求大部分時間在等待資料庫或外部服務 | WebFlux 搭配協程與 R2DBC |
| 全新專案,團隊完全沒有反應式經驗 | 尚未建立反應式除錯經驗,短期內難以補足 | 高併發,等待密集 | 先以 Spring MVC 搭配 suspend function 起步,同步建立團隊反應式能力,再評估遷移時機 |
這張表沒有標準答案可以套用到所有團隊,但它把「該問自己哪些問題」整理清楚了。既有系統與團隊熟悉度決定了現實可行性,併發規模與等待密集程度決定了技術效益的迫切性,兩者交叉比對,答案通常會自然浮現,不需要陷入非黑即白的技術信仰之爭。
這裡也順帶把後續章節的背景交代清楚。這個系列從 《Day 22:收斂進一個具名專案,訂單服務實戰整合版啟動》 起進入實戰整合期,示範專案訂單服務實戰整合版會採用 WebFlux 搭配協程與 R2DBC 這個組合作為主軸,理由正好對應這張表的右下角,示範情境是全新設計、高併發、等待密集的服務,最適合從底層就是非阻塞的組合。後面章節看到的程式碼範例,背景都建立在這個選型之上,不會再回頭示範 Spring MVC 過渡方案的寫法,這裡先說清楚,避免讀者在後續章節看到具體技術棧時感到突兀。
Day 10:在 Spring MVC 裡寫 suspend function,能動但有代價 到今天這五天,處理的是「用什麼底層架構」這個選型問題。選定架構之後,程式碼確實可以用協程風格撰寫,資料庫存取也確實具備非阻塞的潛力,這是生態整合期累積下來紮紮實實的成果。
但選對框架,不代表系統就能自動撐住任何規模的流量。
即使底層是 WebFlux 搭配 R2DBC 這種從骨子裡就非阻塞的組合,如果同時湧入的請求量大到連下游資料庫或外部服務都吃不消,架構本身的非阻塞特性並不會自動幫忙節流、也不會自動處理某個依賴突然回應緩慢的情況。
這些問題目前為止都還沒有正面處理過,前面四天談的一直是「怎麼讓執行緒不被白白浪費」,還沒有談到「當真正大量的請求同時湧入時,該怎麼保護系統不被壓垮」。
這是一個新的命題,跟前面五天討論的內容站在不同的問題層次上。下一篇會正式進入併發深化期,開始處理這些真正的高併發控制問題。
Day 13:協程搭配訊息佇列與外部 API 呼叫的常見模式 把生態整合期最後一塊拼圖擺上桌,今天則把整個階段收斂成一張選型地圖,也在此為生態整合期畫下句點。《Day 15:併發限制,用 Semaphore 保護下游別被打爆》 見。