結論先說:Golden Signals、RED、USE 是選問題的框架,不是要把所有圖表塞進一頁;選哪個要看你正在觀察的是使用者體驗、API,還是資源。本篇(上)先講清楚三套框架各自的定位、AI 時代多出來的訊號,以及怎麼把選好的框架落成一份可版控的指標契約;下篇(下)接著做 SRE Lab 實作與事故情境驗證。
Golden Signals 看 latency、traffic、errors、saturation;RED 看 request rate、errors、duration;USE 看 resource 的 utilization、saturation、errors。/ask API 可從 RED 起步,GPU queue 或資料庫 connection pool 則常需要 USE。
Day 21 已經把 metrics、logs、traces 三種訊號都接起來了,但接起來不等於好用。事故發生時,值班工程師打開 Grafana,畫面上同時有十幾張圖:API latency、CPU 使用率、GPU queue 長度、5xx 比例……每張圖都合法,但沒有人事先講好「哪張圖先看」。問題不是資料不夠,是沒有把資料按「這張圖回答哪個問題」分類過。Golden Signals、RED、USE 存在的理由,就是先把「使用者受影響了嗎」「哪個 API 變慢」「資源是不是快撐不住」這三種不同層級的問題分開,而不是把三種數字混進同一張滿版 dashboard,逼 on-call 工程師用猜的。
Day 21 建好 Prometheus、Loki、Tempo 三件套後,很容易有一種錯覺:訊號都接上了,接下來只是「畫更多圖」的問題。但把三種訊號都接進 Grafana,跟「值班時能不能在兩分鐘內找到根因」完全是兩件事。一個常見的失敗模式是這樣的:
Day 21 完成後的第一版 dashboard
┌─────────────────────────────────────────────┐
│ API p50/p95/p99 CPU% GPU% Mem% │
│ 5xx count DB conn Redis hit rate │
│ request/sec disk io network in/out │
│ container restart queue_depth pod count │
│ error log volume trace error span count │
└─────────────────────────────────────────────┘
↑
14 張圖,一次性攤開在同一頁
值班工程師:「所以我現在該看哪一個?」
這種 dashboard 不是沒有價值,它只是把「找出問題所在」的認知負擔,從系統設計者轉嫁給值班工程師——在凌晨三點、心跳還沒完全清醒、事故已經在燒 error budget 的當下,這個轉嫁成本特別高。SRE 圈子常說:「dashboard 不是用來證明你監控了很多東西,是用來讓值班工程師在最短時間內知道下一步做什麼。」Golden Signals、RED、USE 做的正是「先把問題分層、再決定哪張圖回答哪一層」的工作。
用一個假設但貼近現實的時間軸,具體感受一下「資料齊全但沒有分層」在真實值班現場會拖慢多少時間:
02:14 客服反映使用者抱怨「AI 助理回答變超慢」
02:15 值班工程師打開 Grafana,畫面有 14 張圖
02:16 先看 CPU 使用率——45%,看起來正常,跳過
02:18 看 GPU 使用率——70%,猶豫這算不算高,
Google 搜尋「GPU utilization 多少算飽和」
02:23 看 5xx count——沒有明顯增加,排除是明顯故障
02:26 看 request rate——正常,排除是流量暴增
02:30 想到要看看 queue,但發現沒有這張圖,
開始在 Prometheus 裡臨時拼 query
02:38 拼出一條 queue depth 曲線,發現持續在升高
02:40 才開始往「worker 併發數」這個方向查
02:47 找到問題:上週的一次 deployment
把併發數設定從 8 改成了 1
02:48 修正設定,問題解除
──────────────────────────
從收到回報到找到根因:34 分鐘
同一個場景,如果值班工程師手上有分層清楚的 dashboard 與決策表(第 ④ 節會給出這張表的具體樣子),流程會壓縮成:
02:14 客服反映使用者抱怨「AI 助理回答變超慢」
02:15 打開首頁:P95 TTFT 已經明顯偏離正常範圍
02:16 切到服務層 RED panel:/ask 的 5xx 沒有增加,
但 duration P99 在惡化——排除是明顯故障
02:17 切到資源層 USE panel:queue depth 持續升高,
GPU utilization 沒有明顯異常
02:18 對照 red_flag_action:「queue depth 升高,
先查 worker concurrency 設定」
02:20 確認上週 deployment 把併發數改成了 1
02:21 修正設定,問題解除
──────────────────────────
從收到回報到找到根因:7 分鐘
兩段時間軸的差距,不是因為第二個值班工程師比較厲害,也不是因為系統本身的資料變多了——兩個場景裡的底層資料完全一樣。差距純粹來自於「資料有沒有被事先分層、排好優先順序、並附上第一步該做什麼」。這正是 Golden Signals、RED、USE 三套框架,加上第 ⑩ 節談的 dashboard 分層與 red_flag_action 規格,真正省下來的時間成本。
網路上搜尋 Golden Signals vs RED vs USE,常常會得到「三選一」的印象——好像它們是互相競爭的方法論。這是常見誤解。Google SRE Book 提出 Golden Signals 時鎖定「監控一個服務最少該看哪四件事」;Tom Wilkie 提出 RED 時,明確說這是給微服務架構、request-driven 系統用的簡化版;Brendan Gregg 提出 USE 時鎖定硬體與作業系統資源層。三者作者群沒有互相對立的意思——RED 甚至可視為 Golden Signals 在 API 層的具體實作。真正該問的不是「這次事故該用 RED 還是 USE」,而是「我現在要回答的是使用者層、服務層,還是資源層的問題」。
在細看三套框架各自的內容之前,先用一張表把它們的出身講清楚,這對理解「為什麼三者不衝突」很有幫助——它們誕生的組織、要解決的問題規模、瞄準的讀者群,從一開始就不一樣:
| Golden Signals | RED | USE | |
|---|---|---|---|
| 提出者 | Google SRE 團隊 | Tom Wilkie(Weaveworks/Grafana) | Brendan Gregg |
| 出處 | Google SRE Book(2016) | Weaveworks 工程部落格(2015 前後) | 《Systems Performance》與個人部落格 |
| 要解決的規模 | 一個服務該監控什麼才算完整 | 大量微服務如何自動套用同一套 dashboard 模板 | 單一主機/單一資源如何快速定位瓶頸 |
| 瞄準的讀者 | on-call SRE、服務owner | 平台團隊、需要規模化監控的組織 | 效能工程師、資源層 troubleshooting |
| 核心假設 | 使用者體驗可被四個維度概括 | 服務是 request-driven,可套模板 | 資源診斷可用三個問題窮舉 |
這張表最重要的一格是「要解決的規模」——Golden Signals 從「一個服務」出發,RED 從「一百個微服務」出發,USE 從「一顆 CPU/一張 GPU」出發。三者出發點不同,卻剛好對應到現代系統天然存在的三層粒度(使用者體驗、服務、資源),這也是為什麼疊起來用會比只挑一個更完整——它們原本就不是為了互相取代而設計的。
出自 Google SRE Book,適合服務整體健康度:
Google SRE Book 在提出 Golden Signals 時,特別強調一個常被忽略的細節:latency 這一項不能只看「所有請求的平均延遲」,因為成功請求與失敗請求的延遲分佈往往完全不同——失敗請求可能因為快速失敗(fail-fast)而延遲極低,把它們和成功請求混在一起算平均,會讓真正拖慢使用者的延遲被平均掉。書中建議把「成功請求的延遲」和「失敗請求的延遲」分開追蹤,這個原則放到今天的 AI 系統依然成立:一個 timeout 在 30 秒後才失敗,跟一個 validation error 在 5 毫秒內就失敗,兩者的 latency 意義完全不同,不該被同一條曲線平均掉。
Tom Wilkie(Grafana、Weaveworks)提出的簡化版,適合單一服務或單一 endpoint:
RED 的設計初衷值得多說一句:Tom Wilkie 在 Weaveworks 為微服務架構寫監控工具時,發現逐一為每個服務手動設計獨立的 dashboard 太耗工,於是抽出這三個「幾乎每個 request-driven 服務都該有」的維度,讓監控可以被自動化生成——只要一個服務用標準方式暴露 request count、error count、duration histogram,就能自動套用同一組 dashboard 模板和告警規則。這也是為什麼 RED 特別適合服務數量多、team 分散的組織:它犧牲了 Golden Signals 裡的 saturation(因為那通常需要對每個服務的資源特性有客製理解),換取「一套模板可以套用到幾十個微服務」的規模化能力。
Brendan Gregg 提出,適合 CPU、記憶體、磁碟、網路這類底層資源:
USE 的起點跟 Golden Signals、RED 不太一樣——它不是從「監控一個服務」出發,而是從「系統效能診斷方法論」出發。Brendan Gregg 在自己的著作《Systems Performance》裡把 USE 定義為一套「checklist 方法」:對每一個硬體資源,依序問這三個問題,通常幾分鐘內就能找出瓶頸所在,而不用等到「靈光一閃猜到答案」。這也解釋了為什麼 USE 特別強調 Utilization 和 Saturation 要分開看——Gregg 在文章裡舉過一個經典例子:CPU utilization 100% 可能代表「CPU 忙著做有用的工作」,也可能代表「CPU 忙著自旋鎖等待,實際上什麼有用的事都沒做成」,只有把 saturation(run queue length)一起看,才能分辨這兩種完全不同的狀況。
Golden Signals 回答「使用者現在好不好」,RED 回答「哪個 API 出問題」,USE 回答「哪個資源撐不住」。把這三層畫在同一張圖,等於同時問三個問題,答案通常會互相干擾。
用一個簡化的架構圖表示這個分層關係:
┌───────────────────────────────────────────────┐
│ 使用者層 — Golden Signals │
│ 「使用者現在好不好?」 │
│ latency / traffic / errors / saturation │
└──────────────────────┬──────────────────────────┘
│ 往下拆解:是哪個 API
┌──────────────────────▼──────────────────────────┐
│ 服務層 — RED │
│ 「哪個 API/哪個 endpoint 出問題?」 │
│ rate / errors / duration │
└──────────────────────┬──────────────────────────┘
│ 往下拆解:是哪個資源
┌──────────────────────▼──────────────────────────┐
│ 資源層 — USE │
│ 「哪個資源撐不住?」 │
│ utilization / saturation / errors │
└───────────────────────────────────────────────┘
這張圖的箭頭方向很重要:正常的除錯流程是由上往下(使用者受影響 → 找出哪個服務 → 找出哪個資源),而不是反過來從資源指標開始猜使用者受不受影響。看到 GPU utilization 飆到 95%,第一反應不該是「使用者一定受影響了」,而該先往上確認 Golden Signals 是否真的變差;如果使用者體感正常,這張 GPU 圖此刻只是「資源被有效利用」的證據,不是故障訊號。
三套框架共用幾個相似的詞彙,這是它們最容易被混著用錯的地方。最典型的一組是「Errors」——RED 和 USE 都有 Errors 這個維度,但兩者量的完全不是同一件事:
RED 的 Errors
是使用者能感受到的失敗
+
「這次請求,使用者拿到的是錯誤還是正確的回應?」
(分子分母都以「請求」為單位)
USE 的 Errors
是資源本身回報的錯誤事件
+
「這個資源,在提供服務的過程中出了什麼差錯?」
(分子分母都以「資源事件」為單位,不對應到單一使用者請求)
一個具體的例子最能說明這個差異:GPU 發生一次 ECC 記憶體錯誤並自動修正,這是一個 USE 的 Errors 事件(資源層出了差錯),但如果這次修正沒有造成任何請求失敗,RED 的 Errors 完全不會動;反過來,一次因為 prompt 格式錯誤造成的 validation_failed,是一個明確的 RED Errors 事件(使用者沒拿到能用的答案),但這次失敗完全不涉及任何底層資源異常,USE 的 Errors 也不會動。把這兩種 Errors 疊在同一條曲線上看,會讓「使用者受影響的失敗」和「資源健康度事件」互相稀釋,變成一條誰都解讀不出意義的合成指標。
一個很容易犯的錯,是把 Golden Signals、RED、USE 理解成「三個互不相干的監控主題」,各自建一個 Grafana folder,彼此沒有連結。這樣做的問題是:使用者層看到 latency 升高時,沒有辦法一鍵跳到「是哪個 API」;服務層看到某個 endpoint duration 變差時,沒有辦法一鍵跳到「是哪個資源」。三套框架真正該被實作成的樣子,是同一份 root cause 分析路徑上的三個檢查點,而不是三本互不引用的參考書。這也是第 ⑩ 段要談的「dashboard 分層」設計的核心動機。
RED 用在 /ask 這類 API 沒問題,但 AI workflow 的耗時由 retrieval、model call、tool call 等多個步驟組成。只看整體 duration,無法知道變慢的是 retriever、model 還是 tool;把 RED 拆到各個子步驟,才能定位瓶頸。
舉個具體數字:假設一次 /ask 的 end-to-end duration 是 3.2 秒,只看這一個數字,工程師大概只能猜「模型太慢」,動手加 GPU 或換更快的 model。但把同一次請求拆成子步驟,可能長這樣:
retrieval 0.4s
queue_wait 1.8s ← 真正的大頭
model 0.9s
tool 0.1s
------------------
total 3.2s
真正吃掉時間的是 queue_wait,也就是請求排隊等 worker 的時間,跟 model 本身算得快不快無關。如果只盯著總 duration 這一條 RED 曲線,這 1.8 秒會被誤算進「model 變慢」,接下來的動作(換模型、升級硬體)全部打歪;只有把 RED 拆到子步驟層級,才能讓「哪一段」變成可回答的問題,而不是靠猜。
傳統 web API 的 RED 夠用,是因為一次請求通常只經過「應用程式碼 → 資料庫」這種相對淺的呼叫鏈,duration 變異來源有限。AI workflow 不是這個形狀。一次 /ask 呼叫可能包含:
一次 /ask 請求可能經過的階段
┌──────────────┐
│ 1. 輸入驗證 │ duration 通常穩定、毫秒級
└──────┬───────┘
▼
┌──────────────┐
│ 2. Retrieval │ duration 依向量資料庫負載、index 大小浮動
└──────┬───────┘
▼
┌──────────────┐
│ 3. Queue wait │ duration 依 worker 併發數、當下流量劇烈浮動
└──────┬───────┘
▼
┌──────────────┐
│ 4. Model call │ duration 依 prompt 長度、model route、TTFT/生成 token 數浮動
└──────┬───────┘
▼
┌──────────────┐
│ 5. Tool call │ duration 依外部 API、可能重試、可能逾時
└──────┬───────┘
▼
┌──────────────┐
│ 6. Validator │ duration 通常穩定,但失敗會觸發重試整段流程
└──────────────┘
每一段的 duration 分佈型態都不一樣:輸入驗證接近常數,retrieval 隨資料庫負載波動,queue wait 在尖峰時可能指數成長,model call 隨 prompt 與輸出長度線性增加,tool call 有網路不確定性。把這六段疊在一起只看一條 end-to-end duration 曲線,等於用一個數字描述六種不同隨機變數之和——統計上可以做(卷積),但診斷上毫無意義:P95 變差時,你不知道是哪一段的分佈右移了。
這正是為什麼 Day 20(Tempo 分散式追蹤)建立的 span 邊界,在這裡變成必要條件:沒有 trace 只能拿到一個總數;有了 trace,才能對每個 span 分別套用 duration 維度,變成「六個 mini-RED」而不是一個籠統的 RED。
USE 這層則直接延伸到 GPU:GPU utilization、GPU memory saturation、inference queue 長度,都是 USE 方法可以直接套用的資源指標,只是資源從 CPU/磁碟換成 GPU/VRAM/推論佇列。
把 CPU 時代的 USE 對照表換成 GPU 版本,會長這樣:
CPU(傳統 USE) GPU(AI Era USE)
Utilization CPU busy % (mpstat) GPU compute utilization(SM busy %)
Saturation run queue length inference queue depth、batch 等待、VRAM allocation pressure
Errors machine check exception OOM、driver reset、Xid error、ECC error、worker crash
這張對照表看起來簡單,但魔鬼藏在細節裡。GPU utilization 在不同 exporter、不同推論框架下,代表的意義都不完全相同:有些指標量的是「SM 在取樣時刻是否有任何 kernel 在執行」,這種定義下即使 GPU 大部分時間只做低效運算,utilization 仍可能顯示接近 100%——這跟 CPU 領域「utilization 高但花在自旋鎖」是同一種結構性陷阱,只是換了硬體。這也是為什麼第一步永遠是先看 exporter 本身暴露的原始指標,而不是假設一個熟悉的名稱就直接拿來用:
# 先看你的 GPU exporter 實際暴露了什麼,
# 而不是假設它跟別人的教學文章用一樣的指標名稱
curl -s http://gpu-exporter:9400/metrics | grep -i "gpu\|dcgm" | head -20
# 常見會出現、但定義因 exporter 版本而異的幾類指標:
# DCGM_FI_DEV_GPU_UTIL — SM 忙碌比例(取樣式,非精確工作量)
# DCGM_FI_DEV_FB_USED — VRAM 已用量(saturation 訊號的一部分)
# DCGM_FI_DEV_FB_FREE — VRAM 剩餘量
# DCGM_FI_DEV_XID_ERRORS — driver/硬體層錯誤事件(USE 的 Errors)
即使是這裡列出的例子,也只是 NVIDIA DCGM exporter 這一種選擇下常見的欄位名稱,換一套推論框架可能用完全不同的命名慣例。把這串指令養成習慣,比背下任何一組「看起來通用」的 GPU metric 名稱都可靠。
除了把既有框架套進新元件,AI workflow 還多出一批傳統三套框架都沒涵蓋的訊號:queue delay(請求排隊等 GPU 資源的時間)、TTFT(time to first token,使用者等到第一個字出現的時間)、tool failure(工具呼叫失敗但整體請求仍回傳 200)、evaluation outcome(Day 08 建立的 quality/safety 判定結果)。它們把 Golden Signals 的「使用者感受」延伸到 AI workflow 特有的等待與失敗模式;最後仍要回答「這件事有沒有影響使用者拿到正確結果」。
值得展開的是這四個訊號各自屬於哪一層、又為什麼傳統框架接不住它們:
Queue delay 介於 RED 和 USE 之間——它是一個 per-request 的等待時間(符合 RED 的 duration 定義),但它的根因幾乎永遠在資源層(worker 併發數不夠、GPU 被佔滿)。傳統 web 服務通常沒有這麼明顯的排隊現象,因為大多數 API 呼叫是無狀態、可以水平擴展到近乎瞬間完成;但 GPU 推論是一種「昂貴、有限並行度」的資源,請求排隊是常態而非例外,需要被明確地量測出來,而不是被吞進 end-to-end duration 裡。
TTFT 是串流輸出時代特有的訊號。傳統 RED 的 duration 假設「請求完成」是一個單一時刻的事件;但串流回應把「完成」拆成了「開始輸出」和「全部輸出完畢」兩個不同時刻,使用者對這兩個時刻的感受天差地遠。這一點在下篇(下)會用具體的使用者心理研究數字展開。
Tool failure 挑戰的是 RED 裡「Errors」的定義。傳統服務的 error 幾乎等同於「HTTP 5xx 或例外」,但 AI workflow 常見的模式是:某個工具呼叫失敗了,系統選擇降級(回傳一個較弱的答案、或跳過這個工具),最終仍以 HTTP 200 回應使用者。如果 RED 的 Errors 維度只數 5xx,這類降級會完全消失在監控裡——這正是 Day 07 討論過的 technical success 與 task success 的分裂,在 RED 這個框架裡的具體投影。
Evaluation outcome 則完全超出 RED/USE 的設計範疇。RED 和 USE 兩者都是為了回答「系統有沒有正常運作」而設計的,但 AI workflow 額外多了一個問題:「系統正常運作,答案卻是錯的」。這需要獨立於 RED/USE 之外的第三層訊號,下篇(下)會用兩個真實案例(Anthropic、silent failure 研究)具體說明為什麼這一層不能被省略。
框架很容易被背成縮寫,然後被用成儀表板裝飾。真正有用的作法反過來:先寫事故現場想回答的問題,再決定哪個框架和哪個訊號負責回答。
下面這張表刻意不追求「所有指標都列到」。它只保留值班時可以改變下一步動作的欄位。
| 事故現場的問題 | 第一個看哪個框架 | 第一張圖 | 正常卻仍可能有問題的地方 | 下一步 |
|---|---|---|---|---|
| 使用者現在是否明顯受影響? | Golden Signals | successful task ratio、P95 end-to-end latency | HTTP 成功率高,回答仍可能錯 | 看 evaluation outcome 與抽樣 trace |
/ask 是否正在變慢或失敗? |
RED | rate、5xx/timeout ratio、P95 duration | 平均延遲正常,P99 已經壞掉 | 依 endpoint、model route、deployment 切分 |
| 為何只有部分請求很慢? | RED + trace | retrieval、model、tool 的 duration | 服務總耗時看不出子步驟 | 從慢 trace 找共同 span |
| GPU worker 還能接多少流量? | USE | utilization、queue depth、queue wait | GPU utilization 低也可能卡在 CPU 或 mutex | 看 worker concurrency、CPU、排隊時間 |
| 資料庫連線是否耗盡? | USE | pool in-use、waiters、connection errors | CPU 很低,連線池仍可能全滿 | 查 pool 上限與慢查詢 |
| 某次 rollout 是否傷到品質? | Golden Signals + evaluation | versioned task success、burn rate | 技術錯誤沒有增加 | 對照 prompt/model/retrieval version |
這張表也揭露一件不太討喜的事:一個框架不會自動給答案。GPU utilization=92% 本身不是故障;如果 queue wait 沒增加、TTFT 還在目標內,它可能只是資源被有效使用。反過來說,GPU utilization=35% 也不保證健康,worker 有可能被單一工具呼叫或 connection pool 卡住。
因此數字必須和「使用者結果」相連。每張 operational panel 都要能往上連到 Golden Signals;每個 user-impact panel 都要能往下連到 RED、USE 和 trace。斷掉任何一段,dashboard 都只是在陳列數字。
這張表容易被誤用成「查表工具」——事故發生時對照表格找對應的圖。但真正該被使用的方式是反過來:先讓值班工程師(或 on-call runbook)寫出「現在觀察到的症狀是什麼」,再讓症狀決定往下查哪一層。這跟很多團隊「先打開所有 dashboard 一張一張看過去」的習慣相反——資訊量越大,人在高壓情境下決策反而越慢(選擇越多決策時間越長,是認知心理學已反覆驗證的現象)。決策表的價值正是事先把「症狀 → 該看哪張圖」想清楚,值班時不需要臨場重新推理。
仔細看這張表的第四欄,會發現它其實在講同一件事的六種變體:任何單一指標「看起來正常」都不足以下結論,因為每個指標都只回答了它被設計要回答的那一個問題,而不是全部的問題。這句話值得拆開來看每一列:
這六個例子有一個共同的結構:每一個「正常」的指標背後,都藏著一個沒有被這個指標涵蓋的維度。這正是「多套框架搭配使用」存在的意義——不是因為某一套框架設計得不好,而是因為任何單一指標,本質上都只能回答一個切面的問題。
假設客服 RAG 服務在 10:05 開始收到「回答出現得很慢」的回報。此時不需要先猜是模型、GPU 或網路。
10:05 使用者感受:串流第一個 token 出現變慢
10:06 Golden Signals:P95 TTFT 上升;task success 暫時未知
10:07 RED:/ask rate 穩定,5xx 沒升,duration P99 上升
10:08 USE:GPU utilization 70%,inference queue depth 從 2 升到 38
10:09 Trace:model span 前面多了 queue_wait span;retrieval 正常
這不是「RED 沒用」。RED 正確地說出 API 很慢但沒有大量錯誤;USE 說明排隊正在形成;trace 指向哪一段在等。三者合起來,才足以支持「先限制新流量、增加 worker 或調整 admission control」這類動作。
若只看 5xx,這起事故會被判成一切正常。若只看 GPU utilization,工程師可能錯誤地加 GPU;其實 queue 的根因可能是 worker concurrency 被設成 1。若只看單一 trace,又會不知道這是偶發請求還是所有使用者都被影響。
把這五分鐘拆開看,每一分鐘都在排除一個錯誤假設,而不是單純疊加資料:
五分鐘、四層訊號合起來才拼出完整根因鏈:使用者體感變慢 → API 尾端延遲惡化但技術沒壞 → inference queue 正在累積 → 排隊發生在進 model 之前。任何一層單獨拿出來都撐不住「調整 worker 併發數」這個修復動作——這正是本文開頭那句話在一次具體事故裡的完整展開。
在建 dashboard 前,先寫 metric contract。這一步很像 Day 23 要寫的 trace schema:名稱、單位、label、分母與可回答的問題都要先講清楚。Grafana 可以換,這份契約不該跟著畫面一起消失。
以下是可用於 Lab 的最小 contract。指標名稱只是範例;重點在每個欄位的邊界。
| 指標 | 類型 | 單位 | 低基數 labels | 回答的問題 |
|---|---|---|---|---|
ai_requests_total |
Counter | requests | route、outcome、model_route |
RED rate 與結果比例 |
ai_request_duration_seconds |
Histogram | seconds | route、outcome |
API duration 與 tail latency |
ai_ttft_seconds |
Histogram | seconds | route、model_route |
使用者等第一個 token 多久 |
ai_queue_wait_seconds |
Histogram | seconds | queue、model_route |
請求在 worker 前等多久 |
ai_workflow_steps_total |
Counter | steps | step、outcome |
retrieval/tool/parser 哪步失敗 |
ai_evaluations_total |
Counter | evaluations | result、evaluator_version |
quality/safety 的樣本結果 |
ai_inference_queue_depth |
Gauge | requests | queue |
當下排隊壓力 |
不要把 request_id、user_id、完整 prompt、文件內容、完整錯誤訊息放進 Prometheus label。它們的基數與敏感度都不適合 time series;Day 21 已經把這些細節留給 structured log 和 trace。可接受的 label 應該是一組有限、預先定義的值,例如 outcome="ok" | "upstream_timeout" | "validation_failed"。
model_route 也要有限集合,例如 primary、fallback、local。不要直接以動態 model ID、tenant ID 或 prompt version 當 label。版本比較可放在 deployment annotation、log/trace attribute,或在有明確保留策略的 evaluation 資料集中處理。
「先寫契約,再建 dashboard」聽起來像流程官僚,但解決的是實際痛點:dashboard 是視覺產物,重建成本低;指標契約是資料產物,一旦開始累積歷史資料,改名稱、改 label 結構、改 bucket 邊界,都會讓舊資料和新資料變得不可比較,甚至歷史查詢直接壞掉——這跟資料庫 schema migration 是同一類問題,UI 可以隨時重畫,但底層 schema 一旦定型,改動成本會隨資料量指數成長。
指標契約要回答的問題比表面上看到的多。以 ai_request_duration_seconds 這一列為例,一份完整的契約至少要講清楚:
指標名稱: ai_request_duration_seconds
類型: Histogram
單位: seconds(Prometheus 官方慣例——base unit,不要用 milliseconds)
Label: route, outcome
Label 基數上限: route 固定集合(目前僅 /ask);outcome 固定 7 種取值
Bucket 邊界: 0.1, 0.3, 0.5, 1, 2, 5, 10, 30(依 SLO 目標校準,不是隨手選的等比數列)
分母定義: 每次 /ask 請求完成(成功或失敗)都會被 observe 一次;被 client 中斷連線但 server 端仍在處理的請求,仍會在 server 端完成時被記錄
不會做的事: 不記錄逐一 request 的原始耗時(那是 trace 的責任);不記錄 retry 前的耗時(重試視為新的一次 observe,避免重複計數扭曲分佈)
擁有者: platform-observability team
這份契約裡「不會做的事」跟「retry 視為新的一次 observe」這兩句特別容易被忽略,但正是造成 dashboard 數字「看起來合理但實際上錯」最常見的來源之一——下篇(下)會再談這個「retry 被重複計數」的陷阱。
「先寫契約」這句話如果沒有落成具體檔案,很容易變成一句沒人真的做的口號。實務上一個可行的做法,是把整份 metric contract 寫成一份 YAML,跟程式碼一起進版控,任何新增或修改指標都要走一次 PR review,而不是工程師覺得需要就自己加一個新 label:
# observability/metric-contract.yaml
metrics:
- name: ai_requests_total
type: counter
unit: requests
labels:
route:
cardinality: fixed
values: ["/ask"]
outcome:
cardinality: fixed
values:
- ok
- technical_error
- upstream_timeout
- tool_failed
- validation_failed
- quality_failed
- safety_refused
model_route:
cardinality: fixed
values: ["primary", "fallback", "local"]
answers: "RED rate 與結果比例"
owner: platform-observability
- name: ai_request_duration_seconds
type: histogram
unit: seconds
buckets: [0.1, 0.3, 0.5, 1, 2, 5, 10, 30]
labels:
route: { cardinality: fixed, values: ["/ask"] }
outcome: { cardinality: fixed, values: ["<同上七種>"] }
answers: "API duration 與 tail latency"
owner: platform-observability
- name: ai_inference_queue_depth
type: gauge
unit: requests
labels:
queue: { cardinality: fixed, values: ["inference"] }
answers: "當下排隊壓力"
caveat: "僅代表單一 process 內計數,多 worker 時不可直接相加宣稱全域 depth"
owner: platform-infra
把契約寫成這種結構化檔案帶來兩個好處:cardinality: fixed 加明確的 values 列表,可以搭配 CI 檢查腳本在 PR 階段就擋下不合規的 label 值,把規則從「code review 靠人記得」變成自動化;owner 欄位則直接呼應第 ⑩ 節「每個 panel 都應有 owner」——指標一誕生就標記負責團隊,之後追問「這個指標為什麼長這樣」不用考古。
反過來說,沒有契約檔案的常見退化路徑是:第一個工程師加了 route、outcome;三個月後另一個工程師為排查特定客戶問題臨時加了 customer_id;六個月後又加了 prompt_version。每次新增當下都顯得合理,但沒有契約強制 review,cardinality 就這樣一步步失控——本系列另一篇文章記錄過真實案例:開發者為追蹤使用者流量加上 user_id label,三個月內在百萬活躍使用者規模下產生超過 500 萬個時間序列,讓 Prometheus OOM killed、Grafana 全部空白,恢復動用了戰情室與程式碼回滾。
AI 系統常見的錯誤,是把 HTTP 200 全都算進成功分子。這會讓技術成功蓋住 workflow 失敗和語意失敗。
在這個系列裡,可以先用下列 outcome 契約:
ok 技術流程完成,且已通過必要 validator
technical_error API 或內部程式無法完成流程
upstream_timeout 模型或工具依賴逾時
tool_failed 工具失敗,服務回覆降級結果或錯誤
validation_failed 回應格式、引用或規則檢查未通過
quality_failed evaluator 判定不符合任務要求
safety_refused 系統依政策拒答;是否算 good outcome 取決於任務
這不是通用分類法。真正的產品要和產品、風險與客服團隊定義「拒答在什麼任務中屬於正確結果」。例如使用者要求危險操作時,safety_refused 應該算 policy success;使用者詢問正常的公司制度卻被錯誤拒答,則不能把它包裝成成功。
上面這七種 outcome,寫在 code 裡看起來乾淨俐落,但每一種在實際運作時都有一個「誰決定、什麼時候決定」的問題。technical_error 與 upstream_timeout 最容易正確標記,幾乎完全由程式邏輯決定,可以寫單元測試驗證(下篇(下)會提到)。tool_failed 和 validation_failed 需要工程團隊明確定義「什麼叫工具失敗但仍要降級回應」,決策依據該來自產品對使用者體驗的容忍度,不是工程師覺得方便就好。quality_failed 和 safety_refused 最麻煩,依賴 Day 08 建立的 evaluator,判準會隨版本、prompt、抽樣策略改變——這也是為什麼 ai_evaluations_total 要帶 evaluator_version label,否則升級前後的 quality failure ratio 根本不可比較,卻會被誤讀成品質變好或變差。
一個實用的做法是把這七種 outcome 按照「誰能標記、標記時間點」畫成一張責任表:
outcome 誰標記 標記時間點
ok 程式邏輯 請求完成當下
technical_error 程式邏輯(例外類別) 請求完成當下
upstream_timeout 程式邏輯(timeout) 請求完成當下
tool_failed 程式邏輯 + 工具回傳碼 請求完成當下
validation_failed validator 規則 請求完成當下
quality_failed evaluator(抽樣) 請求完成後的非同步評估
safety_refused policy engine 請求完成當下,但「算不算 good」需要人工覆核政策
前五種都是「請求完成當下就能決定」的同步 outcome;後兩種要嘛需要非同步評估(quality_failed 常常要等 evaluator 跑完才知道),要嘛需要政策判斷(safety_refused 是否算 good outcome,本質上不是工程問題)。把這張責任表寫清楚,可以避免一個常見的組織性錯誤:工程團隊自己決定「safety_refused 一律算失敗」或「一律算成功」,卻沒有讓風控、法務或產品參與這個判斷。
下篇(下)會提到「outcome 映射不一致」是實務上最容易讓 dashboard 產生合理但錯誤數字的原因之一,這裡先把對應的修法寫出來,因為它跟這份 outcome 契約是同一件事的兩面:契約定義了「應該有哪七種 outcome、各自的判準是什麼」,測試則負責確保「程式碼實際產生的 outcome,真的符合契約寫的規則」。一份最小可用的測試,長得像這樣:
import pytest
from app.outcome import map_exception_to_outcome
@pytest.mark.parametrize(
"exc, expected_outcome",
[
(TimeoutError("upstream took too long"), "upstream_timeout"),
(ToolInvocationError("search tool returned 500"), "tool_failed"),
(ResponseValidationError("missing citation"), "validation_failed"),
(RuntimeError("unexpected null pointer"), "technical_error"),
],
)
def test_exception_maps_to_expected_outcome(exc, expected_outcome):
assert map_exception_to_outcome(exc) == expected_outcome
def test_every_outcome_value_is_in_the_contract():
# 防止有人在程式碼裡手打一個契約表格外的字串,
# 例如打錯成 "upstream_time_out"(底線位置錯誤)
# 這種錯字不會讓程式壞掉,但會在 Prometheus 裡
# 悄悄多出一個新的 label 組合,永遠不會被
# 既有的 PromQL query(例如 outcome=~"...|upstream_timeout|...")匹配到。
from app.outcome import KNOWN_OUTCOMES
assert KNOWN_OUTCOMES == {
"ok",
"technical_error",
"upstream_timeout",
"tool_failed",
"validation_failed",
"quality_failed",
"safety_refused",
}
第二個測試函式在防的是:如果某處手打了 "upstream_time_out"(底線位置打錯),Python 不會報錯,Prometheus 也會乖乖收下這個新 label 值——但這個錯字永遠不會被下篇那條 PromQL 正則式匹配到,於是這類失敗會從 error ratio 的分子裡消失,錯誤率顯示得比真實情況低,而且低得毫無徵兆。用固定的 KNOWN_OUTCOMES 集合把契約寫進程式碼、並用測試鎖住它,能把這種靜默偏移在 CI 階段就攔下來,而不是等值班工程師發現「error ratio 跟客訴量對不上」才開始懷疑指標本身有問題。
下集預告:下篇(下)會把今天定義的指標契約接上實際的 FastAPI 實作與 Prometheus panel,並用兩個可重跑的事故情境驗證框架選得對不對,最後談框架的限制與 dashboard 分層原則。
這篇是 Learning SRE for the AI Era 系列的一部分。
我會從 SRE 的服務可靠性基礎開始,逐步探索當系統加入 LLM、RAG、Agent 與 GPU Infrastructure 後,如何讓 AI 系統不只可用,也能被觀測、評估、控制成本並安全演進。
Build → Trace → Break → Measure → Evaluate → Recover → Improve.