服務拆分是微服務架構設計的核心議題:
- 拆分得當:帶來敏捷性與可維護性的提升
- 拆分不當:引入不必要的分散式複雜度
本章涵蓋何時拆分、如何拆分,以及拆分後的服務分層策略。
何時進行服務化拆分#
項目發展階段分析#
第一階段:快速驗證期
項目初期的主要目標是快速開發和驗證想法,證明產品思路是否可行。這個階段:
- 功能設計一般不會太複雜
- 開發採取快速迭代的方式
- 架構不適合過度設計
將所有功能打包部署在一起,集中地進行開發、測試和運維,對於項目起步階段是最高效也是最節省成本的方式。
第二階段:功能擴張期
當可行性驗證通過,功能進一步迭代:
- 需要加入越來越多的新特性
- 需要大規模地擴張開發人員
- 多個功能模組混雜在一起開發、測試和部署會導致相互影響
拆分的信號指標#
| 信號 | 說明 |
|---|---|
| 團隊規模超過 10 人 | 同時進行開發的人員過多,協調成本急劇上升 |
| 發布頻率衝突 | 不同功能的發布週期不同,但被迫一起發布 |
| 故障影響擴大 | 一個模組的問題導致全站不可用 |
| 技術棧受限 | 某些功能需要不同的技術棧但無法獨立選擇 |
經典案例:某影片 App 因短時間內某個付費影片訪問量巨大,超過伺服器承載能力。由於付費影片和免費影片的服務部署在一起,導致免費影片也受波及,幾乎全站崩潰。
服務化拆分的兩種姿勢#
縱向拆分:按業務維度#
縱向拆分是從業務維度進行拆分,標準是按照業務的關聯程度來決定:
- 關聯比較密切的業務適合拆分為一個微服務
- 功能相對比較獨立的業務適合單獨拆分為一個微服務
案例:社交 App 拆分
單體應用
├── 首頁資訊流
├── 評論功能
├── 訊息通知
└── 個人主頁
↓ 縱向拆分
獨立服務
├── 資訊流服務
├── 評論服務
├── 訊息服務
└── 個人主頁服務橫向拆分:按公共功能維度#
橫向拆分是從公共且獨立功能維度拆分,標準是:
- 是否有公共的被多個其他服務呼叫
- 依賴的資源獨立不與其他業務耦合
案例:用戶暱稱功能
無論是首頁資訊流、評論、訊息箱還是個人主頁,都需要顯示用戶的暱稱。如果用戶暱稱功能有產品需求變更:
- 不拆分:需要上線幾乎所有的服務,成本很高
- 拆分後:只需要上線用戶暱稱服務,其他服務不受影響
拆分決策矩陣
| 條件 | 縱向拆分 | 橫向拆分 |
|---|---|---|
| 業務獨立性強 | 適用 | 不適用 |
| 被多個業務共用 | 不適用 | 適用 |
| 變更頻率不同 | 適用 | 適用 |
| 資源依賴獨立 | 適用 | 適用 |
| 技術棧需求不同 | 適用 | 適用 |
服務拆分的前置條件#
業務系統引入新技術必然會帶來架構的複雜度提升。在具體決策前,必須認識到新架構會帶來哪些新的問題,這些問題團隊是否能夠解決。
必須解決的五個問題#
1. 服務如何定義
單體應用中不同功能模組通過類庫方式提供功能。微服務中每個服務執行在各自的行程中,需要通過介面來描述服務:
- 介面名
- 介面參數
- 介面返回值
- 其他描述資訊
2. 服務如何發布和訂閱
拆分為微服務獨立部署後:
- 服務提供者該如何對外暴露自己的地址
- 服務呼叫者該如何查詢所需要呼叫的服務的地址
這需要一個註冊中心來記錄每個服務提供者的地址供服務呼叫者查詢。
3. 服務如何監控
通常對於一個服務,最關心的指標:
- QPS:呼叫量,每秒查詢次數
- AvgTime:平均耗時
- P999:99.9% 的請求效能在多少毫秒以內
需要一種通用的監控方案,覆蓋業務埋點、資料收集、資料處理到資料展示的全鏈路功能。
4. 服務如何治理
拆分為微服務架構後,服務數量變多、依賴關係變複雜:
- 一個服務效能有問題時,依賴的服務都會受影響
- 需要設定呼叫效能閾值,超過時直接返回(熔斷)
- 這是服務治理最常用的手段之一
5. 故障如何定位
一次用戶呼叫可能依賴多個服務,每個服務部署在不同的節點上:
- 需要對用戶請求進行標記
- 在多個依賴的服務系統中繼續傳遞
- 串聯所有路徑,從而進行故障定位
服務分層策略#
雙層服務分層體系#
為了便於理解與架構溝通,建議採用簡潔清晰的「雙層服務分層體系」:
flowchart TB
subgraph 外部接入
E1[PC]
E2[移動端]
E3[第三方]
end
subgraph 第二層["第二層:聚合服務 (Aggregation)"]
A1[PC 聚合服務]
A2[移動聚合服務]
A3[開放平台]
end
subgraph 第一層["第一層:基礎服務 (Basic Services)"]
B1[商品服務]
B2[訂單服務]
B3[用戶服務]
B4[支付服務]
end
E1 --> A1
E2 --> A2
E3 --> A3
A1 --> B1 & B2 & B3
A2 --> B1 & B2 & B3
A3 --> B2 & B4
style 外部接入 fill:#fff3e0
style 第二層 fill:#e3f2fd
style 第一層 fill:#e8f5e9第一層:基礎服務 (Basic Services)#
這一層位於基礎設施之上,是平台最為基礎的支撐服務。
特徵:
- 原子性:每個服務完成一個獨立的業務功能
- 通用性:可以被多個上層服務呼叫
- 穩定性:介面相對穩定,不頻繁變更
典型案例(電商):
- 商品服務
- 訂單服務
- 用戶服務
- 購物車服務
業界常見術語對照
不同公司對這一層有不同的稱呼,但本質相同:
- 核心領域服務 (Core Domain Services)
- 公共服務 (Public Services)
- 中間層服務 (Middle Tier Services):Netflix 體系中的稱呼
第二層:聚合服務 (Aggregation Services)#
為什麼需要聚合服務?因為底層服務傾向於通用與抽象,而外部接入端(PC、手機)的需求是具體且多變的。
核心功能一:適配與裁剪
不同終端設備對資料的呈現需求不同:
- PC 端:螢幕較大,展示資訊豐富
- 移動端:螢幕較小、帶寬有限,需要對基礎服務返回的 Payload 進行「裁剪」
核心功能二:服務聚合
單一頁面往往需要展示來自多個基礎服務的資訊:
flowchart TD
subgraph before["無聚合層:多次網路請求"]
C1["用戶端"]
C1 -- "請求 1" --> S1["商品服務"]
C1 -- "請求 2" --> S2["分類服務"]
C1 -- "請求 3" --> S3["購物車服務"]
end
subgraph after["有聚合層:單次請求"]
C2["用戶端"]
A["聚合服務"]
C2 -- "單次請求" --> A
A -- "內網並行" --> S4["商品服務"]
A -- "內網並行" --> S5["分類服務"]
A -- "內網並行" --> S6["購物車服務"]
end
style before fill:#fff3e0
style after fill:#e8f5e9有些公司稱此層為 Adapter Service(適配服務),Netflix 則稱之為 Edge Service(邊界服務),因為它處於公司內網與外網的邊界上。
拆分粒度的把握#
拆分原則#
並不是功能拆分得越細越好。過度的拆分反而會讓服務數量膨脹變得難以管理。
建議的標準:按照每個開發人員負責不超過 3 個大的服務為標準。
根據開發人員的總人數來決定拆分粒度:
- 10 人團隊 ➡️ 約 30 個服務
- 50 人團隊 ➡️ 約 150 個服務
避免的反模式#
反模式一:過早拆分
在對業務領域理解不深時就進行拆分,導致:
- 服務邊界劃分錯誤
- 頻繁的跨服務重構
- 分散式系統的複雜度卻沒有帶來相應收益
反模式二:按技術層拆分
錯誤地按照技術層次(如 DAO 層、Service 層、Controller 層)進行拆分,而不是按業務能力拆分。
反模式三:忽視資料邊界
拆分服務時沒有同時考慮資料的拆分,導致:
- 多個服務共享同一個資料庫
- 資料庫成為耦合點
- 無法獨立擴展和部署
正確的拆分步驟
- 理解業務領域:深入理解業務流程和領域邊界
- 識別有界上下文:確定每個服務的資料邊界
- 定義服務介面:明確服務間的交互契約
- 評估依賴關係:確保服務間的依賴是合理的
- 規劃資料遷移:制定資料拆分和遷移策略
- 漸進式拆分:從風險最低的模組開始,逐步推進
實踐建議#
拆分決策清單#
在決定拆分某個模組之前,回答以下問題:
| 問題 | 期望答案 |
|---|---|
| 這個模組的業務邊界是否清晰? | 是 |
| 團隊對這個領域的理解是否足夠深入? | 是 |
| 拆分後服務的介面是否已經明確定義? | 是 |
| 相關的資料是否可以獨立? | 是 |
| 團隊是否具備分散式系統的運維能力? | 是 |
| 是否有足夠的監控和追蹤基礎設施? | 是 |
漸進式拆分策略#
推薦的做法是漸進式拆分,而不是一步到位的大爆炸式重構。
步驟一:絞殺者模式 (Strangler Pattern)
在單體應用外部建置新的微服務,逐步將流量從舊系統遷移到新系統。
flowchart LR
subgraph phase1["階段一:全部流量走舊系統"]
U1["用戶"] --> M1["單體應用"]
end
subgraph phase2["階段二:代理分流"]
U2["用戶"] --> P["代理層"]
P -- "舊功能" --> M2["單體應用"]
P -- "新功能" --> MS["新微服務"]
end
subgraph phase3["階段三:完成遷移"]
U3["用戶"] --> MS2["微服務"]
M3["單體應用\n(退役)"]
end
phase1 --> phase2 --> phase3
style phase1 fill:#fff3e0
style phase2 fill:#e3f2fd
style phase3 fill:#e8f5e9
style M3 fill:#ffcdd2,stroke:#e57373,stroke-dasharray: 5 5步驟二:從邊緣開始
選擇風險最低、依賴最少的模組開始拆分,積累經驗。
步驟三:建立基礎設施
在大規模拆分之前,確保:
- 註冊中心已就緒
- 監控系統已就緒
- 日誌系統已就緒
- CI/CD 流水線已就緒
步驟四:持續驗證
每完成一個服務的拆分:
- 驗證功能正確性
- 驗證效能指標
- 評估運維複雜度
- 總結經驗教訓
以 DDD 落地服務邊界#
前面談的是「何時拆、拆多細」,本節談「邊界劃在哪」。領域驅動設計 (DDD, Domain-Driven Design) 提供了一套把業務語言對映到服務邊界的方法。其戰略概念(有界上下文、子域、上下文映射)的完整理論請見 DDD 戰略設計;這裡聚焦如何把它落到微服務拆分。
子域分類:決定投資優先序#
並非所有業務都同等重要。DDD 把問題空間切成三類子域 (Subdomain),這直接影響「哪些該自研、哪些該外購、哪些該優先微服務化」:
| 子域類型 | 定義 | 對微服務的啟示 |
|---|---|---|
| 核心子域 (Core) | 企業的競爭優勢所在 | 投入最強團隊自研,優先獨立成服務 |
| 支撐子域 (Supporting) | 業務需要但非差異化 | 可自研但投入有限,邊界清楚即可 |
| 通用子域 (Generic) | 通用問題(如認證、計費) | 優先採購或用現成方案,不必重造輪子 |
拆分順序應從「核心子域」與「邊界最清晰」的子域下手,而不是從技術上最容易剝離的模組下手。把工程精力投在最有商業價值的地方。
聚合根與服務邊界映射#
有界上下文 (Bounded Context) 是劃定服務邊界的最佳候選——一個有界上下文通常對應一個微服務,內部語言一致、模型自洽。而上下文內部的聚合 (Aggregate) 則決定了交易一致性的最小單元:
- 一個有界上下文 ➡️ 一個微服務:服務擁有自己的資料庫,外部只能透過 API 訪問
- 一個聚合 ➡️ 一個一致性邊界:聚合根 (Aggregate Root) 是唯一的修改入口,聚合內保證強一致,跨聚合只能最終一致
- 跨聚合/跨服務的協作 ➡️ 領域事件:不在同一交易內更新多個聚合,改用事件驅動(見 服務通信 的 Saga)
flowchart TB
subgraph BC1["有界上下文:訂單 → 訂單服務"]
direction TB
AR1["聚合根:Order"]
AR1 --> OL["OrderLine"]
AR1 -. "一致性邊界" .- OL
end
subgraph BC2["有界上下文:庫存 → 庫存服務"]
AR2["聚合根:Inventory"]
end
BC1 -- "領域事件\nOrderCreated" --> BC2
style BC1 fill:#e3f2fd
style BC2 fill:#e8f5e9一條核心規則:一個交易只修改一個聚合。跨多個聚合(尤其跨服務)的一致性不靠分散式交易硬撐,而靠領域事件達成最終一致。這也是後面 Strangler 遷移時資料同步的理論基礎。
防腐層 (ACL) 隔離舊模型#
當新服務必須與遺留系統或外部系統互動,但對方的模型混亂、概念與我方不一致時,直接依賴會讓對方的「壞模型」污染自己乾淨的領域模型。防腐層 (Anti-Corruption Layer, ACL) 是一層轉譯適配器,把外部模型翻譯成本上下文的語言。
flowchart LR
NEW["新服務\n(乾淨領域模型)"] --> ACL["防腐層 ACL\n模型轉譯"]
ACL --> LEGACY["遺留系統\n(混亂舊模型)"]
style NEW fill:#e8f5e9
style ACL fill:#fff3e0,stroke:#ff9800,stroke-width:2px
style LEGACY fill:#ffcdd2ACL 在絞殺者遷移中尤其關鍵:遷移期間新舊系統長期並存,ACL 讓新服務不必遷就舊資料結構,也讓未來退役舊系統時的影響面被收斂在這一層。
Strangler Fig:漸進遷移的完整生命週期#
絞殺者模式(Strangler Fig,得名於纏繞宿主樹直到取而代之的絞殺榕)是把單體安全演進到微服務的主流策略。相較於「大爆炸式重寫」,它的價值在於每一步都可上線、可回滾、風險可控。
完整生命週期分為四個階段:
flowchart TB
subgraph P1["階段一:分流 (Facade)"]
U1["用戶"] --> F1["門面/路由層"]
F1 --> MONO1["單體(全功能)"]
end
subgraph P2["階段二:資料同步並存"]
U2["用戶"] --> F2["門面/路由層"]
F2 -- "舊功能" --> MONO2["單體"]
F2 -- "新功能(灰度)" --> SVC2["新微服務"]
MONO2 <-. "雙寫/CDC 同步" .-> SVC2
end
subgraph P3["階段三:切流完成"]
U3["用戶"] --> F3["門面/路由層"]
F3 -- "100% 新流量" --> SVC3["新微服務"]
MONO3["單體\n(僅讀/待退役)"]
end
subgraph P4["階段四:退役"]
U4["用戶"] --> SVC4["新微服務"]
MONO4["單體\n(已下線)"]
end
P1 --> P2 --> P3 --> P4
style P1 fill:#fff3e0
style P2 fill:#e3f2fd
style P3 fill:#e8f5e9
style P4 fill:#f3e5f5
style MONO3 fill:#ffe0b2,stroke-dasharray: 5 5
style MONO4 fill:#ffcdd2,stroke:#e57373,stroke-dasharray: 5 5各階段重點與回滾策略#
| 階段 | 核心動作 | 資料策略 | 回滾方式 |
|---|---|---|---|
| 一、分流 | 建門面/路由層攔截所有請求,初期全轉發給單體 | 暫無遷移,先建立攔截點 | 移除路由層即回到原狀 |
| 二、並存 | 抽離首個有界上下文成服務,灰度導流 | 雙寫或 CDC 同步,新舊資料並存 | 路由切回單體,棄用新庫 |
| 三、切流 | 流量逐步調至 100% 新服務,舊邏輯停止寫入 | 以新服務資料為準,舊庫轉唯讀 | 路由比例回調,仍可退回階段二 |
| 四、退役 | 移除單體中對應程式碼與資料表 | 歸檔舊資料後下線 | 已不可逆,退役前須確認穩定期足夠 |
階段二的「資料同步」是整個遷移最危險的環節。雙寫容易因失敗造成新舊不一致;較穩健的做法是用變更資料捕獲 (CDC) 從單體資料庫單向同步到新服務(細節見 服務通信 的 Transactional Outbox 與 CDC)。同步方向、衝突處理、回填策略都必須在切流前驗證完畢。
流量分流的灰度節奏#
切流不是一刀切,而是分階段放量並隨時觀測:
- 影子流量 (Shadow):複製真實請求到新服務但不採用其結果,僅比對差異
- 小比例灰度:1% ➡️ 5% ➡️ 25% 逐步放量,每步觀測錯誤率與延遲
- 全量切換:確認指標穩定後切到 100%
- 穩定觀察期:保留舊系統一段時間作為回滾保險,再進入退役
絞殺者模式的精髓不是「技術上如何路由」,而是「每一步都把風險切成可回滾的小塊」。沒有可靠的監控、灰度與回滾機制,就不該啟動遷移。這與第 01 章的「單塊優先、漸進演化」原則一脈相承。