iT邦幫忙

2026 iThome 鐵人賽

DAY 3
0
AI Engineering

Learning SRE for the AI Era:從 SRE Lab 到 Production AI Reliability系列 第 14 篇

Day 11(下)|Production Failure Modes:先替系統想好難看的死法

  • 分享至 

  • xImage
  •  

GitHub:darkstar1227/learning-sre-for-ai-era

結論先說:configuration failure 最快發生也最快被誤診,而六種死法最終都要收斂成同一條可執行的調查順序——先看使用者 failure,再查變更、retry、飽和與 deadline,最後才動手修 fault。

承接上文

上篇談了 failure mode 卡片的五段結構、critical path 的失敗傳播、六種常見死法(crash、timeout、network、resource、configuration、dependency),以及兩個情境的深入拆解:慢 dependency 的 deadline 設計、retry amplification 的 owner 與 budget,還有 degradation 該怎麼設計才不是偷工減料。下篇接著談 configuration failure 本身,再把前面的觀念收斂成一條事故調查流程,最後是今天的 DIY。

⑦ Configuration failure:最快發生,也最快被誤診

不是所有 production failure 都從流量開始。

一次壞的 endpoint、錯的 API key scope、格式不相容的 feature flag,可能在 deploy 後幾秒就讓 dependency error 暴增。

new service version / feature flag
  ↓
provider endpoint 指到錯誤網域
  ↓
connection error
  ↓
retry traffic 增加
  ↓
/ask failure 或 degraded ratio 上升

這時最有價值的關聯欄位通常不是 CPU:

service_version
deployment_id
feature_flag_version
provider_route
config_revision

它們可以放在 trace resource attributes 或 structured log。要避免把 secret、完整 endpoint query string、prompt 或使用者資料直接記進去。

旗標衛生:kill switch 要能立即生效,而且要跟死碼分手

Knight Capital 案例裡最刺眼的一個細節,是新舊功能共用了同一個旗標名稱——這在事後看很荒謬,但在實務上,「重用一個現成的旗標名稱」往往是團隊在時間壓力下最省事的選擇,因為新增旗標涉及新的設定管理、新的部署步驟,看起來比「反正名字差不多,就沿用舊的」麻煩。

一個基本的 feature flag 衛生習慣,是讓「新增」與「移除」跟寫程式一樣被視為正常流程的一部分,而不是可有可無的技術債:

from enum import Enum


class FeatureFlag(str, Enum):
    # 每個旗標只對應一段邏輯,不重複使用
    RETAIL_LIQUIDITY_ROUTING_V2 = "retail_liquidity_routing_v2"


def route_order(order: dict, flags: dict[str, bool]) -> str:
    if flags.get(FeatureFlag.RETAIL_LIQUIDITY_ROUTING_V2, False):
        return route_v2(order)

    # 舊路徑:一旦 v2 全面上線且穩定運作一段時間,
    # 這個分支與底下呼叫的舊邏輯應該被物理移除,
    # 而不是留著「以防萬一」——
    # Power Peg 正是一段「留著以防萬一」最終復活的死碼。
    return route_legacy(order)

這段程式碼的重點不在 route_order() 本身,而在註解點出的那句話:一個旗標的生命週期,應該包含「新增」「全面生效」「移除舊分支與死碼」三個階段,而「移除」不是可選項。很多團隊做到第二階段就停了——新功能上線、確認穩定,旗標就被遺忘在設定裡,舊分支跟它呼叫的舊邏輯也一起被遺忘在程式碼裡。Knight Capital 的 Power Peg 邏輯從 2005 年就該停用,卻留在程式碼裡整整七年,直到一次旗標重用意外把它喚醒。

kill switch 是這個機制更緊急的版本:當異常已經開始發生,需要有辦法在秒級時間內把某段邏輯整個關閉,而不必等待一次完整的部署流程。Knight Capital 的異常持續了將近 45 分鐘,才由人工介入手動關閉系統。如果當時有一個獨立於「重新部署」流程之外的 kill switch,讓交易員能立刻切斷新路由邏輯的執行,而不需要等待另一次可能同樣不可靠的部署,損失很可能被壓縮在遠小於 45 分鐘的範圍內。這也是為什麼 kill switch 通常設計成讀取一個外部設定來源(例如集中式的 feature flag 服務),而不是寫死在需要重新部署才能生效的程式碼裡;它存在的意義,就是要比正常的部署流程更快。

