iT邦幫忙

2026 iThome 鐵人賽

DAY 3
0
AI Engineering

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

Day 16(下)|平均 Latency 為什麼會騙人?請看尾端

  • 分享至 

  • xImage
  •  

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

結論先說:histogram 埋對了只是起點,dashboard 要先讓值班的人看到「使用者受影響多少」,AI workflow 裡一次次呼叫疊起來的尾端延遲,往往比任何單一服務的 P99 都危險。

上篇(Day 16 上)拆解了「平均值為什麼會騙人」:P95 不等於保證 95%、percentile 有 nearest-rank 與內插兩種算法、並動手在 FastAPI 用 Prometheus histogram 記下完整分布,而不是只印一個平均數。下篇接著把這份分布資料變成值班可用的 dashboard,並拆穿幾個常見誤判。

⑦ dashboard:讓值班的人先看到影響,再看到機器

Day16 的 dashboard 不需要一口氣放滿 GPU、CPU、每個 provider 和每一條 query。

先做一個由外向內的版面:

row 1  使用者體驗:request rate、P50、P95、P99、成功比例
row 2  workflow:queue delay、retrieval、TTFT、generation、tool latency
row 3  依賴:provider error、vector DB duration、tool API duration
row 4  資源:worker saturation、connection pool、CPU / GPU utilization

把這個版面翻譯成一張可以直接貼進 Grafana 的 panel 骨架,示意每一列大概放什麼查詢:

{
  "title": "Row 1 - User Experience",
  "panels": [
    {"title": "Request Rate", "expr": "sum(rate(ask_request_duration_seconds_count[5m]))"},
    {"title": "P50", "expr": "histogram_quantile(0.50, sum by (le) (rate(ask_request_duration_seconds_bucket[5m])))"},
    {"title": "P95", "expr": "histogram_quantile(0.95, sum by (le) (rate(ask_request_duration_seconds_bucket[5m])))"},
    {"title": "P99", "expr": "histogram_quantile(0.99, sum by (le) (rate(ask_request_duration_seconds_bucket[5m])))"},
    {"title": "Success Ratio", "expr": "sum(rate(ask_request_duration_seconds_count{workflow_status=\"completed\"}[5m])) / sum(rate(ask_request_duration_seconds_count[5m]))"}
  ]
}

這不是一份要照抄貼上就能跑的完整 Grafana provisioning 檔,而是示意結構:第一列的每個 panel 都是使用者視角能看懂的問題,不是「CPU 用了多少」。

第一列先問「誰在等」。

最後一列才問「哪台機器在喘」。

這個順序能避免把 CPU 70% 直接等同於使用者體驗差,也避免看到 P95 升高就立刻擴容。

一個實際的判讀流程

假設 14:20 的 P95 從 1.2 秒跳到 4.8 秒:

  1. 確認 request count 是否足夠,排除單一樣本把 P95 推高。
  2. 看 P50 是否也上升:若只有 P95/P99 變差,優先懷疑 tail、排隊或偶發 dependency。
  3. 看 workflow breakdown:queue、retrieval、TTFT、generation、tool 哪一段有同方向變化?
  4. 看 error、fallback、timeout 是否同步增加;慢與錯常會一起出現,但不是必然。
  5. 依時間窗用 request_id、trace_id 找少數慢樣本,確認 prompt、model route、文件數與工具呼叫。
  6. 看部署 annotation:prompt、model、依賴設定或程式版本是否剛好改變。
  7. 根據證據選擇 mitigation:限流、縮小 context、切 fallback、擴 worker、回退版本,或暫停 rollout。

這套順序刻意從使用者影響開始。

直接從 CPU、記憶體或某一筆 exception 開始,很容易修到一個剛好存在、卻不是這次 tail latency 的症狀。

把上面的流程走一次:一個假想的值班場景

抽象的七步驟不容易記,走一次具體場景會清楚很多。以下是一個依照本文設計出來的假想值班情境,用來示範這套判讀順序實際怎麼用,不是真實 incident 記錄。

