危險之路:營運主導的微服務如何創造分散式單體系統
執行摘要
從單體應用程式到微服務的旅程充滿了危險的路徑,可能使組織陷入架構的流沙之中。其中最危險的一種反模式是,當營運團隊在微服務教條的驅使下,並以服務網格技術為武器,強行將第一階段的單體系統分解為人為分離的服務時,便會出現這種情況。這種方法創造了我們所謂的「分散式單體」(Distributed Monolith)—— 一個結合了兩種架構模式最壞方面,卻未能帶來任何承諾好處的系統。
本綜合分析探討了由營運主導的分解,加上對服務網格控制的獨佔,如何造成通常比原始單體系統更嚴重的瓶頸。我們將檢視這種反模式所產生的技術、組織和營運上的失敗,並提供識別和避免這些架構陷阱的指引。
兩條路徑:演進 vs. 強制分解
第一階段:傳統單體系統(正確的起點)
在探討危險路徑之前,讓我們先確立一個正常運作的第一階段單體系統是什麼樣子的:
第一階段的主要特徵:
- 單一交易邊界:所有業務邏輯在單一資料庫交易內執行
- ACID保證:完全的一致性、隔離性和持久性
- 簡化的除錯:單一程序、單一資料庫、清晰的錯誤處理
- 可預測的效能:業務邏輯步驟之間沒有網路呼叫
- 明確的所有權:一個團隊、一個程式碼庫、一個部署單元
反模式:具有服務網格的分散式單體
現在,將此與危險路徑——強制分解方法——進行對比:
服務網格控制台的壟斷
營運團隊的鐵腕控制
這種反模式最陰險的方面之一,是營運團隊如何壟斷服務網格控制平面。最初的「我們將管理基礎設施」很快就變成「我們控制所有服務對服務的通訊」。
控制台成為瓶頸:
- 流量路由:每條服務通訊規則都需要營運批准
- 安全策略:開發者無法調整自己服務之間的認證/授權
- 負載平衡:用於金絲雀部署的流量分割被營運流程阻擋
- 可觀測性:監控和追蹤組態由營運團隊控制
- 斷路器:容錯模式需要營運團隊介入
票證佇列的死亡螺旋
YAML組態地獄
習慣於基礎設施即程式碼的營運團隊,通常將服務網格組態視為另一個YAML管理問題。這導致了:
組態漂移的放大:
# 服務A的Istio組態(由營運維護)
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: service-a-routing
spec:
hosts:
- service-a
http:
- match:
- headers:
version:
exact: v1.2.3 # 營運不知道此版本已棄用
route:
- destination:
host: service-a
subset: v1
weight: 100
由營運管理的服務網格YAML的問題:
- 過時的組態:營運不知道服務版本何時變更
- 對業務邏輯的無知:與業務需求不匹配的路由規則
- 安全組態錯誤:過於寬鬆或過於嚴格的策略
- 效能瓶頸:次優的負載平衡和斷路器設定
同步呼叫鏈的噩夢
喪失的交易邊界
這種反模式最具破壞性的方面,是在維持同步呼叫模式的同時,喪失了ACID屬性:
沒有分散式交易
需要手動補償 ServiceA-->>-Client: 錯誤(部分處理後)
補償模式的噩夢
當同步呼叫在鏈的中途失敗時,團隊被迫實作手動補償:
// 反模式:在分散式單體中的手動補償
public class OrderService {
public OrderResult processOrder(Order order) {
// 步驟1:儲存訂單(無法回滾)
orderRepository.save(order);
try {
// 步驟2:透過Istio呼叫庫存服務
InventoryResult inventory = inventoryService.validateInventory(order);
try {
// 步驟3:透過Istio呼叫付款服務
PaymentResult payment = paymentService.processPayment(order);
try {
// 步驟4:透過Istio呼叫履行服務
fulfillmentService.createShipment(order);
return OrderResult.success();
} catch (FulfillmentException e) {
// 手動補償地獄開始
paymentService.refundPayment(order); // 可能失敗
inventoryService.releaseInventory(order); // 可能失敗
orderRepository.markAsFailed(order); // 可能失敗
return OrderResult.failure("履行失敗");
}
} catch (PaymentException e) {
inventoryService.releaseInventory(order); // 可能失敗
orderRepository.markAsFailed(order); // 可能失敗
return OrderResult.failure("付款失敗");
}
} catch (InventoryException e) {
orderRepository.markAsFailed(order); // 可能失敗
return OrderResult.failure("庫存驗證失敗");
}
}
}
營運複雜度的爆炸性增長
除錯分散式故障
過去在單體系統中只是一個簡單的堆疊追蹤,現在變成了分散式除錯的噩夢:
單體系統除錯(簡單):
OrderService.processOrder() line 45
-> validateInventory() line 67
-> PaymentService.charge() line 23
-> DatabaseException: Connection timeout
分散式單體除錯(噩夢):
服務A日誌:「成功呼叫服務B」
服務B日誌:「成功呼叫服務C」
服務C日誌:「付款處理失敗」
Istio日誌:「503 Service Unavailable」
RDS A日誌:「交易已提交」
RDS B日誌:「交易已提交」
RDS C日誌:「交易已回滾」
Kubernetes日誌:「因OOMKilled而重新啟動Pod」
監控與警報的混亂
每個服務都需要自己的監控,但業務交易橫跨所有服務:
- 服務A:監控訂單建立成功率
- 服務B:監控庫存驗證延遲
- 服務C:監控付款處理錯誤
- 服務D:監控履行佇列深度
問題:沒有單一指標能告訴您「訂單處理」是否健康。您需要在4個以上服務之間建立關聯,每個服務都有不同的SLI,由不同團隊擁有,並由營運團隊設定。
團隊動態的災難
逆康威定律
強制分解並非圍繞業務能力組織團隊,而是創造了人為的團隊邊界:
之前(單體):
- 訂單團隊:擁有整個訂單處理流程
- 責任明確:成功或失敗是明確的
- 業務對齊:團隊了解完整的客戶旅程
之後(分散式單體):
- 庫存團隊:只知道庫存水平
- 付款團隊:只知道交易
- 履行團隊:只知道運送
- 整合團隊:試圖協調所有人(但失敗了)
指責遊戲的開始
當分散式單體失敗時(它一定會),互相指責變得不可避免:
- 庫存團隊:「我們回傳了有效的庫存水平」
- 付款團隊:「我們成功處理了付款」
- 履行團隊:「我們從未收到運送請求」
- 營運團隊:「服務網格運作正常」
- 整合團隊:「這不是我們的錯,是時序問題」
效能下降的現實
網路延遲的倍增
過去的程序內方法呼叫,變成了網路呼叫:
第一階段單體系統效能:
- 訂單處理:50毫秒(全在記憶體中)
- 資料庫交易:10毫秒
- 總計:60毫秒
分散式單體效能:
- 服務A → 服務B:20毫秒 + 15毫秒處理
- 服務B → 服務C:25毫秒 + 30毫秒處理
- 服務C → 服務D:15毫秒 + 20毫秒處理
- 總計:125毫秒(慢2倍,不含故障)
重試風暴問題
當服務失敗時,同步的特性會產生重試風暴:
# Istio重試組態(由營運團隊管理)
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: payment-service-retries
spec:
host: payment-service
trafficPolicy:
outlierDetection:
consecutiveErrors: 3
interval: 30s
baseEjectionTime: 30s
connectionPool:
tcp:
maxConnections: 100
http:
http1MaxPendingRequests: 50
maxRequestsPerConnection: 10
retryPolicy:
attempts: 3 # 這會放大故障!
perTryTimeout: 5s
問題:當付款服務遇到困難時,每個上游服務都會重試3次,在最糟糕的時機造成3倍的負載放大。
演進之路:本該如何發生
正確的方式:事件驅動的演進
與其強制分解,正確的演進路徑應遵循以下階段:
正確的演進階段:
- 第二階段:引入帶有訊息佇列的非同步處理
- 第三階段:實作事件溯源和最終一致性
- 第四階段:建立平台即服務的事件串流基礎設施
- 第五階段:透過領域事件實現真正的服務獨立性
識別模式:您是否正走在危險之路上?
技術警訊
服務網格指標:
- 營運團隊控制所有服務網格組態
- 開發者需提交票證以進行路由變更
- 服務網格組態儲存在基礎設施儲存庫中
- 服務間的策略(例如重試、逾時)由營運而非開發者定義
架構指標:
- 多個服務被同步呼叫以完成單一業務交易
- 服務擁有自己的資料庫,但仍以同步方式通訊
- 手動實作複雜的補償邏輯(例如Saga模式)來處理故障
- 新功能需要跨多個「獨立」服務進行程式碼變更
組織警訊
- 開發者抱怨「營運團隊太慢」
- 營運團隊抱怨「開發者不遵守我們的流程」
- 發生故障時,開始互相指責
- 開發團隊對其服務在生產環境中的執行情況缺乏可見性
- 部署新服務需要經過手動的營運閘門
回復之路:如何擺脫分散式單體
雖然擺脫這個架構死胡同很困難,但並非不可能。
步驟1:奪回控制權 - 分散式治理
營運團隊必須放棄集中控制,採納聯合治理模型。
分散式策略管理:
- 開發團隊擁有:每個開發團隊定義並擁有其服務特定的服務網格組態(重試、逾時、路由)。
- GitOps:這些組態儲存在應用程式的程式碼儲存庫中,而非基礎設施儲存庫。
- 營運提供的平台:營運團隊提供一個自助服務平台,供開發者部署和管理策略。他們成為推動者,而非守門人。
步驟2:切斷同步呼叫 - 引入非同步事件
每個同步呼叫都是一個潛在的故障點。回復之路始於引入非同步、事件驅動的通訊。
事件驅動的優點:
- 真正的解耦:服務不需要知道彼此的存在
- 彈性:即使付款服務中斷,訂單仍可被接受並稍後處理
- 可擴展性:每個服務都可以獨立擴展
- 開發者自主性:團隊可以在不阻塞其他團隊的情況下新增功能
步驟3:重組團隊 - 回歸業務領域
讓康威定律為您效力。圍繞業務領域重組團隊。
- 訂單接收團隊:負責接收訂單並發布
OrderPlaced
事件 - 庫存管理團隊:訂閱
OrderPlaced
事件並管理庫存 - 付款處理團隊:訂閱
OrderPlaced
事件並處理付款
結論:要演進,不要強迫
由營運主導的微服務轉型是一條通往地獄的善意之路。它創造了一個比原始單體更難管理、效能更差、更脆弱的分散式單體。
真正的彈性和敏捷性,並非來自於調整服務網格的設定,而是來自於對架構原則的根本性轉變。
關鍵要點:
- 同步呼叫是分散式單體的症狀。
- 事件驅動架構是實現真正解耦的關鍵。
- 營運團隊應該是推動者,而非守門人。
- 開發者的自主性是高效團隊的先決條件。
避免走上危險之路,選擇一條受控的、事件驅動的演進之路。您的團隊和您的客戶將會感謝您。