反例:先擴容再看 deploy

如果 error spike 與 deployment_id 完全同時出現,先 rollback 或關閉 flag,往往比先加 worker 更符合因果。

擴容可能暫時降低 queue depth,卻讓更多 request 打到錯的 endpoint。那不是修復,只是把故障面積放大。

Cloudflare 的 2025-11-18 postmortem 是一個值得閱讀的 configuration / dependency 類事故案例:它提醒我們,control-plane 或資料產生流程的變更也可能擴大到實際服務路徑。事故的觸發點是一次資料庫權限設定變更,讓原本被過濾掉的重複欄位意外出現在查詢結果裡,使得 Bot Management 用的 feature 設定檔案大小加倍,超過系統原本假設的欄位數上限,直接讓 proxy 軟體 panic。從 2025-11-18 11:20 UTC 開始明顯可見影響,到 17:06 UTC 完全恢復,將近 6 小時。Cloudflare postmortem

這個案例特別值得放進「最快發生」這句話的討論,因為它示範了 configuration failure 不一定來自應用程式碼的部署:一次看似跟服務邏輯無關的資料庫權限調整,就足以在沒有任何 code review 覆蓋到的路徑上,觸發一個「正常維運操作下意外撞見沒被涵蓋的邊界條件」的災難。系統原本假設 feature 檔案不會超過某個大小,這個假設從來沒有被寫進測試裡,因為在正常情況下它永遠成立,直到一次跟它完全無關的變更,意外讓這個假設破功。

為什麼 progressive rollout 是對抗 configuration failure 的結構性解法

Knight Capital 與前面 Cloudflare 的案例,都有一個共同的缺口:新配置或新程式碼是一次性推到所有伺服器或所有流量上的。Knight Capital 是八台伺服器同時部署(結果七台成功、一台失敗,且沒有機制擋下這個不一致狀態繼續服務);Cloudflare 的權限變更則是資料庫層面的全域切換,沒有分階段生效的概念。

Progressive rollout(漸進式發佈,常見形式是 canary release:先讓一小部分流量或機器跑新版本,觀察一段時間指標正常,才逐步擴大比例)想解決的正是這類問題。它的核心邏輯是把「配置變更後可能出錯」這件事,從「賭注是全部流量」降低成「賭注是 1% 流量」:

沒有 progressive rollout
新配置
  ↓ 一次性套用到 100% 流量 / 全部伺服器
壞的配置 → 100% 的使用者立刻受影響

有 progressive rollout
新配置
  ↓ 先套用到 canary(例如 5% 流量或 1 台伺服器)
  ↓ 觀察 error rate / latency / 業務指標是否異常
  ├── 異常 → 立刻回滾,影響範圍只有 canary 那一小部分
  └── 正常 → 逐步擴大到 25% → 50% → 100%

如果 Knight Capital 的新訂單路由邏輯是先在一台伺服器上跑、確認正常後才逐步擴大到其餘七台,那台部署失敗、還跑著舊 Power Peg 邏輯的伺服器造成的損害,會被限制在八分之一的流量範圍內,而不是立刻對外接收全部客戶訂單。progressive rollout 本身就是本文一直強調的「先想好難看的死法」在部署流程上的具體實踐:假設這次變更會出錯,那麼出錯時,能不能只讓一小部分請求受害?

progressive rollout 也有前提。它假設「壞的影響會在觀察窗口內被偵測到」——如果壞的效應要等到高流量時段、或者要累積一段時間才會顯現,一個 5% 流量、5 分鐘的 canary 窗口可能完全看不出異常。這也是為什麼「觀察多久才算過關」本身也需要跟 failure mode 卡片一樣被明確定義,而不是憑感覺覺得「看起來沒事就好了」。

業界實例:Knight Capital 用 45 分鐘示範什麼叫「來不及」

如果要選一個教材說明 configuration failure 可以有多快變成災難,Knight Capital 是最常被引用的案例。