時間 14:20,值班的 on-call 工程師收到告警:/ask 的 P95 從平常的 1.2 秒跳到 4.8 秒。

14:20  告警觸發:p95_latency_seconds{route="/ask"} > 3
14:21  第一步:看 request count
       過去 5 分鐘有 340 筆請求,樣本數足夠,排除單筆離群值撐起整條線的可能
14:22  第二步:看 P50
       P50 從 180ms 只微升到 210ms,P95/P99 卻大幅上升
       → 初步判斷:多數請求正常,問題集中在尾端
14:24  第三步:看 workflow breakdown
       retrieval_ms、queue_ms 平穩
       ttft_ms 的 P95 從 300ms 上升到 3.9 秒
       generation_ms 平穩
       → 問題縮小到「拿到第一個 token 之前」這一段
14:26  第四步:看 error / fallback / timeout
       5xx 沒有明顯上升,workflow_status=completed 比例正常
       → 這不是錯誤事件,是「慢但沒壞」
14:29  第五步:用 trace_id 撈幾筆慢樣本
       發現這些慢請求的 model_route 都指向同一個 provider endpoint
14:31  第六步:看部署 annotation
       13:55 有一筆變更:把預設 model_route 從 primary 切到 secondary(灰度測試)
14:33  第七步:選擇 mitigation
       把灰度流量切回 primary,觀察 TTFT 是否回落
14:41  P95 回到 1.3 秒附近,宣告緩解

這個場景刻意設計成「沒有任何一段 CPU 或記憶體出問題」,純粹是流量被切到一個排隊比較嚴重的 provider endpoint。如果值班工程師一開始就直奔資源使用率的 dashboard,大概率會看到一片正常的 CPU 曲線,然後陷入「明明資源正常,為什麼會變慢」的困惑——這正是 ⑦ 段開頭強調「先問誰在等,再問哪台機器在喘」的原因:資源正常,不代表使用者體驗正常;體驗變差的根因,經常藏在應用層的路由、佇列或第三方依賴,而不是基礎設施層。

⑧ AI workflow 的尾端,常藏在「乘法」裡

傳統同步 API 已經會受 dependency tail latency 影響。

AI workflow 又常將多個步驟串起來:

retrieve
  + rerank
  + model TTFT
  + model generation
  + optional tool call
  + validation

每一段都有自己的分布。

如果每個階段都只在 P95 看起來「還可以」,端到端 tail 仍可能不可接受。

舉例來說,retrieval P95 400 ms、TTFT P95 1.2 秒、generation P95 2 秒、tool P95 1 秒,不能直接相加後就宣稱端到端 P95 是 4.6 秒;慢事件是否同時發生取決於相關性。

但這份加總足以提醒我們:別讓每個 team 都只優化自己的局部平均值,最後由使用者承擔整條鏈的尾端。

每個 team 都達標,使用者卻不滿意

這個現象值得單獨講一次,因為它在組織裡特別容易發生。

retrieval team 的 OKR 是「P95 維持在 400ms 以下」,他們達標了。

model serving team 的 OKR 是「TTFT P95 維持在 1.2 秒以下」,他們也達標了。

tool team 的 OKR 是「tool call P95 維持在 1 秒以下」,同樣達標。

三個 team 的季度回顧都寫著「本季 SLO 全部達標,服務品質穩定」。

但使用者感受到的是這三段疊在一起的端到端體驗。

如果三段慢請求剛好落在同一次請求裡的機率不算低,使用者的端到端 P95 完全可能遠超過任何一個 team 自己回報的數字。

這不是任何一個 team 說謊,是組織把一個端到端的使用者體驗問題,拆成了互不相干的局部指標。

解法不是取消各 team 的局部 SLO,而是在它們之上,額外保留一條端到端的 SLI,由沒有局部利益的角色(例如平台團隊或 SRE)負責盯著。

Day17 會正式把這種端到端 SLI 的定義方式講清楚。

