iT邦幫忙

2026 iThome 鐵人賽

DAY 23
0
Software Development

遠古聖遺物改造工程:遺留系統全面重構實務指南系列 第 23

[Day 23] 如何讓系統更穩定?需要分散式架構嗎?

  • 分享至 

  • xImage
  •  

如何讓系統更穩定?需要分散式架構嗎?

系統穩定代表它能在已定義的處理量與失敗情境下維持必要功能,或在允許時間內恢復。增加執行副本、拆分服務或導入容器編排平臺,只能處理其中一部分問題。程式錯誤、不正確的重試、並行更新衝突及無法復原的保存資料,不會因為系統拆得更細就消失。

設計目標系統時,應該先把穩定需求改寫成可檢查條件,再判斷單一部署單位是否已經無法滿足需求。只有獨立部署、獨立擴充、故障隔離或跨地區運作等需求具有明確價值時,才需要承擔分散式架構(Distributed Architecture)新增的通訊與維運成本。

先定義穩定的可檢查結果

「系統要很穩定」無法用來選擇架構。相同的中斷對不同系統可能具有完全不同的影響,因此要先記錄必要功能、限制值及失敗後的預期結果。

確認面向 要回答的問題 可檢查結果範例
必要功能 發生部分故障時,哪些功能仍要繼續運作?哪些功能可以暫停? 指定相依項目失效時,必要功能仍能完成或回覆明確狀態
中斷時間 每次故障允許中斷多久?多久內要恢復? 從偵測故障到恢復必要功能的時間沒有超過限制
保存結果 可以遺失多少已完成變更?復原後要如何確認內容正確? 復原結果符合已核准的資料範圍與一致性檢查
處理能力 日常與尖峰情境需要完成多少工作?允許多少延遲與失敗? 在指定處理量下,回應時間與失敗比例符合門檻
功能降級 相依項目暫時無法使用時,可以停止、改用較簡單流程或稍後處理嗎? 每種降級方式都有觸發條件、對外結果及恢復方式
變更影響 發布一項功能時,可以中斷或重新部署哪些部分? 指定變更能在允許範圍內完成部署與驗證

這些條件要連回已確認的需求與風險。沒有可檢查結果時,「高可用性」或「自動擴充」只是一項技術描述,無法證明架構改善了甚麼。

先確認不穩定的真正來源

分散式架構適合處理特定的部署與執行邊界問題,無法代替一般的程式品質與復原設計。決定拆分前,應該根據測試結果、執行記錄與故障紀錄,將問題分成可以採取不同處理方式的類型:

  • 確認失敗是否來自未處理的輸入、程式例外或狀態錯誤,並先修正功能規則與錯誤處理。
  • 確認處理量問題是否集中於少數工作,並比較改善演算法、限制並行數量與增加執行資源的效果。
  • 確認中斷是否由單一相依項目造成,並驗證逾時、降級、備援與復原程序。
  • 確認發布是否因所有功能必須同時部署而受到限制,並記錄真正需要獨立變更的部分。
  • 確認故障是否會耗盡共用資源或阻塞其他功能,並找出可以建立隔離範圍的位置。
  • 確認並行更新是否會覆蓋結果或重複執行,並在保存邊界加入原子操作或衝突處理。

如果問題來自錯誤的功能規則,拆成多個服務只會讓錯誤經過網路傳播。如果瓶頸來自共同保存機制,單純增加程式副本也可能讓競爭更嚴重。必須先找出限制發生的位置,才能判斷要改善程式、調整部署單位或改變架構。

模組化單體與微服務

模組化單體(Modular Monolith)在程式內建立清楚的模組責任,正式執行時仍維持單一部署單位。微服務(Microservices)則將部分責任設計成可以獨立建置、部署及運作的服務。兩者都能建立良好邊界,主要差異在於邊界是否跨越程序與通訊環境。

比較面向 模組化單體 微服務
開發 可以在同一程式中修改與重新整理模組介面 跨服務變更要管理介面契約、版本與相容順序
部署 所有模組共用建置成品與發布時程 每個服務可以獨立部署,但要管理多個版本與設定
擴充 通常增加整個部署單位的執行數量或資源 可以只擴充受影響服務,但跨服務相依仍可能形成瓶頸
故障隔離 同一程序的資源耗盡或未處理錯誤可能影響整體 程序邊界可以限制直接影響,但逾時與重試仍可能造成連鎖故障
資料一致性 同一保存邊界內較容易使用單一交易完成變更 跨服務變更通常要處理部分成功、延遲同步與補償操作
測試 可以在同一執行環境驗證多個模組的組合 除了各服務測試,還要驗證契約、網路失敗與不同版本組合
問題調查 呼叫與狀態通常集中在較少執行單位 要以關聯識別碼、集中記錄與分散式追蹤串連處理過程
維運 部署單位、設定與監控對象較少 還要處理服務探索、通訊保護、個別容量與故障復原