2012 年 8 月 1 日開盤前,Knight Capital 為了參與紐約證交所新推出的 Retail Liquidity Program,把新的訂單路由程式部署到八台生產伺服器。部署工具沒有回報錯誤,但其中一台伺服器沒有成功套用新版本——一段 2005 年就該停用、原本只給內部測試用的 Power Peg 邏輯因此被留在原地。多篇事後分析指出,新功能還重複使用了舊系統原本用來觸發 Power Peg 的同一個旗標。那台沒更新成功的伺服器收到新旗標時,執行的其實是舊的 Power Peg 邏輯,把每一筆進來的客戶訂單都當成觸發測試迴圈的訊號,開始對市場瘋狂下單。

開盤後 45 分鐘內,這套系統原本只該處理 212 筆客戶訂單,卻對市場送出超過 400 萬筆訂單,涉及 140 多檔證券、約 3.97 億股。等交易員意識到異常、手動關閉系統時,Knight Capital 已經產生 4.6 億美元虧損,隔年被收購,公司獨立營運的歷史就此結束。SEC 新聞稿

這個案例精準對應到 configuration failure 的三個具體弱點:

部分部署:8 台伺服器只有 7 台成功套用新版本,
          沒有機制阻止「不一致的叢集」繼續對外服務。
旗標重用:新舊功能共用同一個開關名稱,
          舊的死碼因此被新流程意外喚醒。
沒有 kill switch:異常訂單持續了將近 45 分鐘,
          才由人工介入關閉,系統本身沒有自動熔斷。

回頭看本文一直在強調的東西:failure-mode card 要求寫「立即減傷」與「不可做」,六種死法的表格把 configuration 獨立列成一類,都是因為它可以在極短時間內把一個健康系統變成無法控制的災難。

部署工具回報「成功」
≠
所有伺服器都真的處於一致狀態

deploy 是否完整落地到每一台機器、舊程式碼是否真的被移除或至少切斷旗標,都不是事後才補的檢查項,而是上線那一刻就要能驗證的事。

本文不把任何單一事故硬套成你的根因。案例的用途是提醒:變更時間線本身是一種 observability evidence,而你需要在「高峰時分」驗證新配置的級聯影響。

⑧ 把 failure mode 變成可執行的調查順序

收到 /ask 可用性告警時,可以先走這條小流程:

使用者 failure 是否真的發生?
  ├── 否:確認 alert / SLI 定義與抽樣。
  └── 是
       ├── 某個 deploy / flag 是否剛變更?
       │    └── 是:先評估 rollback 或關閉 flag。
       ├── dependency attempt rate 是否高於 logical request rate?
       │    └── 是:檢查 retry owner、budget 與 in-flight。
       ├── timeout / queue / saturation 是否同步升高?
       │    └── 是:考慮 fail fast、load shedding、degraded mode。
       └── 單筆 trace 的 deadline 花在哪裡?
            └── 檢查 dependency、DNS、connection、queue、application work。

五個問題各自對應一種容易被跳過的調查捷徑,把它們並排放在一起看,能更清楚每一題存在的理由:

問題 如果跳過這一題,常見的錯誤捷徑 這一題實際在防止什麼
使用者 failure 是否真的發生? 直接相信告警文字,不查實際使用者影響 誤判抽樣雜訊或告警定義錯誤為真實事故
deploy / flag 是否剛變更? 先假設是流量問題,直接考慮擴容 錯過因果關係最清楚、修復成本最低的 rollback 選項
attempt rate 是否高於 request rate? 只看 error rate,沒有比對呼叫次數 忽略 retry 正在放大流量這件事,錯失情境二的線索
timeout / queue 是否同步升高? 只看單一 metric(例如只看 CPU) 漏掉 resource exhaustion 「先變慢才失敗」的漸進訊號
單筆 trace 的 deadline 花在哪裡? 滿足於「知道有 timeout」,不再往下挖 少了鎖定根因所在 span 的最後一步,postmortem 只能寫到「timeout 發生了」

這不是完整 runbook,也不會取代 incident commander。

它刻意從使用者 failure 開始,因為 CPU 90%、provider 429、container restart 都可能只是鏈中的一段。Day 10 的分類在這裡才有用:先減少 failure,再縮小 error,最後找 fault。

走一遍:把流程圖套在一個虛構但具體的時間軸上

流程圖是抽象的,這裡用一個虛構但貼近真實節奏的時間軸,示範這五個問題實際被回答時大概長什麼樣子。假設值班人員在某天 14:02 收到 /ask 可用性告警:

14:02  收到告警:/ask 5xx ratio 超過門檻
14:03  Q1 使用者 failure 是否真的發生?
       → 查 dashboard,確認 5xx ratio 從 0.2% 跳到 4.1%,
         同時 P95 latency 沒有明顯異常(排除「只是抽樣雜訊」)
       → 是,繼續往下

14:04  Q2 某個 deploy / flag 是否剛變更?
       → 查 deployment 時間線:12 分鐘前有一次 feature flag 變更,
         時間點跟 5xx 開始上升幾乎重合
       → 是,先評估 rollback;同時繼續看下一題,
         不等 rollback 生效才開始調查(兩件事平行做)

14:05  Q3 dependency attempt rate 是否高於 logical request rate?
       → 查 dependency_attempts_total 對比 ask_requests_total:
         attempt/request 比例從平常的 1.1 上升到 2.8
       → 是,代表 retry 正在放大流量;
         查 retry=true 的 attempts 比例,確認是同一批請求在重試

14:06  執行 rollback(回應 Q2 的判斷)

14:07  Q4 timeout / queue / saturation 是否同步升高?
       → 查 in-flight requests,確認確實在累積但尚未達到資源上限
       → 是,但幅度可控,暫不需要額外的 load shedding

14:09  Q5 單筆 trace 的 deadline 花在哪裡?
       → 抽一筆失敗的 request_id,看 trace:
         時間幾乎都花在 policy_provider 這個 span,
         span 上的 error 是新 flag 版本裡一個沒被涵蓋的 schema 分支
       → 找到具體的因果鏈:新 flag → provider 收到不相容格式 →
         provider 回 error → retry 放大 → attempt rate 上升 → 5xx ratio 上升

14:11  rollback 生效,5xx ratio 開始下降
14:15  5xx ratio 回到基線,關閉告警,開始寫 postmortem 草稿

這個時間軸想示範的重點是:五個問題不是循序漸進地一次只做一件事,而是彼此提供交叉驗證:Q2 的「deploy 剛變更」讓人有理由先啟動 rollback,但不代表可以跳過 Q3、Q4、Q5;反而是 Q3 發現的 retry 放大現象,跟 Q2 的變更時間點吻合,才讓人更有把握這不是巧合。同一份 evidence(deployment 時間線、attempt rate、trace)在不同問題底下被重複核對,正是這條流程圖真正的價值;它不是要你一步步走到底才能行動,而是提醒你行動前後都要能用觀測資料回頭驗證自己的判斷。

不要把所有防護同時打開

事故時容易出現一串「看起來積極」的改動:

調大 timeout
增加 retry
加大 queue
擴容
開啟 fallback
改 alert threshold

這樣做的問題不是每一項都錯,而是你失去因果關係。

一個較安全的順序是:

1. 停止會擴大 blast radius 的 rollout。
2. 降低不必要的工作與 retry。
3. 用已設計、可標示的 degraded mode 保住核心任務。
4. 收集 metrics、logs、traces 與變更時間線。
5. 在恢復後修正 fault,並把情境加進測試或演練。

如果第 3 步不是事先設計的,事故現場通常不是第一次發明它的好時機。

為什麼順序很重要,而不是同時全開

把上面六個動作同時打開,表面上是「盡全力搶救」,但它破壞的是事故調查裡最寶貴的東西:因果關係的可追溯性。假設值班人員同時調大 timeout、增加 retry、擴容、開啟 fallback,接著 5xx ratio 真的降下來了——這時候沒有人知道降下來的原因是四個動作裡的哪一個,甚至可能是某個動作在幫忙,另一個動作在扯後腿,只是淨效果剛好是正的。

這件事的後果不只是「這次搞不清楚」,而是下一次類似的事故發生時,團隊沒有辦法從這次的經驗裡學到「哪個動作真正有效」,只能重複「把所有東西都打開試試看」這個沒有累積性的策略。因此,上面的順序把「停止會擴大 blast radius 的 rollout」放在第一步——它對應 ⑦ 一直在講的:如果 error 跟一次 deploy 或 flag 變更時間點重合,rollback 通常是成本最低、因果最清楚的第一動作,做了之後可以馬上看到有沒有效,不會跟其他改動的效果混在一起。