context 越大,不等於答案越可靠

當 P95 突然上升,常見反應是「多塞一點文件給模型」。

這可能同時拉高 prompt token、TTFT、成本與 parser 壓力。

更多 context 有時提高 groundedness,有時只增加噪音;它是 evaluation 問題,也是 latency 問題。

比較兩個 retrieval 設定時,請至少並排看:

面向 設定 A 設定 B 要問的問題
retrieved documents 3 12 多出來的文件真的有用嗎?
input tokens 較少 較多 TTFT 是否被拉長?
P95 end-to-end 較短 較長 使用者 deadline 是否仍成立?
groundedness evaluation 待量測 待量測 品質有沒有換到可證明的改善?
token cost 較低 較高 每個成功任務的成本是否合理?

數字欄位故意寫「待量測」。

沒有跑過固定資料集和相同流量條件,不要把「文件更多」說成品質更好,也不要把「P95 更短」說成最佳設計。

這張表刻意把 latency 欄位和 groundedness、cost 欄位放在同一張表裡,是想強調一個常被拆開來看、但其實密不可分的事實:retrieval 設定的每一個改動,幾乎都同時是延遲問題、品質問題、成本問題三者疊在一起的決策。

只從 latency 團隊的角度看,會傾向把 TopK 調小,讓 P95 好看;只從 evaluation 團隊的角度看,可能傾向把 TopK 調大,追求更高的 groundedness 分數;只從財務角度看,兩邊都嫌貴。

如果三個團隊各自拿著自己的那個欄位去做決策,很容易在不同季度來回搖擺,調大又調小,卻從未真正解決任何一方的問題。

比較健康的做法,是像這張表一樣,把三個維度攤在同一張表格裡,一起看,一起決定要往哪個方向妥協——這也呼應⑧段稍早那個「每個 team 都達標,使用者卻不滿意」的教訓:局部最佳化,經常是全域次佳的來源。

retry 可以讓尾端變成雪崩

慢請求最危險的修法之一,是沒有上限地重試。

若 provider 原本已擁塞,client timeout 後再送一次,不只延長那位使用者的等待,還替 provider 增加一個新工作。

這不是紙上談兵。DoorDash 在 2021 年 6 月 19 日的一次正式 postmortem 中,公開描述了幾乎一模一樣的機制:16:30 PDT,payment 基礎設施開始出現高延遲,Dasher App 依賴的下游呼叫回應時間拉長;16:35 系統告警觸發、工程師被 page。讓事故真正升級的不是延遲本身,而是接下來發生的事——Dasher 端系統偵測到請求變慢或逾時後,開始對本已不健康的 payment 服務發起額外重試,這些重試流量疊加在原本就吃緊的基礎設施上,形成經典的正回饋迴圈:延遲越高、重試越多;重試越多、延遲越高。事故一路升級到 17:19 PDT 被迫暫停顧客下新單、17:22 對 Drive 合作夥伴做同樣處置止血,真正的轉折點在 18:25——團隊靠設定變更阻斷了下游 payment 呼叫,而不是靠更聰明的重試策略,才讓系統有空間喘息、逐步恢復。從 16:30 到 18:36,多數 Dasher 完全無法接單或上線,事故總長超過兩小時。這個案例的重點不是「DoorDash 的重試邏輯寫得差」,而是提醒我們:沒有上限、沒有 circuit breaker 概念的重試,在系統已經處於高延遲狀態時,止血手段往往是「先讓下游呼叫停下來」,而不是「更努力地重試」。

AI 呼叫還會增加 token 成本;每次 retry 都可能重新送入 prompt。

把 retry 的成本換算成實際數字

「每次 retry 都可能重新送入 prompt」講起來是一句話,但把它換算成具體數字,才會知道這件事在 AI workflow 裡有多昂貴。假設一次 /ask 呼叫的 prompt(含檢索回來的文件片段)平均是 3,000 input tokens,輸出平均是 500 output tokens,用一個常見的定價量級估算(每百萬 input tokens 數美元、output tokens 通常是 input 的數倍價格),一次呼叫的成本可能落在幾分錢美金的等級——單看很便宜。但把 retry 疊上去:

