可擴展架構設計#
可擴展性是指系統為了應對將來需求變化而提供的擴展能力。當新需求出現時,系統不需要或只需少量修改就可以支援。
可擴展性的複雜度#
設計可擴展系統需要同時滿足兩個條件,而這兩個條件本身都很複雜:
| 維度 | 預測變化 | 應對變化 |
|---|---|---|
| 挑戰 1 | 不能每個點都考慮 | 封裝變化 |
| 挑戰 2 | 不能完全不考慮 | 隔離變化 |
| 挑戰 3 | 預測可能出錯 | 接口設計 |
軟件的本質是變化。對於建築來說,永恆是主題;對於軟件來說,變化才是主題。
預測變化:2 年法則#
預測的困境#
- 過度預測:每個設計點都考慮擴展性,架構師不堪重負,系統過度設計
- 不預測:剛上線就要重構,前期投入白費
- 預測錯誤:期望的需求不來,做的擴展沒用
2 年法則#
只預測 2 年內的可能變化,不要試圖預測 5 年甚至 10 年後的變化。
原因:
- 變化快的行業,能預測 2 年已經足夠
- 變化慢的行業,預測 2 年和 5 年結果差不多
2 年法則實例
場景:設計一個電商系統
需要考慮的(2 年內可能發生):
- 支援更多支付方式(微信、支付寶、銀聯)
- 接入更多物流商
- 商品分類增加
不需要考慮的(超出 2 年預測):
- 元宇宙購物
- 腦機接口下單
- 支援外星貨幣
應對變化:兩種基本方案#
方案一:分層架構(隔離變化)#
將不變的部分封裝在「穩定層」,將變化封裝在「變化層」。
方式一:穩定層依賴變化層
flowchart TB
A["業務邏輯<br/>(穩定層)"] -->|呼叫| B["資料訪問層<br/>(MySQL/Oracle)<br/>(變化層)"]
style A fill:#c8e6c9
style B fill:#fff3e0方式二:變化層依賴穩定層
flowchart TB
A["接口適配層<br/>(XML/JSON/PB)<br/>(變化層)"] -->|呼叫| B["業務邏輯<br/>(穩定層)"]
style A fill:#fff3e0
style B fill:#c8e6c9分層的複雜性:
- 如何劃分變化層和穩定層?不同人有不同理解
- 如何設計層間接口?接口要足夠穩定
方案二:抽象設計(封裝變化)#
提煉出「抽象層」和「實作層」,通過抽象接口封裝變化。
flowchart TB
A["抽象層(規則)<br/>定義接口、處理流程、擴展點"]
A --> I1["實作 A"]
A --> I2["實作 B"]
A --> I3["實作 C"]
style A fill:#e3f2fd
style I1 fill:#c8e6c9
style I2 fill:#c8e6c9
style I3 fill:#c8e6c9典型應用:設計模式、規則引擎
設計模式範例:策略模式
// 抽象層:定義支付接口
type PaymentStrategy interface {
Pay(amount float64) error
}
// 實作層:不同的支付實作
type WechatPayment struct{}
func (w *WechatPayment) Pay(amount float64) error {
// 微信支付邏輯
}
type AlipayPayment struct{}
func (a *AlipayPayment) Pay(amount float64) error {
// 支付寶支付邏輯
}
// 業務程式碼(穩定)
type Order struct {
payment PaymentStrategy
}
func (o *Order) Checkout(amount float64) error {
return o.payment.Pay(amount) // 不關心具體實作
}新增支付方式時,只需添加新的實作,不需要修改業務程式碼。
1 寫 2 抄 3 重構原則#
Martin Fowler 的「Rule of Three」:事不過三,三則重構。
flowchart TD
A["第 1 次需求<br/>直接實作,不考慮擴展"] --> B["第 2 次類似需求<br/>可以拷貝修改"]
B --> C["第 3 次類似需求<br/>重構,抽取公共部分"]
style A fill:#e3f2fd
style B fill:#fff3e0
style C fill:#c8e6c9實例:對接第三方錢包#
| 階段 | 做法 | 原因 |
|---|---|---|
| 1 寫 | 直接對接微信支付 | 業務能否做起來還不確定 |
| 2 抄 | 拷貝程式碼,改為支付寶 | 快速上線更重要 |
| 3 重構 | 抽象支付接口,統一封裝 | 已有模式,值得抽象 |
不要一開始就過度設計。等到真正出現重複時再抽象,這時你對需求的理解也更深刻。
可擴展架構模式#
分層架構#
最常見的可擴展架構,通過分層隔離關注點。
flowchart TB
subgraph Layers["分層架構"]
L1["表現層 (Presentation)"]
L2["業務層 (Business)"]
L3["持久層 (Persistence)"]
L4["資料層 (Database)"]
end
L1 --> L2 --> L3 --> L4
style L1 fill:#e3f2fd
style L2 fill:#fff3e0
style L3 fill:#e8f5e9
style L4 fill:#f3e5f5優點:
- 層次清晰,職責明確
- 各層可以獨立演進
- 便於團隊分工
缺點:
- 層數過多時效能下降
- 可能出現「瀑布式」修改(改一個功能要改所有層)
SOA 架構#
面向服務的架構,將系統拆分為獨立的服務。
flowchart TB
ESB["ESB (企業服務總線)"]
ESB --> S1["用戶服務"]
ESB --> S2["訂單服務"]
ESB --> S3["支付服務"]
ESB --> S4["庫存服務"]
ESB --> S5["物流服務"]
style ESB fill:#fff3e0特點:
- 服務獨立部署、獨立演進
- 通過 ESB 進行服務編排和協議轉換
- 適合大型企業應用
微服務架構#
SOA 的輕量化演進,去掉中心化的 ESB。
flowchart TB
subgraph Services[微服務集群]
U[用戶服務] <--> O[訂單服務]
O <--> P[支付服務]
P <--> I[庫存服務]
end
U --> SD[服務發現<br/>Consul]
O --> SD
P --> SD
I --> SD微服務 vs SOA:
| 維度 | SOA | 微服務 |
|---|---|---|
| 服務粒度 | 粗 | 細 |
| 通信方式 | ESB(重量級) | 輕量級協議(HTTP/gRPC) |
| 資料管理 | 共享資料庫 | 每個服務獨立資料庫 |
| 治理方式 | 集中式 | 去中心化 |
微服務拆分原則
1. 基於業務拆分
- 按業務領域劃分:用戶、商品、訂單、支付
- 每個服務對應一個限界上下文
2. 基於穩定性拆分
- 穩定的核心業務獨立
- 變化頻繁的邊緣業務獨立
3. 基於可靠性拆分
- 核心服務和非核心服務分離
- 避免非核心服務故障影響核心服務
4. 基於效能拆分
- 高頻訪問的功能獨立
- 便於針對性最佳化和擴容
水平擴展 vs 垂直擴展#
垂直擴展(Scale Up)#
提升單機效能:加 CPU、加內存、換 SSD。
| 狀態 | 組態 |
|---|---|
| Before | 4 核 8G |
| After | 32 核 128G |
優點:簡單,不需要修改架構 缺點:有上限,成本指數增長
水平擴展(Scale Out)#
增加更多機器。
| 狀態 | 伺服器數量 |
|---|---|
| Before | 1 台 |
| After | 4 台(可無限擴展) |
優點:理論上無上限 缺點:需要架構支援,引入分布式複雜性
如何選擇#
| 場景 | 推薦方案 | 原因 |
|---|---|---|
| 初創階段 | 垂直擴展 | 快速、簡單 |
| 效能瓶頸明確 | 垂直擴展 | 針對性解決 |
| 需要高可用 | 水平擴展 | 冗餘 + 負載均衡 |
| 長期發展 | 水平擴展 | 突破單機上限 |
資料擴展:分庫分表#
當單庫單表無法支撐時,需要進行資料層面的水平擴展。
分庫分表策略#
| 維度 | 垂直拆分 | 水平拆分 |
|---|---|---|
| 方式 | 按業務拆分資料庫 | 按規則分散資料 |
| 範例 | 用戶庫、訂單庫 | Hash、Range、時間 |
| 效果 | 降低單庫壓力 | 突破單表資料量限制 |
分片策略#
| 策略 | 方式 | 優點 | 缺點 |
|---|---|---|---|
| Hash | hash(key) % N | 分布均勻 | 擴容麻煩 |
| Range | 按範圍劃分 | 擴容簡單 | 可能不均勻 |
| 時間 | 按月/年分表 | 歷史資料歸檔方便 | 熱點集中 |
| 映射表 | 查映射關係 | 靈活 | 多一次查詢 |
分庫分表帶來的問題
問題一:跨庫 Join
-- 這個查詢在分庫後無法直接執行
SELECT * FROM orders o
JOIN users u ON o.user_id = u.id解決方案:
- 應用層組裝
- 資料冗餘
- 使用搜索引擎
問題二:分布式事務
- 2PC:效能差,實作複雜
- TCC:業務侵入性強
- 最終一致性:通過消息隊列
問題三:全局 ID
- 資料庫自增 ID 會衝突
- 解決方案:UUID、Snowflake、資料庫號段
問題四:分頁查詢
-- 分頁變得複雜
SELECT * FROM orders ORDER BY create_time LIMIT 100 OFFSET 10000解決方案:
- 禁止深度分頁
- 使用游標分頁
擴展性設計檢查清單#
在設計架構時,可以用以下問題檢查擴展性:
| 檢查項 | 問題 |
|---|---|
| 容量擴展 | 用戶量增長 10 倍,架構能否支撐? |
| 功能擴展 | 新增一個功能需要改動多少模塊? |
| 技術演進 | 如果要換資料庫/框架,改動範圍多大? |
| 團隊擴展 | 新人加入,是否能快速理解和開發? |
| 運維擴展 | 是否支援灰度發布、快速回滾? |
本章小結#
| 主題 | 核心思想 | 實踐建議 |
|---|---|---|
| 預測變化 | 只看 2 年內 | 不過度設計,也不忽視擴展 |
| 應對變化 | 隔離或封裝 | 分層架構 + 抽象設計 |
| 重構時機 | 1 寫 2 抄 3 重構 | 等到重複出現再抽象 |
| 擴展模式 | 分層/SOA/微服務 | 根據規模選擇 |
| 資料擴展 | 分庫分表 | 慎重,引入複雜性 |
可擴展性設計的核心原則:
- 預測要適度,不多也不少
- 封裝變化,暴露穩定接口
- 簡單優先,複雜度是代價