Microsoft 的微服務架構說明也將獨立部署、資料擁有權與擴充能力列為微服務的特性,同時指出服務間通訊、資料一致性、測試及問題調查會增加整體複雜度。架構選擇應該比較完整成本,不能只比較單一服務的程式碼大小。

當需求仍在變動、模組邊界尚未穩定,或所有功能必須一起發布與驗收時,模組化單體通常能用較低成本保留清楚責任。等到實際結果顯示某個邊界需要獨立運作,再評估拆成服務,也能避免一開始就建立大量不穩定的通訊介面。

確認獨立服務的成立條件

程式中的類別、資料夾或模組不會自動成為適當的服務。候選服務至少要能回答下列問題:

  • 候選範圍是否具有集中且可以單獨說明的功能責任?
  • 輸入、輸出、錯誤結果與版本相容規則是否可以形成穩定契約?
  • 是否存在需要獨立發布、獨立擴充或隔離故障的可檢查需求?
  • 候選服務是否能擁有自己的狀態變更規則,避免其他服務直接修改其保存內容?
  • 候選服務是否能獨立建置、測試、部署、觀察及復原?
  • 如果服務暫時無法使用,呼叫端是否具有明確的停止、降級或稍後處理方式?
  • 負責開發與維運的人員是否能長期處理介面相容、故障調查及版本更新?

只符合「程式碼很多」或「名稱看起來像一個模組」不構成拆分理由。如果兩個候選服務經常需要同時修改、共同查詢內部結構,而且不能分開驗收,跨程序邊界會把原本的程式內耦合改成較難發現的通訊耦合。

定義資料擁有權與一致性邊界

服務能獨立演進的前提,是每項狀態變更都有明確擁有者。其他服務可以透過已定義的互動方式取得結果或提出變更要求,不應該直接修改擁有者的內部保存結構。

「一個服務擁有資料」不代表每個服務都必須採用不同的資料庫產品,也不要求使用不同的實體執行環境。即使因部署限制而共用同一個保存平臺,也應該分開結構與寫入責任,避免一項結構變更迫使所有服務同時調整。Microsoft 的微服務資料設計說明也將避免共用結構與直接跨服務讀寫列為降低耦合的重點。

單一功能如果必須同步修改多個服務的狀態,代表架構需要處理部分成功。可以先重新檢查服務邊界,確認這些變更是否其實屬於同一個一致性範圍。如果確實需要跨服務流程,再定義每一步的擁有者、可重試條件、完成狀態與補償操作。最終一致性(Eventual Consistency)、訊息傳遞及交易寄件匣模式(Transactional Outbox Pattern)的詳細設計會由後續章節說明。

為跨服務互動設定失敗邊界

程序內呼叫通常只有成功或拋出例外。跨服務呼叫還可能遇到連線失敗、回應延遲、對方已完成但回覆遺失,以及不同版本暫時並存。呼叫端不能無限等待,也不能把所有失敗都直接重試。

處理方式 要解決的問題 設計時要確認的內容
逾時 避免呼叫長時間占用處理能力 每一段呼叫的時間上限、整體操作期限及逾時後結果
有限重試 處理短暫且再次執行可能成功的失敗 可重試錯誤、次數上限、退避間隔及整體重試額度
冪等性(Idempotency) 避免相同操作因重試而重複產生效果 操作識別碼、重複判斷範圍及既有結果的回覆方式
斷路器(Circuit Breaker) 暫停持續呼叫已經失效的相依項目 開啟條件、等待時間、試探方式及恢復條件
艙壁模式(Bulkhead Pattern) 限制一項相依故障耗盡所有共用處理能力 可以隔離的工作佇列、連線、並行數量或執行單位
降級與補償 在完整結果暫時無法取得時維持可接受行為 可省略功能、待處理狀態、補償動作及人工介入條件

Azure 暫時性故障處理建議指出,重試要有次數與總量限制,並且避免大量請求以固定間隔持續重試。艙壁模式則用隔離資源的方式限制故障擴散。這些機制要按照實際失敗語意組合,重試無法修復的錯誤只會增加相依項目負擔。