正常路徑(無 retry)
  1 次呼叫 = 3,000 input + 500 output tokens = 成本 X

client timeout 後重試一次(無上限 retry 的情境)
  第 1 次:3,000 input + 500 output(provider 端可能仍在處理,只是 client 沒等到)
  第 2 次:3,000 input + 500 output(重新送出同一個 prompt)
  = 至少 2 倍的 token 成本,而且 provider 端可能同時在處理兩份幾乎一樣的工作

如果重試策略沒有上限,且觸發條件過於寬鬆(例如任何 timeout 都無條件重試 3 次),一個流量高峰期間的 provider 延遲,可以在幾分鐘內把 token 成本推高到平常的數倍——這還沒算上 DoorDash 案例裡真正致命的那件事:這些重試流量疊加在已經吃緊的 provider 之上,讓延遲更嚴重,觸發更多 timeout,形成 ⑧ 段開頭那句「延遲越高、重試越多;重試越多、延遲越高」的正回饋迴圈。傳統 Web API 的 retry 風暴,代價主要是「更多連線」和「更長的排隊」;AI workflow 的 retry 風暴,多了一層「每次重複都在燒真金白銀的 token 預算」,這是為什麼 AI 系統的 retry policy 不能直接沿用傳統系統的預設值,需要額外把成本預算算進去。

可以先把 retry policy 寫成可閱讀的 contract:

only retry: connection reset、temporary 429、明確可重試的 5xx
do not retry: invalid request、policy denial、parser contract failure
max attempts: 2
timeout: bounded by the user-facing deadline
backoff: exponential with jitter
record: attempt number, retryable reason, final outcome

這不是萬用設定。

它要求你在寫 retry 前回答:原本請求是否可能已被 provider 執行?重送是否具冪等性?剩餘 deadline 還夠不夠?成本預算是否允許?

Day29 會回來把 timeout、backoff、circuit breaker 和 degradation 放到同一個故障模型中;今天先不要用 retry 把一張慢圖遮掉。

一個簡化的判斷順序:遇到慢下游,該用哪一招

讀者讀到這裡,可能已經在腦中同時裝了 hedged requests、retry、timeout、circuit breaker 好幾個工具,容易混淆什麼時候該用哪一個。

先講清楚它們各自解決的問題不同,不是互相替代的關係。

下游偶爾、獨立地變慢(stragglers)
  → hedged requests:用多打一份請求換取「取先到的那個」

下游暫時失敗、值得再試一次
  → bounded retry:有上限、有 backoff、確認冪等性

下游已知會花多久、你只是要設一個放棄的底線
  → timeout:不是用來讓下游變快,是用來保護自己的資源

下游持續、系統性地故障,重試只會讓情況更糟
  → circuit breaker:先暫停呼叫,給下游喘息空間,自己也切到 fallback

四個工具分別對應四種不同的下游行為模式,用錯地方,效果適得其反:對一個持續故障的下游用 hedged requests,等於把負荷直接翻倍打上去,加速它的崩潰;對一個系統性故障的下游只調大 timeout,只是讓使用者等更久才等到同一個失敗結果。

Day29 會把這四個工具放進同一張決策表,搭配明確的判斷條件;今天先記住:它們是針對不同故障模式設計的,不是「效果差不多、選一個順手的就好」的同義詞。

⑨ 常見誤判:漂亮平均值、醜陋體驗

這一段整理的五個誤判,有一個共同的心理機制:它們都是「看到一個數字改善了,就停止追問」的結果。

percentile、histogram、breakdown log 這些工具,存在的目的不是取代判斷,而是讓「停止追問」這個動作,多一道有證據支撐的關卡。

以下每個誤判,都是本文前面某個概念被略過之後,實際會發生的具體後果。

誤判一:只看 mean,就宣布版本更快