第二步「降低不必要的工作與 retry」對應的是 ⑤ 整節的論點:與其在系統已經過載時加大 timeout 讓更多請求排隊等待,不如先讓系統少做一點事:減少 retry、甚至暫時關閉非必要的功能,把資源留給還有機會成功的請求。第三步的 degraded mode,前提是它是「已設計、可標示」的(呼應 ⑥ 的整節論述),而不是事故現場臨時發明的權宜之計。只有在前三步都做完、系統行為已經穩定下來之後,才輪到第四步收集完整的 evidence、第五步真正修 fault。這個順序背後的邏輯其實很單純:先讓系統停止傷害自己,再讓系統恢復健康,最後才是理解到底發生了什麼。三件事分開做,才不會互相干擾彼此的訊號。

⑨ 今日 DIY:兩種 failure、同一套 evidence

今天的目標不是壓測出很高的 QPS。

目標是讓同一條 /ask path 在兩種 failure 下,都留下足以判讀的 evidence。

這個 DIY 到底在驗證什麼,讀不讀程式碼都要看懂這段

上篇加這篇前面八節談了很多原則:response contract 要區分兩個軸線、deadline 要往下傳遞剩餘預算、retry 要有唯一擁有者、timeout 不能藏在 200 OK 裡。這些原則單獨看都合理,但合不合理最終要靠一件事驗證:同一段程式碼,能不能在正常與異常兩種路徑下,都留下足以事後重建「發生了什麼」的證據。

這個 DIY 專案驗證的是:練習 A、B、C 能不能從 evidence 找到那個 timeout / retry,而不是請求有沒有成功。一個系統的 evidence 品質,往往要等到它壞掉的那一刻才看得出來——平時運作正常時,粗糙的 logging 一樣能撐過去;只有在 timeout、retry、degraded 同時發生時,才會看出 request_id 有沒有貫穿 metric/log/trace 三種資料、retry 的次數有沒有被誠實記下來、失敗的回應有沒有被悄悄包裝成成功。

具體對應到前面的論點:

練習 A(慢 provider)
  驗證的論點:上篇④ 的 response contract 設計 ——
  timeout 發生時,回應不能是「看起來成功但其實沒有答案」,
  而必須明確標示 unavailable,且這個標示能在 log/trace/metric 三處被獨立確認。

練習 B(不可用 provider + 有限 retry)
  驗證的論點:上篇⑤ 的 retry owner 與 retry budget ——
  重試次數有沒有真的被限制在 MAX_RETRIES,
  attempt rate 是否真的可以跟 logical request rate 分開量測。

練習 C(自己寫一張 failure-mode card)
  驗證的論點:上篇① 的「failure mode 是可操作契約,不是 alert 名稱」——
  能不能把一個自己選的 dependency,寫成一張別人也看得懂、
  能拿去對照程式碼行為的卡片。

如果讀者不打算真的把 DIY 專案跑起來,至少讀懂上面這張對照表,就能理解這個 DIY 在系列文章裡的位置——它不是「附加的練習題」,而是「用最小可運作的程式碼,讓前面的抽象論述變成可以肉眼檢查的具體行為」。

前置條件

沿用 Day 2 的隔離 SRE Lab,或在讀者自己的 Day11/DIY 專案準備 FastAPI 與 Uvicorn。建議的最小結構:

Day11/DIY/
├── pyproject.toml
└── app/
    └── main.py

main.py 可使用本文情境一的程式,再把情境二的 call_provider_with_budget() 接到 /ask。

若你的 Lab 已有 Prometheus、Loki、Tempo 或 OpenTelemetry Collector,保留 Day 2 的設定並新增本文的 counters、structured events 與 child span。不要為了這篇文章另外建立第二條 telemetry pipeline。

練習 A:慢 provider

  1. 先以 failure_mode: "normal" 送一筆請求,記下 request_id。
  2. 再以 failure_mode: "slow" 送一筆請求。
  3. 從回應、log、trace 與 metric 各找一次那個 timeout。
  4. 確認 slow request 沒有得到 answer_status: "answered"。
  5. 確認 metric label 沒有出現 request_id 或 question。