同步呼叫適合需要立即取得結果,而且呼叫端能接受等待與失敗的情境。非同步訊息傳遞可以分開處理時間,但會新增訊息重複、順序、積壓與延遲結果。兩者都要先定義失敗後的狀態,不能只用通訊方式判斷可靠性。

處理多副本的並行更新

增加相同程式的執行副本後,多個操作可能同時讀取相同狀態並寫入不同結果。這種衝突在單一部署單位也可能發生,只是在水平擴充後更容易出現。處理方式應該放在真正擁有狀態的邊界,避免每個呼叫端各自實作不同規則。

處理方式 適合評估的情況 必須驗證的失敗情境
原子操作或唯一限制 保存機制可以直接完成不可分割的檢查與更新 重複要求、同時建立及限制衝突時的明確結果
樂觀鎖定(Optimistic Locking) 衝突不常發生,可以使用版本值判斷讀取後是否遭修改 版本不符時中止更新、重新讀取及有限次重試
悲觀鎖定(Pessimistic Locking) 同一狀態同時修改會造成不可接受結果,需要依序執行 等待上限、鎖定範圍、死結處理及持有者失敗
分散式鎖定(Distributed Locking) 多個執行者必須協調保存邊界以外的互斥工作 鎖逾時、續期、錯誤釋放、網路分割及互斥保證

優先使用保存機制提供的原子操作或衝突控制,因為它最接近狀態擁有者。只有工作跨越單一保存交易,而且確實需要互斥時,才評估分散式鎖定。鎖定也不能代替冪等性,因為呼叫端仍可能在結果不明時再次送出相同操作。

如果系統已確認使用 Redis,Redis 交易說明中的 WATCH 可以監看鍵值是否在交易執行前遭到修改,衝突時中止交易,屬於樂觀鎖定的做法。需要跨多個執行者使用 Redis 鎖定時,還要按照Redis 分散式鎖定說明驗證期限、釋放、故障與互斥條件。加入 Redis 本身不會自動消除並行更新衝突。

理解 Kubernetes 能處理的範圍

如果目標系統已確認採用容器、具有多個可替換副本,而且需要由叢集維持預期執行狀態,可以評估 Kubernetes。它能協助調度工作負載、維持副本數量、替換失效的 Pod,並且把服務互動導向可用端點。Kubernetes 自我修復說明也明確區分平臺可以替換失效容器,但應用程式本身的錯誤仍要另外處理。

在這種情境下,負載分流與自動擴充仍需要應用程式配合:

  • 每個副本都要能處理被分配的操作,不能依賴只存在於某一副本的未共享狀態。
  • 健康檢查要反映程式是否已準備接受操作,不能只確認程序仍在執行。
  • 相同操作可能到達不同副本,因此狀態變更要具備並行控制與冪等處理。
  • 自動擴充要採用能反映真正瓶頸的指標,並且確認增加副本不會壓垮共同相依項目。
  • 副本縮減、重新啟動與部署更換都要處理進行中的工作,避免直接中斷或重複執行。

Kubernetes 水平 Pod 自動擴充會根據已設定的資源或自訂指標調整目標工作負載的副本數量。它不會替目標系統決定容量門檻,也不會修正過慢的功能規則、錯誤的資料一致性設計或無上限的重試。

評估分散式架構增加的工作

微服務讓部署單位可以獨立變更,也把原本由程式內環境提供的能力轉成需要明確設計與維護的系統功能。採用前,至少要估算下列工作:

  • 建立服務名稱、位置與版本的探索方式,並處理服務暫時不存在的情況。
  • 分開管理各服務的執行設定、敏感設定與相容版本。
  • 建立介面契約及相容規則,驗證不同版本同時運作時的結果。
  • 使用關聯識別碼、集中記錄、指標與分散式追蹤還原一次跨服務操作。
  • 建立可以重現多服務互動的開發與測試環境,並模擬延遲、逾時與部分失敗。
  • 為各服務定義容量、部署、備份、復原、告警及處理責任。
  • 建立故障調查方式,避免問題在多個服務之間轉交而沒有明確負責範圍。

這些工作如果沒有明確負責人與可執行流程,服務數量增加後會直接降低變更速度與故障處理能力。維運能力也是架構限制,不能等到正式部署後才補上。

透過概念驗證比較實際成本