新版模型把 95% request 從 700 ms 降到 500 ms,卻讓 5% request 從 2 秒變成 20 秒。

mean 可能仍下降。

若使用者恰好落在那 5%,他不會因為其他人變快就覺得自己被服務得很好。

用具體數字看這個陷阱有多容易發生:

舊版本:100 筆請求,全部落在 650-750ms 之間
  mean = 700ms

新版本:100 筆請求
  95 筆落在 480-520ms(改善明顯)
  5 筆落在 19,000-21,000ms(某個 edge case 觸發重試循環)
  mean = 95×500ms/100 + 5×20,000ms/100 = 475 + 1,000 = 1,475ms

咦,這個算法下新版本 mean 反而更差,看起來很容易被抓到。

問題在於很多團隊比較的不是逐筆重算的 mean,而是儀表板上滾動窗口的移動平均——如果那 5% 的極端值恰好被平滑掉,或是被同一時間窗內大量的正常請求稀釋,滾動平均線很可能還是往下走的。

這正是為什麼「附上 count、P50、P95、P99」不是嚴謹主義的形式要求,而是唯一能攔住這種誤判的辦法:任何一個「變快了」的結論,都要能同時交叉看到分布沒有被破壞,而不是只看一條平滑過的線在往下掉。

每次宣稱「變快」至少附上 count、P50、P95、P99、時間窗、request class 與版本資訊。

誤判二:看到 P99 就立刻擴容

P99 上升可能是 worker 飽和,也可能是單一 provider 請求卡住、GC pause、DNS 重試、過長 prompt 或低樣本。擴容也許有用,但它不是解釋——先用 trace 找共同特徵,再決定是否把錢花在更多 worker、更多 GPU、provider 路由或更小的 context。

一個常見的反例:P99 上升,團隊立刻加開兩倍 worker,P99 卻紋風不動。事後查才發現,瓶頸根本不在 worker 數量,而是所有 worker 都在排隊等同一個外部 provider 的 API 配額——worker 越多,只是讓越多請求同時卡在等待配額的狀態,佇列變得更長而不是更短,還多花了運算資源的錢,錢燒在了不會解決問題的地方。先看 trace 裡「時間花在哪一段」,比先看「哪個資源使用率比較高」更接近真正的瓶頸。

誤判三:把 timeout 從 3 秒調成 30 秒

較大的 timeout 會減少「timeout error」,卻不會讓下游更快,可能讓 queue 更長、佔住 connection、延後 fallback、讓使用者等更久。timeout 是 deadline contract 的一部分;變更前要看 end-to-end latency、併發、retry 行為與使用者能接受的等待時間。

把 timeout 想成一個「你願意讓使用者等多久,系統才主動放棄」的承諾——調大它不會讓下游變快,只是把「放棄的時間點」往後推。在被推後的這段時間裡,原本那個 connection、worker、配額都被佔住,無法服務其他請求;如果同時有更多使用者送出新請求,佇列會因為舊請求遲遲不肯放手而變得更長,更長的佇列又意味著更多新請求要等更久才輪到自己。這是另一種正回饋迴圈,跟 ⑧ 段講的 retry 風暴同樣的機制:一個試圖止血的設定,反而讓失血速度加快。正確的順序是先問「這個下游本身該花多久」,而不是先問「要設多大 timeout 才不會跳錯誤」。

誤判四:用低流量的 P99 寫 SLA

十筆請求裡的 P99,沒有足夠樣本支撐精細的外部承諾。先累積足夠資料,或改用較適合低流量服務的評估方式,例如 synthetic check、request count guardrail 與案例測試。SLA 是對外責任,不是 dashboard 上一個看起來專業的數字。