預期會看到什麼:正常請求應在幾毫秒內回 200,body 帶 answer_status: "answered";慢請求應在 CLIENT_TIMEOUT_SECONDS(約 250ms)左右回 503,body 帶 answer_status: "unavailable",同時 log 裡出現一筆 dependency_timeout 事件,欄位裡的 elapsed_ms 應該落在 timeout 門檻附近,而不是遠遠超過它——如果 elapsed_ms 明顯超過 250ms 太多,代表 asyncio.wait_for() 沒有確實生效,可能是呼叫鏈裡混進了某個不回應取消的阻塞呼叫。

容易踩的坑:第一次跑這個練習的人,很容易漏掉「先送 normal 請求」這一步,直接跳去測 slow,這樣就沒有基準可以比對「正常時 log 長什麼樣子」,事後也不容易判斷 timeout 版本的 log 差在哪裡。另一個常見的坑是把 request_id 誤植進 Prometheus 的 label 裡——寫程式時很容易順手把「這次請求的所有資訊」都塞進同一個 emit 呼叫,卻忘了 metric 跟 log/trace 對「該記錄什麼」有完全不同的規則;驗收步驟 5 特別把這件事單獨列出來,就是因為它是最容易被無意間破壞、卻又不會讓程式報錯的錯誤。

練習 B:不可用 provider 與有限 retry

  1. 將 handler 改為呼叫 call_provider_with_budget()。
  2. 以 failure_mode: "unavailable" 送一筆請求。
  3. 確認最多只有一次 retry,也就是最多兩次 dependency attempt。
  4. 檢查 dependency_retry_scheduled log 是否有 retry_attempt 與 jitter delay。
  5. 在隔離環境用少量併發重複請求,觀察 attempt rate 和 logical request rate 的差距。
  6. 將 retry 暫時換成錯誤範例比較,但不要把它留在正常路徑。

預期會看到什麼:單一請求應該產生剛好兩筆 dependency 相關的 log(第一次嘗試失敗、觸發 dependency_retry_scheduled,接著第二次嘗試也失敗,最終回 503),且第二次嘗試前有一段 jitter 延遲,每次執行這個延遲值應該不完全相同——如果每次跑出來的 backoff 數字都一模一樣,代表 random.uniform() 沒有真的參與計算,很可能是程式碼裡不小心把 jitter 上限寫成固定值。步驟 5 用少量併發重複送請求時,應該能算出 attempt rate / logical request rate 大約落在 2 上下(每個請求最多兩次 attempt),如果這個比例遠高於 2,代表有某一層在你沒注意到的地方額外做了重試,這正是情境二整節在講的「多層 retry 疊加」的縮小版重現。

容易踩的坑:步驟 6 特別提醒「不要把它留在正常路徑」,是因為 wrong_retry() 這個錯誤範例寫起來比 call_provider_with_budget() 簡單很多,實測時很容易因為「先求能動」而順手把簡化版留下來,之後忘記換回去。另一個坑是在本機用迴圈製造併發時,容易不小心把併發數開得太大,讓 laptop 上的 event loop 或作業系統本身成為瓶頸,量到的其實是本機資源限制而不是 retry 放大——這也是文中特別提醒「先從 5 個開始,不要把自己的 laptop 當 DDoS 練習場」的原因,數字失真比沒有數字更容易誤導後續判斷。

練習 C:寫一張你自己的 failure-mode card

選一個不是本文 provider 的 dependency,例如 PostgreSQL、Redis、email provider 或 tool API。

## <dependency> unavailable

- 觸發條件:
- Error class:
- 使用者 failure:
- retriable / non-retriable 條件:
- retry owner 與上限:
- fallback 是否允許:
- 需要的 metric / log / trace evidence:
- 立即減傷:
- 不可做:
- 需要人類確認的項目:

最後一行很實際。不是每個 fallback 都能自動做決定;涉及資料正確性、權限或不可逆操作時,系統應把不確定性留在合約裡,而不是藏在背景。

寫這張卡片時,刻意選一個跟本文範例不同的 dependency(PostgreSQL、Redis 或某個 tool API),是為了確認前面學到的框架能不能真正遷移到另一種依賴上,而不是只在「provider timeout」這個特定情境下說得通。一個常見的檢驗方式,是拿寫好的卡片對照上篇③的六種常見死法,看看自己選的 dependency 更接近哪一種或哪幾種組合——PostgreSQL 不可用,通常同時牽涉 network 與 dependency 兩類;Redis 連線耗盡,通常是 resource 與 dependency 的組合;tool API 的 schema 不相容,則更接近 configuration 這一類。