架構圖只能顯示預定邊界,無法證明獨立擴充與故障隔離真的有效。可以選擇一個最有拆分理由的候選模組進行概念驗證(Proof of Concept, POC),並且與模組化單體方案使用相同條件比較:

  1. 使用相同功能案例完成建置、部署、測試與版本更換。
  2. 單獨增加候選功能的處理量,確認能否獨立擴充並改善已定義指標。
  3. 模擬服務停止、回應延遲與部分成功,確認逾時、重試、斷路器及降級結果。
  4. 重複送出相同狀態變更,確認冪等性與並行控制不會產生重複結果。
  5. 在不同服務版本同時存在時執行契約與完整流程測試。
  6. 從對外結果追查所有相關記錄與追蹤資料,確認問題能在限制時間內定位。
  7. 執行保存資料的備份與復原,確認服務邊界沒有遺漏共同狀態。
  8. 記錄部署時間、測試時間、故障恢復時間、人工步驟與新增維護項目。

如果拆分後不能獨立擴充、不能隔離故障,或每次變更仍要同步部署多個服務,就沒有達成原本的架構目標。此時應該保留模組邊界並維持較簡單的部署方式,等重新評估條件成立後再開啟決策。

使用架構決策紀錄保存判斷

完成需求確認與 POC 後,使用架構決策紀錄(Architecture Decision Record, ADR)保存採用或不採用分散式架構的理由。

ADR 欄位 應記錄的內容
決策問題 哪一項穩定、部署或擴充問題需要架構處理
已確認條件 中斷、恢復、處理量、發布、故障隔離與資料一致性要求
候選方案 單一應用程式、模組化單體、部分獨立服務或其他可行方案
邊界與擁有權 每個候選服務的責任、互動契約、資料擁有者及失敗範圍
驗證結果 POC 的容量、故障、部署、測試、復原及問題調查結果
選擇與代價 採用方案、解決的問題、新增工作及仍存在的風險
重新評估條件 發布頻率、處理量、故障影響或責任分工達到何種條件時重新比較

「微服務比較有彈性」或「Kubernetes 可以自動擴充」不足以形成決策。ADR 要能指出哪一項需求、哪一個候選邊界及哪一組驗證結果支持選擇,也要記錄不採用分散式架構的理由。

完成架構判斷的檢查

決定目標系統的部署架構前,可以使用下列問題確認:

  • 穩定需求是否已經改寫成中斷、恢復、保存結果、處理量及變更影響的可檢查條件?
  • 是否已經找出問題來自程式錯誤、相依項目、共同資源、部署單位或並行更新?
  • 是否存在必須獨立部署、獨立擴充、隔離故障或跨地區運作的明確需求?
  • 每個候選服務是否具有清楚責任、穩定契約、資料擁有權與獨立驗收方式?
  • 跨服務呼叫是否具有逾時、有限重試、冪等性、斷路器、隔離與降級規則?
  • 多副本並行更新是否在狀態擁有邊界使用原子操作或適合的衝突控制?
  • 如果採用 Kubernetes,自我修復、負載分流與自動擴充的前提是否已經完成驗證?
  • 開發與維運人員是否能長期負擔服務探索、設定、監控、追蹤、測試及復原工作?
  • POC 是否證明分散式方案的價值高於新增的通訊與維運成本?
  • ADR 是否記錄採用或不採用的理由、已知代價及重新評估條件?

重點整理

  • 系統穩定要先轉換成必要功能、中斷時間、保存結果、處理能力、降級方式與變更影響等可檢查條件。
  • 分散式架構只能處理特定的部署、擴充與故障邊界問題,程式錯誤、復原缺漏及並行更新衝突仍要分別修正。
  • 模組化單體與微服務都能建立清楚責任。微服務提供獨立部署與擴充能力,同時增加通訊、資料一致性、測試及維運成本。
  • 候選服務要具有集中責任、穩定契約、資料擁有權、獨立驗收方式與明確失敗處理,不能只按照程式碼大小拆分。
  • 跨服務互動要定義逾時、有限重試、冪等性、斷路器、艙壁隔離、降級及補償操作,避免局部失敗擴散。
  • 多副本執行時,並行更新應該在狀態擁有邊界使用原子操作、樂觀鎖定、悲觀鎖定或經過完整驗證的分散式鎖定。
  • Kubernetes 可以維持工作負載副本及按照指標調整數量,無法自行修正應用程式錯誤、資料衝突或不正確的容量條件。
  • POC 要以相同條件比較模組化單體與分散式方案,ADR 則保存選擇、代價及重新評估條件。

上一篇
[Day 22] 系統如何部署?應該使用甚麼工具部署?
下一篇
[Day 24] 分散式服務之間的資料要如何同步?
系列文
遠古聖遺物改造工程:遺留系統全面重構實務指南24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言