用數字說明這有多不可靠:nearest-rank 算法下 ceil(0.99 × 10) = 10,十筆裡的 P99 就是排序後最後一筆——本質上等於「這批樣本裡最慢的那一筆」,跟其餘九筆完全沒有關係。只要剛好有一筆請求因冷啟動、GC 或任何偶發原因變慢,P99 就會直接反映那唯一一筆,跟系統整體表現的關聯性微乎其微。要讓 P99 具備統計意義,得有足夠樣本讓「第 99 百分位」真的代表一群請求裡的某個位置,而不是單獨一筆的巧合。對流量本來就低的內部工具或早期產品,與其硬套 P99 SLA,更誠實的做法是先用 synthetic check 搭配 request count guardrail(明確標示樣本數低於門檻時僅供參考),等流量成長到可信的量級,再談外部 SLA 承諾。

誤判五:只看 service 成功,忽略 client cancellation

使用者已關閉頁面,server 仍完成了 30 秒生成;後端可能把它記為 successful request。server-side latency 仍有價值,但它不能替代 user journey metric。若收集 client telemetry,必須先處理 consent、資料最小化與身份資訊,不要為了做漂亮圖表把敏感 prompt 一起送走。

這個誤判特別容易發生在串流場景:使用者看到前幾個 token 就覺得「有反應了」,切走分頁去做別的事,但 server 端不知道使用者已離開,連線技術上還開著,繼續把剩下的 token 一個個生成、送出。30 秒後生成完畢,伺服器把這筆請求記成 workflow_status=completed,histogram 也正常記了一筆 30 秒的 observation——從系統角度看是一次成功、只是比較慢的請求;從使用者角度看,他根本沒等到答案。要抓到這種落差,通常需要偵測連線是否仍存活(例如 FastAPI 的 request.is_disconnected()),把「client 已斷線」標記成獨立的 workflow_status,而不是塞進 completed 或 failed 裡混淆視聽。多花的那 30 秒運算資源與 token 成本也是實際發生的浪費——即使使用者從未見到結果,provider 帳單一樣照算。

誤判六:把「context 變大」和「P95 變長」誤認為因果不明的巧合

團隊常常同時做兩件事:加大 retrieval 的文件數量、同時觀察到 P95 上升,卻把這兩件事當成兩條獨立、需要分開排查的線索。實際上它們經常是同一件事:更多文件代表更多 input tokens,更多 input tokens 直接拉長 TTFT(模型需要先處理完整個 prompt 才能吐出第一個 token),也可能拉長 generation(更長的上下文有時讓模型輸出也變長)。這不是巧合,是 ⑧ 段「context 越大,不等於答案越可靠」那張對比表想強調的因果鏈——如果你剛好在同一週做了「加大 retrieval TopK」和「調整 timeout 設定」兩件事,P95 上升時,請先檢查有沒有部署 annotation 對到「加大 TopK」的那次變更,而不是先假設是 timeout 設定出了問題。

五種誤判放在一起看

誤判 表面現象 真正該檢查的東西
只看 mean 宣布變快 平均值下降 P50/P95/P99 是否同時下降,還是被少數樣本拉動
看到 P99 就擴容 P99 上升 是資源飽和,還是單一依賴、GC pause、低樣本
把 timeout 調大 timeout error 減少 end-to-end latency、佇列長度是否同時惡化
用低流量 P99 寫 SLA 樣本數不足 是否累積了足夠請求量再承諾外部指標
只看 service 成功 忽略 client cancellation server-side 完成 ≠ 使用者真的收到結果
context 變大與 P95 變長視為巧合 兩個獨立變更同時發生 部署時間軸是否指向同一次變更

這張表格的共同結論是:每一個誤判的根源,都是把「看到的現象」直接當成「發生的原因」,跳過了中間本該做的交叉檢查。tail latency 的排查,幾乎每一步都需要至少兩個獨立訊號互相印證(P50 與 P95 一起看、latency 與 error rate 一起看、metric 變化與部署時間軸一起看),單一訊號幾乎永遠不足以下結論。

⑩ 今日驗收:你能解釋一條 tail latency 圖嗎?