驗收

[ ] 能區分 failure mode、alert 與 root cause hypothesis。
[ ] 已在文件或程式中定義 `/ask` 的 unavailable response contract。
[ ] 慢 provider 情境能產生可關聯的 response、log、trace 與 metric 設計。
[ ] `request_id` 僅用於 log / trace 關聯,沒有進 Prometheus label。
[ ] 已指定唯一的 retry owner、可重試 error class 與每個 request 的上限。
[ ] 已理解多層 retry 會乘法放大 downstream attempts。
[ ] 已為 fallback 定義來源、新鮮度與 `degraded: true` 的揭露;
    或明確決定在此 failure 下回 503。
[ ] 已為至少一個 configuration / dependency failure 寫好 failure-mode card。
[ ] 沒有把 Lab 的 timeout、延遲或一次結果宣稱為 production SLO 或已驗證行為。

本文未替讀者建立環境、安裝依賴、啟動服務、送出請求或驗證上述練習。

⑩ 本文結論

Production system 不會只用一種方式壞掉。

比較可靠的做法不是替每個 exception 補 retry,而是先定義:

什麼條件算 failure mode?
哪一層擁有 retry?
什麼工作可以安全少做?
什麼情況必須誠實地拒絕?
用哪些 evidence 證明系統正朝恢復,而不是朝級聯失敗前進?

你不需要預測所有事故。

但至少要讓最常見的 timeout、dependency unavailable 與 bad configuration,不會在壓力下被「多試幾次」放大成整條服務都做無用功。

把今天五個案例放在一起看

今天引用的五個真實事故(Google Shakespeare Search 的資源洩漏、一個 Agent 花 200 美元重試注定失敗的請求、GitHub 2026 年 8 月的 retry storm、Knight Capital 的死碼復活、Cloudflare 2025 年 11 月的權限變更)乍看分屬不同的 failure mode 分類,但把它們並排放在一起,會浮現一條共同的線索:每一個事故的「初始 fault」都不算特別罕見或特別離奇,資源沒釋放乾淨、重試邏輯沒區分錯誤種類、部署工具沒回報一台伺服器失敗、資料庫權限調整意外暴露重複欄位、一個運作良好的子系統在高峰流量下觸發隱藏的交互作用,這些放在任何一個工程團隊的待辦清單裡,都不會被列為「高風險項目」。

真正讓它們變成大型事故的,是本文反覆強調的那個環節:系統面對這些初始 fault 時,沒有被設計成「少做一點事」,而是被動或主動地選擇「做得更多」——洩漏的資源在重試中被反覆消耗、注定失敗的請求被無止盡地重試、放大十倍的流量被送進已經吃緊的服務、沒套上新程式碼的伺服器繼續對外服務、超出容量假設的設定檔被照常處理、客戶端在資料庫過載時集體選擇重試而不是後退。如果這幾個系統在對應的那個環節,能夠像本文的 failure-mode card 要求的那樣,明確寫下「不可做」的那一條,結果很可能完全不同。

Day 12 預告

今天的 failure 大多能從 HTTP status、timeout、queue 與 dependency error 看見。

Day 12 會看更麻煩的一類:模型、retriever、tool 與 parser 都沒有明顯 crash,卻讓 AI workflow 在技術成功裡產生 task、quality 或 safety failure。

今天定義的 response contract、retry owner、circuit breaker 與 failure-mode card,在 Day 12 不會被丟掉——它們會被拿來當底層保護:一條 AI workflow 呼叫的 LLM、retriever、tool 任何一段,本質上都是本文說的一種 dependency,一樣需要 deadline、一樣需要有邊界的 retry。差別只在於,Day 12 要多處理一種今天完全沒遇到的情況:呼叫「技術上成功」,回傳的內容卻不能被信任。

延伸閱讀


這篇是 Learning SRE for the AI Era 系列的一部分。
Build → Trace → Break → Measure → Evaluate → Recover → Improve.


上一篇
Day 10(下)|Fault、Error、Failure:別把警報名稱當成根因
下一篇
Day 12(上)|AI Workflow Failure Modes:模型有回話,不等於工作完成
系列文
Learning SRE for the AI Era:從 SRE Lab 到 Production AI Reliability 共 44 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言