過載保護 (Overload Protection)
過載保護的核心思想是:讓系統在壓力下優雅降級,而不是全面崩潰。
設計良好的系統,在 10 倍正常流量下應該還能服務其中 30%,而不是讓 100% 的請求一起失敗。
它不是單一工具,而是多層次防線的組合:每一層都有它能擋住的、也有它擋不住的東西。面試時展示「為什麼要多層」比背名詞更有說服力。
為什麼過載是核心問題:雪崩
流量不是平滑的曲線,它有尖峰、突發、惡意攻擊、節慶促銷、KOL 突然分享你的連結。當流量超過系統能承受的上限,若無任何保護機制,結果通常是雪崩:
請求堆積在佇列 → 延遲上升 → 執行緒池耗盡 → 記憶體膨脹 → GC 瘋狂
→ DB 連線池塞滿 → 下游服務被拖慢 → 整個系統停止回應
關鍵災難點:不只是超出容量的那部分請求失敗,而是「所有」請求都失敗了。10% 的超量流量可以打掛 100% 的服務。
六層防線的心智模型
過載保護工具可拆成六個層次,每層解決的問題不同:
| 層次 | 手段 | 思路 | 解決什麼 |
|---|---|---|---|
| 預防 | Rate Limiting、Concurrency Limiting | 在入口把過多流量擋掉 | 防濫用、保護內部資源 |
| 吸收 | Queue-based Load Leveling | 用佇列平滑瞬間峰值 | 不拒絕,只是延後處理 |
| 擴容 | Auto-scaling | 從根本增加容量 | 跟上需求 |
| 自保 | Load Shedding、Request Prioritization | 主動丟棄部分請求 | 上面手段都跟不上時的最後防線 |
| 隔離 | Bulkhead | 把資源分艙 | 局部過載不蔓延成全面崩潰 |
| 協同 | Backpressure | 壓力訊號沿鏈路向上傳播 | 讓整條鏈路一起減速 |
外部流量
↓
[預防] Rate Limiting ── 擋惡意 / 超量的單一客戶端
↓
[預防] Concurrency Limiting ── 保護後端資源不被耗盡
↓
[吸收] Queue-based Load Leveling ── 吸收突發、平滑流量
↓ ← [擴容] Auto-scaling 持續在這層加機器
後端服務處理
↓
[隔離] Bulkhead ── 隔離不同依賴,防止局部過載蔓延
↓
[自保] Load Shedding + Request Prioritization ── 最後防線:選擇性丟棄
↑
[協同] Backpressure ── 整條鏈路協同減速訊號
重點是理解為什麼需要多層:每一層能擋的東西不同。Rate limiting 擋不住「總流量合法但超容量」,那要靠 load shedding;load shedding 太粗暴,所以加 request prioritization 決定丟誰。
Rate Limiting(速率限制)
最基本的入口防線:限制每個客戶端在單位時間內能發出的請求數。解決兩個問題:① 防止單一客戶端(或攻擊者)耗盡整個系統資源;② 確保不同客戶端之間的公平使用。
四種演算法對照
| 演算法 | 機制 | 允許突發 | 邊界突發問題 | 記憶體 | 適用 |
|---|---|---|---|---|---|
| Fixed Window 固定視窗 | 每分鐘重置計數器,超過上限就拒絕 | — | 有(邊界 2 秒可放行 2 倍量) | 最低 | 最簡單、要求不嚴格 |
| Sliding Window 滑動視窗 | 滾動時間窗計算請求數,無固定重置點 | — | 消失 | 較高(存每筆時間戳) | 通用、邊界精確 |
| Token Bucket 令牌桶 | 固定速率放令牌、桶有上限、每請求耗一令牌 | 是 | 無 | 低 | 最符合真實場景 |
| Leaky Bucket 漏桶 | 任意速率進、固定速率流出處理 | 否 | 無 | 低 | 嚴格控制輸出速率(呼叫外部 API) |
Fixed Window 的邊界突發:客戶端可在第 59 秒發 100 個、第 61 秒再發 100 個 → 跨視窗的 2 秒內實際放行 200 個。
Token Bucket(最常用) Leaky Bucket(強制平滑)
令牌以固定速率滴入 請求任意速率進
↓ ↓
┌─────────┐ 上限 100 ┌─────────┐
│ ●●●●●●● │ ← 累積可應對突發 │ ▓▓▓▓▓▓▓ │
└─────────┘ └────┬────┘
每請求耗 1 令牌,空了就拒 ↓ 固定速率流出(不允許突發)
它允許「閒置時累積令牌、突發時一次放行」,最貼近真實流量。Leaky Bucket 反而強制平滑、不允許突發,只在你需要嚴格保護下游(如限制對第三方 API 的呼叫速率)時才選。
分散式 Rate Limiting
單機計數很簡單,但多台伺服器各自計數就失效——客戶端把請求分散打到不同機器就能繞過限制。
解法:集中式計數器(通常用 Redis)
Server 1 ┐
Server 2 ┼──→ Redis(INCR + EXPIRE / Lua script 原子操作)
Server 3 ┘ ↑ 保證計數無 race condition
- 延遲代價:每個請求多一次 Redis 查詢,通常 < 1ms,可接受。
- 極低延遲需求:可用「本地計數器 + 定期同步」,犧牲一點精確性換速度。
- 滑動視窗常用 Redis Sorted Set:
ZREMRANGEBYSCORE移除窗外舊記錄 →ZCARD數窗內請求 →ZADD加入這次時間戳 →EXPIRE,全部包進 pipeline。
被限流時的正確回應
回傳 429 Too Many Requests,並用 header 告知客戶端何時可重試:
HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1700000060
這讓客戶端能配合退避(backoff),而不是盲目重試加重伺服器壓力。詳見 01-Networking/07-Fault-Handling。
Concurrency Limiting(並發限制)
- Rate limiting 管的是速率(每秒幾個請求)。
- Concurrency limiting 管的是同時在處理中的請求數量。
假設每個請求平均 500ms。系統有 100 條執行緒 → 安全並發上限就是 100。Rate limiting 說「每秒最多 200 個」,但若每個請求要花 1 秒,同時就有 200 個在跑,遠超執行緒池容量 → 雪崩。Concurrency limiting 更直接對應系統資源。
實作上常用 Semaphore:取得失敗(已達上限)立刻拋 OverloadError,而不是排隊等待。兩者通常一起用:rate limiting 擋突發流量,concurrency limiting 保護內部資源不被耗盡。
根據延遲動態調整並發上限:延遲升高(系統開始過載)→ 自動降低並發數;延遲下降 → 逐步放寬。比靜態寫死的值更能適應真實狀況。本質與 Adaptive Auto-scaling 的回授思路一致。
Queue-based Load Leveling(佇列削峰)
Rate / Concurrency limiting 都是拒絕多餘請求。但有時你不想拒絕,只是需要時間處理。
用戶請求 → API → 訊息佇列(Kafka / SQS)→ 工作服務(以自己能承受的速率慢慢處理)
↑ 吸收瞬間尖峰 ↑ 後端看到的是平滑流量
對非同步任務特別有效:影片轉碼、Email 發送、報表生成、訂單確認——這些不需要在用戶等待的幾秒內完成。
| 維度 | 說明 |
|---|---|
| 效果 | 佇列把尖峰吸收進去,後端只看到自己能承受的平滑流量 |
| 代價 | 處理從同步變非同步,用戶拿到「已收到,處理中」而非即時結果 |
| 適用界線 | 影片上傳等幾分鐘轉碼 OK;用戶付款後等 30 秒才收確認不合理 |
| 監控訊號 | 佇列深度持續增長 = 消費跟不上生產 → auto-scaling 的觸發訊號 |
詳見 09-Messaging-Coord/02-Queue、10-Design-Patterns/03-Long-Running-Tasks。
Auto-scaling(自動擴縮容)
前面手段都是在現有容量內管理流量;Auto-scaling 從根本上增加容量,需求增加時橫向擴展、減少時縮回省成本。
觸發指標對照
| 指標 | 準確度 | 說明 |
|---|---|---|
| CPU 使用率 | 中 | 最常見(>70% 擴容),但 I/O 密集服務 CPU 低卻可能已過載 |
| RPS | 高 | 直接反映流量 |
| 佇列深度 | 高 | 消費者服務根據積壓量決定要不要加機器 |
| P99 延遲 | 最高 | 最能反映用戶體驗——P99 超閾值代表系統開始承壓 |
Reactive vs Predictive
Reactive(反應式):流量來了 → CPU 升高 → 才觸發擴容
⚠ 啟動新實例 + 健康檢查 + 加入 LB 池 需要 1~3 分鐘
這段空窗期過載仍在 → 需要 load shedding 撐住
Predictive(預測式):根據歷史模式提前擴
電商知道每天 9 點流量上來 → 8:50 就開始擴(AWS/GCP 提供 ML 預測)
最佳實務:Predictive 處理已知模式 + Reactive 處理預期外突發
縮容太激進,一個小流量回升又觸發擴容 → 振盪(oscillation)。典型設定:擴容條件觸發 1 分鐘後執行,縮容需持續 10 分鐘才執行。
延伸:02-Distributed-Systems/03-Scalability、11-Infrastructure/03-Container(K8s HPA)。
Load Shedding(負載卸除)
當 auto-scaling 來不及、佇列也有深度上限、rate limiting 只擋外部攻擊——這些都跟不上時,最後防線是主動丟棄部分請求,保護核心功能不崩潰。
選項 A:處理 30% 的請求,其他 70% 快速失敗(明確錯誤) ← Load Shedding 選這個
選項 B:試圖處理 100%,結果系統崩潰,100% 全失敗
讓部分請求快速失敗,遠比讓所有請求一起慢死要好。
在哪裡做 Load Shedding
| 位置 | 成本 | 決策資訊 | 取捨 |
|---|---|---|---|
| 入口(API Gateway / LB) | 最低(不碰後端) | 少(只有請求速率、連線數) | 便宜但不精確 |
| 服務內部 | 較高(請求已進來) | 多(CPU、記憶體、延遲) | 處理前先檢查健康,超閾值立刻回 503、不跑業務邏輯 |
服務內部典型門檻:CPU > 85% 或 Memory > 90% → 回傳 503 Service Unavailable。
該丟棄什麼請求(不是隨機丟)
| 策略 | 理由 |
|---|---|
| 重試請求優先丟 | 帶 X-Retry-Count 代表已讓系統處理過,這次過載可能就是它造成的;保護第一次嘗試的新請求 |
| 低優先級請求先丟 | 分析請求、監控上報、非關鍵推薦,比用戶核心操作可更快丟 |
| 讀取可比寫入更早丟(視場景) | 讀取有快取保底;寫入一旦失敗可能有資料遺失風險 |
Request Prioritization(請求優先級)
Load shedding 決定「要不要丟」,request prioritization 決定「丟誰」。
過載時不是所有請求平等:
| 優先級 | 請求類型 | 理由 |
|---|---|---|
| 最高 | 付費用戶的核心操作 | 直接影響收入與 SLA |
| 高 | 一般用戶的核心操作 | 業務關鍵功能 |
| 中 | 非核心功能(推薦、搜尋) | 可降級或跳過 |
| 低 | 後台任務、分析請求 | 可延遲處理 |
| 最低 | 監控、日誌上報 | 不影響用戶 |
實作:請求進入服務時,依用戶身份 / 請求類型打上優先級標記,過載時低優先級先被丟(如用 priority queue,超容量時丟棄堆中優先級最低者)。
Bulkhead(艙壁隔離)
名字來自輪船設計:把船艙分成多個隔艙,一個艙進水不會讓整艘船沉沒。在系統設計裡,把不同工作負載 / 租戶 / 服務依賴隔離到獨立資源池,確保一部分過載不會耗盡整個系統。
三種常見實現
① 執行緒池隔離
┌── Thread Pool A(DB 操作,40 條)──┐
┌── Thread Pool B(支付 API,10 條)─┐
支付 API 變慢 → 只耗盡 Pool B 的 10 條 → DB 的 Pool A 完全不受影響
(沒有 bulkhead:支付的慢會吃光「共用」執行緒池,讓 DB 操作也跟著超時)
→ Netflix Hystrix 即以執行緒池隔離 + 熔斷器為核心
| 實現 | 邊界 | 例子 |
|---|---|---|
| 執行緒池 | 每個下游依賴一個池 | 支付池掛掉不影響 DB 池 |
| 租戶隔離 | 大客戶獨立、小客戶共用 + concurrency limit | 多租戶 SaaS:大企業獨立實例 / 分片,小客戶共用但限並發 |
| 連線池 | 讀寫分離不同池 | read_pool=80(replica)/ write_pool=20(primary),大量讀不耗盡寫連線 |
太細 → 管理成本高、資源浪費(一池閒著另一池排隊);太粗 → 隔離沒意義。依「故障影響的相關性」劃分:把「如果它掛掉,你希望不影響哪些功能」的依賴隔離成獨立 bulkhead(支付掛掉不該影響商品瀏覽)。從 3~5 個 bulkhead 起步,再依實際故障模式調整。
連線池隔離與讀寫分離詳見 05-Database-Advanced/03-Replication、10-Design-Patterns/01-Scaling-Reads。
Backpressure(背壓)
前面所有機制都是承壓的那一側自己做決定。Backpressure 不同:它讓下游主動告知上游「我撐不住了,慢一點」,讓壓力訊號沿鏈路向上傳播。
無 backpressure:上游不斷打向下游 → 佇列無限增長 → 崩潰
有 backpressure:下游發訊號 → 上游主動放慢 → 整條鏈路協同
| 層級 | 機制 |
|---|---|
| TCP | 接收緩衝區(receive window)快滿 → 通知發送方縮小傳送視窗 → 自動放慢(網路層背壓) |
| gRPC / HTTP-2 | flow control:每個 stream 與 connection 都有流量控制視窗;接收方可暫停接收,客戶端自然放慢;streaming 中服務端用處理速度自然施加背壓 |
| 應用層 | 服務端回 429 + Retry-After,或依佇列深度超閾值主動回 busy,讓客戶端退避 |
- Load shedding:直接丟棄請求。
- Backpressure:不丟棄任何東西,只是讓上游放慢、整條管線流速降下來。
Backpressure 更溫和,但需要上游配合(支援背壓訊號),不是所有情況都能用。TCP/UDP 對比見 01-Networking/02-TCP-vs-UDP。
面試話術
設計任何大流量系統時,主動講多層保護:「我們在 API Gateway 做 rate limiting(每用戶每分鐘 1000 個,Redis + 滑動視窗)。核心訂單服務設 concurrency limiting(最多 50 並發,超過立刻回 503 讓客戶端退避)。不需即時的任務(通知、報表)放進 SQS 讓後端慢慢消費,即使尖峰後端也不崩。」
常見情境的標準答法:
- 電商秒殺(流量 100 倍):商品詳情頁用 CDN + 靜態快取 讓 99% 讀取不打後端;下單進 Redis 隊列 + Lua 原子扣庫存,超賣直接回失敗;後端固定速率取單完全不受前端流量影響;配 rate limiting 防同一用戶重複提交。
- 多租戶 SaaS:每租戶獨立 rate limit 配額;大企業客戶獨立實例(bulkhead),中小客戶共用但各有 concurrency limit。
- 視訊轉碼:上傳後任務進 SQS,依佇列深度觸發 auto-scaling(>100 加一台、<10 持續 10 分鐘縮一台),高峰自動擴、閒置縮回省成本。
常見 Deep Dive
Rate limiting 針對個別客戶端:限制某特定用戶 / IP 能發多少請求,解決公平性與防濫用。即使沒有任何單一客戶端違規,總流量仍可能超過系統容量。
Load shedding 針對整個系統狀態:不管流量從哪來,當系統整體過載時主動丟棄部分請求。
兩者互補:rate limiting 是入口的公平性控制,load shedding 是系統的最後防線。
Reactive 擴容通常 1~3 分鐘,靠三招撐住:① 預測性擴容(依歷史提前擴好);② load shedding 兜底(擴容跟上前以較低容量服務,優先高優先級、丟低優先級);③ 佇列緩衝(非同步任務進佇列等待,擴容跟上後再消化積壓)。
根據兩個數字算:後端實際容量 + 正常用戶合理使用量。例:後端每秒 10,000 req、1,000 活躍用戶 → 每用戶合理約 10 req/s,留 buffer 設 20 req/s 應對偶發突發。太低傷正常用戶體驗,太高失去保護意義。不同端點不同 limit:搜尋寬鬆、登入嚴格(防暴力破解)、管理員 API 需更強驗證而不只是限速。
太細→管理成本高、資源浪費;太粗→隔離沒意義。依故障影響相關性劃分:把「掛掉時你希望不影響哪些功能」的依賴隔離成獨立 bulkhead(支付掛掉不該影響商品瀏覽)。同一 bulkhead 內的依賴可一起降級。從 3~5 個起步再調整。
不是選一個,而是層層疊加:入口 Rate Limiting(擋惡意 / 超量客戶端)→ Concurrency Limiting(保護後端資源)→ Queue Load Leveling(吸收突發)+ Auto-scaling(持續擴容)→ Bulkhead(隔離防蔓延)→ Load Shedding + Prioritization(最後防線)+ Backpressure(鏈路協同減速)。面試重點是說清每層擋什麼、擋不住什麼。
Rate limiting 管「每秒幾個」,但若每個請求變慢(下游拖慢),同時在跑的數量會暴增,遠超執行緒池容量。Concurrency limiting 直接對應「同時佔用的資源」,是更貼近真實過載的維度。Netflix 的 Adaptive Concurrency Limiter 還能依延遲動態調整上限。
自我測驗重點
| 問題 | 重點 |
|---|---|
| 過載不保護會怎樣 | 雪崩——不只超量部分失敗,所有請求都失敗 |
| 四種限流演算法 | Fixed/Sliding/Token Bucket/Leaky;面試預設 Token Bucket |
| Fixed Window 缺點 | 邊界突發(跨視窗 2 秒可放行 2 倍) |
| Rate vs Concurrency limiting | 速率(每秒幾個) vs 同時處理中的數量 |
| 分散式 rate limiting | 集中式 Redis 計數器(INCR+EXPIRE / Lua 原子操作) |
| 被限流回什麼 | 429 + Retry-After,讓客戶端退避 |
| Queue load leveling 代價 | 同步變非同步;付款等 30 秒不合理、轉碼等幾分鐘 OK |
| Auto-scaling 指標 / 節奏 | P99 最準;擴容快、縮容慢防振盪 |
| Reactive 空窗怎麼撐 | 預測性擴容 + load shedding + 佇列緩衝 |
| Load shedding 丟誰 | 重試請求 / 低優先級 / 讀取(視場景)先丟 |
| Shedding vs Prioritization | 要不要丟 vs 丟誰 |
| Bulkhead | 分艙隔離;執行緒池 / 租戶 / 連線池;依故障相關性定粒度 |
| Backpressure vs Load shedding | 放慢不丟 vs 直接丟;背壓需上游配合(TCP / HTTP-2 flow control) |
Related Notes
- 01-Networking/07-Fault-Handling — 429/退避、超時、熔斷與過載保護同屬韌性工具箱;idempotency 配合重試
- 01-Networking/06-Load-Balancing — 入口 load shedding 與健康檢查通常在 LB / API Gateway 這層做
- 11-Infrastructure/01-API-Gateway — Rate limiting、入口 load shedding 的標準落點
- 09-Messaging-Coord/02-Queue — Queue-based load leveling 削峰、佇列深度是 auto-scaling 訊號
- 09-Messaging-Coord/03-Kafka — 大規模削峰與背壓常用的訊息骨幹
- 10-Design-Patterns/02-Scaling-Writes — 寫入削峰、load shedding 對寫入路徑的取捨
- 10-Design-Patterns/03-Long-Running-Tasks — 非同步任務進佇列、用 worker 慢慢消費的範式
- 02-Distributed-Systems/03-Scalability — Auto-scaling 是橫向擴展的具體手段
- 01-Networking/02-TCP-vs-UDP — TCP receive window 是最底層的內建 backpressure
- 05-Database-Advanced/03-Replication — 讀寫分離連線池 = 資料庫層的 bulkhead
- 12-Ops-and-Reliability/01-Dealing-With-Contention — 過載常源於資源競爭;限流 / 隔離降低爭用
- 12-Ops-and-Reliability/04-Observability — P99 延遲 / 佇列深度監控是觸發 auto-scaling 與 shedding 的依據