以下是讀者自行執行 Lab 後可勾選的驗收項目。

  • [ ] 已明確寫下 latency event 的開始點與結束點。
  • [ ] 已將互動 API、batch job、health check 分開量測或明確排除。
  • [ ] 已建立有限 bucket 的 histogram,而不是只有平均值。
  • [ ] metric label 沒有包含 request ID、user ID、prompt、完整錯誤字串。
  • [ ] 已送出固定比例的受控慢請求,並記錄請求數與 delay 值。
  • [ ] 已在同一個時間窗同時查看 P50、P95、P99 與 sample count。
  • [ ] 能區分 server end-to-end、TTFT、generation、queue、retrieval 與 tool latency。
  • [ ] 已確認 histogram percentile 是 bucket 估算,並理解小樣本限制。
  • [ ] 已能從 P95 異常的時間窗找到至少一筆對應 log 或 trace。
  • [ ] 已避免把 Lab 的 3 秒 target 寫成已承諾或已驗證的 production SLO。
  • [ ] 已記錄任何 fallback、retry、prompt 或 model version,避免將版本差異誤讀成效能差異。

為什麼是這十一項,而不是別的

這份清單每一項都對應本文某個曾經講過的教訓:第一項對應 ①、⑤ 段「percentile 描述的是一個已定義好的母體」;第二到四項對應⑤段「一致的母體」,避免混進不同流量或高基數 label;第五、六項對應 ②、⑥ 段的 DIY 精神;第七項對應④段「一句『很慢』不夠拿來排障」;第八項對應⑥段步驟 5「histogram_quantile 是估算」;第九項對應④段 latency contract 與⑦段判讀流程;第十、十一項對應⑨段的常見誤判。如果讀者跑完 Lab 回頭勾不完這十一項,不代表 Lab 失敗——它剛好標出了下一步該補強的地方。

⑪ 限制與下一步

本文的 injection 只是在 application 內加上可預期的等待。

它不會模擬真實 provider 的 token streaming、網路抖動、排隊理論、connection pool 耗盡或 GPU VRAM 壓力。

它的價值是把「平均值看起來正常」這個錯覺變成可重跑的反例。

這個 Lab 練習,跟真實 production 的差距在哪

老實列出差距,比假裝這個 Lab 已經涵蓋一切更有用。

Lab 練習 真實 production 差距的影響
asyncio.sleep() 固定延遲 provider 端排隊、GPU 搶佔、真實網路抖動 真實延遲的分布形狀比固定值複雜得多
單機、單 worker 多副本、跨可用區、負載平衡 需要額外考慮 hedge、跨副本聚合
20 筆手動送出的請求 每秒數百到數千筆的真實流量 percentile 的統計意義完全不同
沒有真實下游依賴 database、cache、第三方 API 各自有自己的分布 端到端 latency 是多個分布的疊加
沒有並發控制 connection pool、rate limit、backpressure 高並發下的排隊效應在 Lab 裡看不到
一次性測試 全天候、跨時區的流量波動 需要看不同時間窗下 percentile 是否穩定

這張表不是要讀者氣餒,而是提醒:本文的 DIY 只解決「我有沒有能力量到分布」這個第一層問題。

第一層問題沒解決之前,後面的排隊理論、hedge、跨副本聚合都無從談起。

這也是為什麼 SRE 的能力養成常常是階梯式的:先確保看得見,才談得上優化。

如果 P95 完全沒反應,可能不是 Prometheus 的問題

除了 ⑥ 段步驟 4.5 提到的 scrape target 檢查,還有幾個更隱蔽的可能性值得排除。第一,bucket 邊界沒有涵蓋你注入的延遲值——如果 buckets 只設到 1 秒而你注入 500ms,這筆請求會被歸進 le="1" 這個桶,不會單獨顯示成異常。第二,label 基數爆炸讓某些 series 被 Prometheus 丟棄——如果不小心把 request_id 當成 label(⑥ 段步驟 1 警告過這件事),可能觸發 sample limit,悄悄丟掉部分資料點。第三,middleware 或 reverse proxy 層有自己的 timeout,請求根本沒走到你埋 histogram 的那段程式碼——這種情況下 Prometheus 端看起來完全正常,因為被攔截的請求根本沒機會被計入。排查時先問「這筆慢請求有沒有真的執行到我埋 metric 的那一行」,比懷疑 Prometheus 設定錯誤更快找到答案。

