高效能架構設計#
高效能是架構設計中最常見的需求之一。本章介紹高效能架構的核心模式,包括快取、非同步處理和負載均衡。
高效能的複雜度來源#
高效能帶來的複雜度主要體現在兩個層面:
| 複雜度維度 | 單機複雜度 | 集群複雜度 |
|---|---|---|
| 核心問題 | 進程/線程模型、I/O 模型、並發控制、資源管理 | 任務分配(負載均衡)、任務分解(服務拆分)、狀態管理、資料一致性 |
架構設計決定了系統效能的上限,實作細節決定了系統效能的下限。如果架構設計沒有做到高效能,後面的最佳化空間是有限的。
單機高效能模式#
並發模型#
並發模型有兩個關鍵設計點:
- I/O 模型:阻塞、非阻塞、同步、異步
- 進程模型:單進程、多進程、多線程
| 模式 | 特點 | 適用場景 | 代表產品 |
|---|---|---|---|
| PPC (Process Per Connection) | 每連接一個進程 | 連接數少,處理邏輯重 | 傳統 CGI |
| TPC (Thread Per Connection) | 每連接一個線程 | 連接數中等 | 傳統 Servlet |
| Reactor | 非阻塞 I/O + 多路復用 | 高並發連接 | Nginx, Redis |
| Proactor | 異步 I/O | 高並發 + 重計算 | Windows IOCP |
Reactor 模式詳解
Reactor 模式的核心是「I/O 多路復用 + 非阻塞 I/O」,有三種典型實作:
1. 單 Reactor 單進程/線程
┌─────────────────────────────────┐
連接請求 │ Reactor │
─────────→ │ ┌─────────┐ ┌───────────┐ │
│ │Acceptor │ │ Handler │ │
I/O 請求 │ └────┬────┘ └─────┬─────┘ │
─────────→ │ │ │ │
│ └────────┬──────┘ │
│ ↓ │
│ ┌──────────┐ │
│ │ 處理邏輯 │ │
│ └──────────┘ │
└─────────────────────────────────┘- 代表:Redis
- 優點:簡單,無需考慮並發問題
- 缺點:無法利用多核 CPU
2. 單 Reactor 多線程
┌──────────────────────────────────────────┐
│ Reactor │
請求 │ ┌─────────┐ ┌─────────────────────┐ │
─────────→ │ │Acceptor │ │ Handler Pool │ │
│ └────┬────┘ │ ┌───┐ ┌───┐ ┌───┐ │ │
│ │ │ │ H │ │ H │ │ H │ │ │
│ │ │ └───┘ └───┘ └───┘ │ │
│ └────────→│ │ │
│ └─────────────────────┘ │
└──────────────────────────────────────────┘- 優點:可以利用多核 CPU
- 缺點:Reactor 可能成為瓶頸
3. 多 Reactor 多進程/線程
┌──────────────────────────────────────────┐
│ Main Reactor │
連接請求 │ ┌─────────┐ │
─────────→ │ │Acceptor │ │
│ └────┬────┘ │
│ │ 分發連接 │
│ ↓ │
│ ┌──────────────────────────────────┐ │
│ │ Sub Reactors │ │
│ │ ┌──────┐ ┌──────┐ ┌──────┐ │ │
I/O 請求 │ │ │ R1 │ │ R2 │ │ R3 │ │ │
─────────→ │ │ │ + H │ │ + H │ │ + H │ │ │
│ │ └──────┘ └──────┘ └──────┘ │ │
│ └──────────────────────────────────┘ │
└──────────────────────────────────────────┘- 代表:Nginx, Memcached
- 優點:充分利用多核,Reactor 不會成為瓶頸
- 缺點:實作複雜
快取架構#
快取是高效能架構中最常用的技術,基本原理是將可能重複使用的資料放到記憶體中,一次生成、多次使用。
快取帶來的問題#
雖然快取能大幅提升效能,但也引入了架構複雜性:
| 問題 | 描述 | 解決方案 |
|---|---|---|
| 快取穿透 | 查詢不存在的資料,每次都打到 DB | 空值快取、布隆過濾器 |
| 快取雪崩 | 大量快取同時過期 | 更新鎖、後台更新、隨機過期時間 |
| 快取熱點 | 熱門資料集中在少數快取節點 | 多副本、本地快取 |
快取穿透詳解#
場景一:資料確實不存在
用戶 ────→ 快取(無) ────→ DB(無) ────→ 返回空
│ │
└──────────── 每次都穿透 ←─────────────┘解決方案:
// 查詢資料
data := cache.Get(key)
if data == nil {
data = db.Query(key)
if data == nil {
// 關鍵:緩存空值,設置較短過期時間
cache.Set(key, EMPTY_VALUE, 60)
} else {
cache.Set(key, data, 3600)
}
}場景二:快取生成耗時長
例如電商分頁,爬蟲遍歷所有頁面時,大量冷門分頁的快取已過期,需要重新從 DB 生成。
這種情況沒有完美解決方案。常見做法是識別爬蟲並限流,或做好監控及時處理。
快取雪崩詳解#
快取大量過期
│
▼
┌─────────────────────────────────┐
│ 大量請求湧入 │
│ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │
│ │ R │ │ R │ │ R │ │ R │ │ R │ │
│ └─┬─┘ └─┬─┘ └─┬─┘ └─┬─┘ └─┬─┘ │
│ │ │ │ │ │ │
│ └──┬──┴──┬──┴──┬──┴──┬──┘ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌─────────────────────────┐ │
│ │ 全部打到 DB │ │
│ │ (DB 崩潰) │ │
│ └─────────────────────────┘ │
└─────────────────────────────────┘方案一:更新鎖
def get_data(key):
data = cache.get(key)
if data is None:
# 嘗試獲取分布式鎖
if lock.acquire(key):
try:
data = db.query(key)
cache.set(key, data)
finally:
lock.release(key)
else:
# 未獲取到鎖,等待重試或返回默認值
time.sleep(0.1)
return get_data(key)
return data方案二:後台更新
- 快取永不過期
- 後台線程定時更新
- 業務線程只讀快取
後台更新的優勢:既適用於單機多線程,也適用於分布式集群,實作比更新鎖簡單。
快取熱點詳解#
某明星發微博,瞬間上千萬用戶湧入查看。即使是 Redis,單 key 的 QPS 也有上限。
解決方案:多副本 + 隨機讀取
原始:key = "weibo_12345"
副本:key = "weibo_12345_1", "weibo_12345_2", ..., "weibo_12345_100"
讀取時隨機選擇一個副本不同副本的過期時間要隨機化,避免同時失效。
負載均衡架構#
當單機效能無法滿足需求時,需要通過集群來擴展效能。
負載均衡分類#
| 類型 | 範圍 | 特點 | 代表產品 |
|---|---|---|---|
| DNS 負載 | 全局 | 地理位置、運營商分流 | DNS 服務商 |
| 硬件負載 | 本地 | 高效能、高可靠、價格昂貴 | F5, A10 |
| 軟件負載 | 本地 | 靈活、成本低 | LVS, Nginx |
負載均衡算法#
| 算法 | 描述 | 適用場景 |
|---|---|---|
| 輪詢 | 依次分配 | 伺服器效能相近 |
| 加權輪詢 | 按權重分配 | 伺服器效能差異明顯 |
| 隨機 | 隨機選擇 | 大量請求時效果接近輪詢 |
| 最少連接 | 選擇連接數最少的 | 長連接場景 |
| 源地址 Hash | 同一來源固定到同一伺服器 | 需要會話保持 |
| 一致性 Hash | 節點變化時影響範圍小 | 分布式快取 |
一致性 Hash 詳解
問題背景:傳統 Hash(如 hash(key) % N)在節點增減時,大部分資料的映射都會改變,導致快取失效。
一致性 Hash 原理:
0/2³²
●
/|\
/ | \
/ | \
/ | \
●────●────●
/ | \
/ | \
● ● ●
Node1 Node2 Node3
資料 key 順時針找到第一個節點虛擬節點:為每個物理節點創建多個虛擬節點,解決資料分布不均的問題。
type ConsistentHash struct {
ring map[uint32]string // hash -> 節點
sortedKeys []uint32 // 排序的 hash 值
virtualReplicas int // 虛擬節點數
}
func (ch *ConsistentHash) Get(key string) string {
hash := ch.hash(key)
idx := ch.search(hash) // 二分查找
return ch.ring[ch.sortedKeys[idx]]
}非同步處理架構#
非同步處理是提升系統吞吐量的重要手段,核心思想是將非關鍵路徑的操作延後處理。
非同步架構模式#
flowchart LR
subgraph 同步處理
A1[用戶] --> B1[業務邏輯]
B1 --> C1[發短信<br/>100ms]
C1 --> D1[記日誌<br/>50ms]
D1 --> E1[返回<br/>總耗時 150ms+]
end
subgraph 非同步處理
A2[用戶] --> B2[業務邏輯]
B2 --> E2[返回<br/>立即]
B2 -.-> Q[消息隊列]
Q -.-> S1[短信服務]
Q -.-> S2[日誌服務]
end消息隊列選型#
| 產品 | 特點 | 適用場景 |
|---|---|---|
| RabbitMQ | 功能完善,AMQP 協議 | 業務消息 |
| Kafka | 高吞吐,持久化 | 日誌收集、流處理 |
| RocketMQ | 事務消息,順序消息 | 交易系統 |
| Redis | 簡單,速度快 | 輕量級任務隊列 |
非同步設計要點#
非同步不是銀彈,引入消息隊列會帶來以下問題:
- 消息丟失:需要持久化和確認機制
- 消息重複:消費端需要冪等處理
- 消息積壓:需要監控和擴容機制
- 順序問題:可能需要特殊處理
效能最佳化方法論#
最佳化層次#
flowchart TB
A["業務最佳化<br/>(效果最大)"] --> B["架構最佳化"]
B --> C["程式碼最佳化"]
C --> D["組態最佳化<br/>(效果最小)"]
style A fill:#c8e6c9
style B fill:#e8f5e9
style C fill:#fff3e0
style D fill:#ffecb3常見最佳化手段#
| 層面 | 最佳化手段 | 效果 |
|---|---|---|
| 業務 | 減少不必要的功能 | 最好的效能最佳化是不做 |
| 架構 | 快取、非同步、拆分 | 數量級提升 |
| 程式碼 | 算法最佳化、減少 I/O | 倍數級提升 |
| 組態 | JVM 參數、連接池 | 百分比提升 |
本章小結#
| 模式 | 核心思想 | 典型應用 |
|---|---|---|
| 單機並發 | 選擇合適的 I/O 和進程模型 | Reactor (Nginx, Redis) |
| 快取 | 空間換時間,減少重複計算 | Redis, Memcached |
| 負載均衡 | 任務分配,水平擴展 | Nginx, LVS |
| 非同步 | 削峰填谷,提升吞吐量 | Kafka, RabbitMQ |
記住這個公式:高效能 = 合適的並發模型 + 快取 + 負載均衡 + 非同步處理