三個值得了解的失效概念
歡迎
分散式系統會以特定模式失效。一旦你學會這些模式,每份事後分析報告都會變成辨識練習,而非謎題。
三個概念涵蓋了生產環境失效分析中最重要的部分:
單點故障(SPOF):其失效會導致更大系統停機組件。常隱蔽:所有人依賴的 DNS 伺服器;所有東西都向其續期的憑證;單一的資料庫主節點。
連鎖失效:一個元件的故障觸發另一個元件的故障,進而觸發更多元件的故障。緩慢的資料庫導致 API 層出現逾時,進而引發重試,重試又對資料庫造成更大負載,導致更多逾時。爆炸範圍不斷擴大。
爆炸半徑:當系統中某一部分失效時,有多少部分會隨之癱瘓。架構決策會限制或解除對爆炸半徑的約束。單點故障(SPOF)具有無限的爆炸半徑。而採用隔離(Bulkhead)設計的服務則具有有限的爆炸半徑。
完成本課程後,您將能夠:
- 透過檢查識別架構中的單點故障(SPOF)
- 識別連鎖失效模式:雷群效應(thundering herd)、重試風暴(retry storm)、死亡佇列(queue of death)
- 閱讀真實的時間軸,並區分觸發事件與該觸發事件所暴露的潛在缺陷
- 撰寫無責備(blameless)的行動項目,針對系統而非個人,涵蓋預防/偵測/恢復
- 將隔離(bulkheads)與斷路器(circuit breakers)視為限制爆炸半徑的工具進行推理
找出單點故障
分層架構檢查
考慮一個小型的 Web 架構:
- DNS:api.example.com -> 單一網域伺服器 IP 203.0.113.10,由一家 DNS 供應商託管
- CDN:在 api.example.com 前方使用單一 CDN 供應商
- Ingress:負載平衡器後方有兩台反向代理機器
- 後端:在兩個可用區(每個可用區三個)中部署六個 API 副本
- 資料庫:一個主資料庫 + 一個唯讀副本,位於同一個可用區
- 快取:Redis 叢集,三個節點分散在相同的兩個可用區(Availability Zones)內
問題:哪些元件是單點故障(SPOF)?提示:SPOF 不一定是顯而易見的「單台機器」類型。一個由三台機器組成、但全部位於同一個可用區的叢集,對於該可用區的故障而言,就是一個 SPOF。
三種經典級聯模式
失效透過相依性傳播
模式 1:群集效應(Thundering herd)。 共享資源(快取、鎖、資料庫)失效或重新啟動。所有依賴該資源的客戶端同時重試。湧入的請求淹沒了剛恢復的系統;重試累積的速度快於恢復所能吸收的速度;恢復永遠無法完成。
模式 2:重試風暴(Retry storm)。 下游服務變慢。上游呼叫者沒有失敗,而是進行重試。重試使原始負載倍增。緩慢的服務變得更加緩慢,觸發更多重試。最終,負載甚至超過了該服務健康狀態下的處理能力。
模式 3:死亡佇列(Queue of death)。 一個沒有背壓(backpressure)的處理佇列,接收速度大於處理速度。佇列無限增長。記憶體耗盡;消費者崩潰;重新啟動;發現佇列變得更大;再次崩潰。
共同點:微小的初始擾動觸發了正回饋迴路。系統自身的回應放大了失效,而非將其衰減。
衰減機制
帶有抖動(jitter)的指數退避(Exponential backoff)。 重試的客戶端每次等待時間會更長,並加入隨機偏移量。這可防止同步的重試浪潮。
熔斷器(Circuit breaker)。 呼叫端追蹤下游的失敗率。當超過閾值時,呼叫端會停止呼叫一段冷卻時間,並立即讓自己的請求失敗。這可避免浪費資源,並讓下游系統得以恢復。
隔離(Bulkhead)。 依依賴項隔離資源。資料庫使用連線池 A,快取使用獨立的連線池 B。慢速的資料庫無法耗盡所有連線;快取的呼叫仍可繼續。
負載削減(Load shedding)。 當系統過載時,在邊緣直接丟棄請求,而不是接受它們並緩慢失敗。在 1 毫秒內回傳 429 錯誤,優於在 30 秒後回傳 500 錯誤。
背壓(Backpressure)。 當消費者無法跟上時,減慢生產者的速度。佇列變得有界;發送端被阻塞;工作的原始來源會感受到這種阻力。
診斷級聯故障
某團隊的 API 層在常規資料庫故障轉移期間崩潰。時間軸如下:
- 14:00:00 — 運維人員將備用資料庫提升為主要資料庫。預期不可用時間:約 10 秒。
- 14:00:08 — 主要資料庫不可用。API 層請求開始因資料庫連線錯誤而失敗。
- 14:00:08 — API 層進行重試(預設設定:重試 5 次,無退避策略,間隔 100 毫秒)。
- 14:00:11 — 備用資料庫已提升,開始接受新連線。
- 14:00:11 — API 層同時開啟數千個新的資料庫連線(每個副本 × 每個並發請求 × 每次重試)。
- 14:00:13 — 新主要資料庫的連線池耗盡;新連線被拒絕。
- 14:00:13-14:05:00 — API 層副本耗盡連線池,拋出例外,崩潰,重啟,並重複此過程。
- 14:05:00 — 運維人員手動停止 API 層流量;資料庫恢復穩定。
- 14:10:00 — 逐步恢復流量完成。總停機時間:約 10 分鐘(預期約 10 秒)。
DNS SERVFAIL:兩個相互加劇的缺陷
一個真實形態的事後分析
以下是經過脫敏處理的真實事件版本。供應商名稱已更改,IP 位址已匿名化;但事件的形態、時間軸及教訓都是真實的。
摘要
網站 example.com 從所有公開 DNS 解析器返回 SERVFAIL,持續約 3-4 小時。同一 DNS 主伺服器上的其他 46 個區域未受影響。根本原因:兩個相互加劇的缺陷。
1. 供應商 A(一家次要 DNS 供應商)新增了一個內部同步 IP,該 IP 未包含在主伺服器的 allow-axfr-ips 允許清單中。
2. example.com 區域存在一個多年未處理的、違反 RFC 規範的 CNAME 衝突(demo.example.com 在同一標籤下同時存在 CNAME 及 MX/TXT 記錄),導致供應商 A 在進行新的 AXFR 時拒絕該區域。
時間軸(UTC)
- 約 15:00 — 供應商 A 將其基礎設施中的新同步 IP 198.51.100.42 加入
- 15:02 — 主要 DNS 日誌中首次出現針對 198.51.100.42 的 AXFR-out denied(此訊號未設定告警)
- 約 18:00 — 達到 SOA 過期時間窗口;供應商 A 從快取中移除 example.com 區域
- 約 18:30 — 外部偵測到 SERVFAIL
- 約 19:45 — 確認根本原因
- 20:00 — 將 198.51.100.42 加入 allow-axfr-ips;重新啟動主要伺服器
- 20:05 — 發送 NOTIFY;啟動 AXFR;區域仍為 SERVFAIL(CNAME 衝突)
- 20:07 — check-zone 顯示 1 個錯誤:demo.example.com 上的 CNAME 衝突
- 20:09 — CNAME 已替換為 A 記錄;區域檢查結果乾淨(0 個錯誤)
- 20:10 — 已發送 NOTIFY;AXFR 完成;供應商 A 開始提供該區域服務
- 20:11 — dig @8.8.8.8 example.com A 返回正確的 IP — 已解決
為什麼只有 example.com?
所有 47 個區域共用同一個 DNS 主要伺服器。AXFR IP 封鎖影響了所有區域。但只有 example.com 存在 CNAME 衝突,且只有在拒絕執行時,只有 example.com 需要新的 AXFR。其他區域在拒絕執行前已經刷新,或者當時還不需要刷新。
潛在缺陷
demo.example.com 上的 CNAME 衝突已存在多年。它能運作是因為主要伺服器從其資料庫提供區域服務(對 RFC 違規採取寬容態度),而供應商 A 使用的是在違規引入之前過期的快取資料。當供應商 A 清除其快取並需要新資料時,該違規才浮現出來。
觸發
供應商 A 悄悄新增了一個同步 IP。主節點的允許清單中未包含該 IP,導致 AXFR 請求被拒絕。三小時後(SOA 過期時間),供應商 A 移除了該區域。當系統嘗試恢復時,這個潛在缺陷才浮現出來。
撰寫無責備行動項目
無責備:針對系統,而非個人
無責備的行動項目應指明系統應如何改變行為,而非個人應如何改變行為。「訓練操作員」屬於有責備的表述;「新增一個自動化檢查,在部署前捕捉此問題」則屬於無責備的表述。
良好的無責備行動項目通常歸納為以下三個維度:
- 預防:使不良情況更難發生或完全無法發生
- 偵測:若問題發生,能更早察覺
- 恢復:當問題發生時,將損害降至最低
每個項目應明確指出 (1) 具體的系統變更、(2) 負責團隊,以及 (3) 其所服務維度。
不會隨船沉沒的艙室
借鑒自船舶工程
船舶配備有密閉艙壁:將船體劃分為多個艙室的垂直牆壁。一個艙室進水不會導致整艘船沉沒;另一個艙室失效也不會影響其餘部分。
分散式系統借用了相同的術語與相同的概念。
艙壁模式:依依賴項隔離資源。一個呼叫三個下游 API 的服務,會使用三個獨立的連線池、三個獨立的執行緒池、三個獨立的重試預算。某個緩慢或失效的下游依賴無法耗盡分配給另外兩個依賴的資源。
沒有艙壁:一個緩慢的依賴耗盡共享的執行緒池;對其他依賴的呼叫會阻塞並等待執行緒;整個服務變得無響應。
有艙壁:一個緩慢的依賴耗盡其自身的池;對它的呼叫快速失敗;對其他依賴的呼叫正常繼續;爆炸半徑被限制在失效的依賴範圍內。
斷路器
熔斷器模式(Circuit breaker pattern):一個有狀態的包裝器,用於封裝下游依賴並追蹤失敗率。它有三種狀態:
- 關閉(正常):請求正常通過。失敗會被計數。
- 開啟(觸發):當失敗率超過閾值(例如,過去 30 秒內失敗率達到 50%)時,熔斷器開啟。請求會立即失敗,而不會嘗試呼叫依賴服務。這避免了呼叫方浪費資源,也防止了不健康的依賴服務繼續承受負載。
- 半開啟(測試):經過一段冷卻期後,熔斷器允許一小部分請求通過。如果這些請求成功,熔斷器會恢復為關閉(正常)狀態。如果失敗,則重新開啟並進入下一次冷卻期。
關鍵洞察:熔斷器在已知不健康的期間防止了無效的嘗試,並讓下游服務有機會在沒有持續負載的情況下恢復。
隔離舱(Bulkheads)限制了爆炸半徑。熔斷器(Circuit breakers)防止爆炸持續自我維持。
限制爆炸半徑
您的 API 服務呼叫了四個下游服務:使用者服務(User Service)、推薦服務(Recommendation Service)、通知服務(Notification Service),以及一個第三方的支付 API。團隊聽說「推薦服務最近有點不穩定」,希望確保當它失敗時,系統的其餘部分仍能保持健康。
今天,該服務使用一個共用的 200 執行緒池以及一個共用的 HTTP 連線池。這四個下游服務都在爭奪這些資源。目前沒有設定任何斷路器(circuit breakers)。
設計失敗模式審查
綜合
你已學會透過檢查識別單點故障(SPOF)、辨識連鎖失敗模式、在閱讀事後分析報告時區分觸發因素與潛在缺陷、撰寫涵蓋預防/偵測/恢復的無責備行動項目,以及透過隔離區(bulkheads)+電路斷路器(circuit breakers)+優雅降級(graceful degradation)來限制爆炸半徑。
應用全部五項技能。
你的團隊即將推出新服務 search.example.com,該服務依賴三個下游服務:主要搜尋索引(index.example.com)、分析服務(analytics.example.com)以及推薦服務(recs.example.com)。團隊希望由你主導上線前的「失敗模式審查」。
本課程接下來將走向何方
本課程接下來將走向何方
現在,您可以識別 SPOF、辨識級聯故障、有效地閱讀事後分析報告、撰寫無責任歸咎的行動項目,並透過設計來限制爆炸半徑。
本課程的最後一課(cs_distsys_observability_and_capacity)將教導您測量哪些指標,以便在用戶發現問題之前,您就能察覺問題正在發生。內容包括健康檢查、版本端點、代理層的四個黃金信號,以及突發容量決策如何與觀測數據相連。
配套課程:geometry_of_failure_modes_and_blast_radius 推導介數中心性(哪個圖節點是瓶頸)與最小割(爆炸半徑的上限)。
做得很好。繼續前進。