若 P95 沒有隨注入延遲上升,先不要假設 Prometheus 壞了,依序確認:

  1. 慢請求是否真的走到計時區間?
  2. histogram 是否被 metrics endpoint 暴露?
  3. Prometheus target 是否成功 scrape?
  4. query 的 label、time range、rate window 是否包含測試事件?
  5. request count 是否足以讓選擇的 percentile 有意義?

這個順序能把「測量鏈路壞了」與「服務沒有變慢」分開——排查量測本身是否可信,本來就得沿著量測鏈路走一遍。
下一篇會把 Day14 的 SLO spec、Day15 的 availability classifier 和今天的 latency histogram 合在一起,建立不會混淆使用者旅程與機器健康度的 FastAPI SLI。

今天的內容,跟系列前面幾天怎麼接起來

到了 Day16,可以回頭把幾個看似分開的主題,串成一條線。

Day3 講故障注入:讓延遲、500、DB 不可用變成可重現的事件,而不是只能等它自然發生。

Day16 延續同一種精神,只是把「注入」的對象,從整個 request 換成「特定比例的延遲」——目的一樣,都是把抽象的風險變成可觀察、可重跑的具體現象。

Day14 講 SLO spec:先把「合格」定義清楚,才能談有沒有達標。

Day16 ④ 段那份 latency contract,用的正是同一套語言——request class、valid request、completion、exclusions,跟 Day14 的 SLO spec 骨架幾乎一模一樣,只是把 latency 當成觀察對象。

Day15 講 availability:用 good / valid 這種比例定義使用者體驗,而不是用技術指標。

Day16 講的 P95、P99,本質上也是同一種思路的延伸:不是問「系統有沒有壞」,而是問「使用者這次的等待,落在整個分布的哪個位置」。

三天的內容合起來,回答的其實是同一個更大的問題:「使用者體驗好不好」該怎麼被量化、被觀察、被驗證,而不是各自獨立的三個知識點。

這條線會在 Day17 正式收斂:把 availability(Day15)、latency percentile(Day16)、dependency 健康度,一起塞進一組完整的 SLI 定義裡。

⑫ 本文結論

回到文章一開始那個數字:九十五個請求各 100ms,五個各 10 秒,平均約 595ms。

這篇文章從頭到尾,其實都在拆解「平均約 595ms」這句話裡藏著的問題。

它藏著一個計算方法的問題:percentile 該用 nearest-rank 還是內插,樣本數夠不夠支撐一個可信的數字。

它藏著一個定義範圍的問題:量的是哪一段(server-side、TTFT、還是 end-to-end)、哪些流量該被排除。

它藏著一個組織責任的問題:各 team 局部達標,不等於使用者端到端滿意,需要有人盯著加總後的體驗。

它也藏著一個工程選擇的問題:尾端延遲不是只能忍受的天災,Google、Discord、Netflix 的案例都證明,它是可以被拆解、被投資、被系統性改善的目標。

平均值適合看總體成本,尾端才會告訴你誰被卡住。下一篇把 availability、latency 和 dependency 組成一組可用的 FastAPI SLI。

Day 17 預告

Day 16 把平均值會騙人的機制拆開來看:分位數才知道最慢那群人在等多久。Day 17 接著把 availability、latency 跟 dependency 湊成一組可用的 FastAPI SLI,重點是 SLI 要貼近使用者體驗,不是貼近基礎設施指標。

延伸閱讀


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


上一篇
Day 16(上)|平均 Latency 為什麼會騙人?請看尾端
下一篇
Day 17(上)|好的 SLI 與 FastAPI SLO:量得到不代表該量
系列文
Learning SRE for the AI Era:從 SRE Lab 到 Production AI Reliability 共 44 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言