iT邦幫忙

2026 iThome 鐵人賽

DAY 3
0
AI Engineering

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

Day 14(下)|SLI、SLO、SLA:先量承諾,再談百分比

  • 分享至 

  • xImage
  •  

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

結論先說:分子分母定義得再漂亮,沒量出來、沒告警規則接住,也只是一份文件;今天把上篇的規格接上 counter、window、error budget 與 burn-rate alert,變成值班工程師真正會看見的訊號。

上篇(Day 14 上)拆開 SLI、SLO、SLA 的責任邊界,並為 /ask 定義了 event contract:哪些欄位進 metrics、哪些進 logs,以及一份可測試的 good/bad classifier。下篇接著把這份規格落地成可查詢、可告警的東西。

⑥ 從 completion event 匯總成 Prometheus counter

在 Day 2 的 Lab 中,應讓應用程式只在 request 已經有最後結果時增加 counter。

概念上,輸出的 metric 可以長成這樣:

ask_sli_events_total{
  route="/ask",
  sli_definition_version="v1",
  sli_eligible="true",
  sli_result="good"
} 482

失敗事件則使用同一個 metric family 的有限 label 值:

ask_sli_events_total{
  route="/ask",
  sli_definition_version="v1",
  sli_eligible="true",
  sli_result="bad"
} 6

使用同一個 counter 的好處,是分子與分母來自同一批 completion event。

Google SRE 對 SLI 的核心提醒也相同:先定義使用者可見的好與壞,再把它可靠地量測。不要把 dashboard 中看似相關的兩個 metric 臨時相除,期待它們剛好涵蓋相同流量。Google SRE:Service Level Objectives

在 Prometheus 中,counter 需要用 rate() 或 increase() 看一段時間的增量;直接相減會在 process restart 後得到誤導結果。官方文件也以 rate(counter[5m]) 示範這個基本模式。Prometheus:Understanding metric types

原因值得展開講清楚,而不是背一句「不要直接相減」。Prometheus 的 counter 只會單調遞增,process 重啟時會歸零重新累加——這是設計上刻意的行為,讓每次抓取到的原始數字本身沒有直接意義,只有「跨時間的差值」才有意義。假設 ask_sli_events_total{sli_result="good"} 在某次抓取時是 1000,deployment 重啟後歸零,五分鐘後又累積到 40。若直接拿最新值減最舊值(40 - 1000 = -960),會得到一個負數,完全誤導判斷;increase() 與 rate() 內建了偵測 counter reset 的邏輯,遇到數值往回掉就視為一次重置,改用「重置後的累積量」接續計算,因此在 deploy 頻繁的服務上,這不是選配的最佳實務,而是避免每次上版就讓 SLI 曲線出現離譜負值或尖峰的必要條件。increase() 回傳的是視窗內的估計總增量(rate() × 視窗秒數),適合直接拿來當分子分母相除;rate() 回傳的是每秒平均速率,適合畫成隨時間變化的曲線。兩者用同一份底層邏輯,差別只在於你要「一段時間的總量」還是「當下的速度」。

「估計」兩個字不是隨口說的

上一段特別把 increase() 的結果稱為「估計總增量」,這不是用詞隨便,而是 Prometheus 的實際行為:increase() 底層先算 rate() 再乘上視窗長度換算回總量,但 scrape 是間隔性的(常見每 15 或 30 秒一次),視窗邊界很少剛好落在抓取點上,因此結果是線性內插外推,不是精確加總。

這個機制多數情況誤差極小;但視窗長度接近或小於抓取間隔(例如對 5 秒視窗做 increase())、或counter 剛好在視窗邊界附近重啟時,估計值可能明顯偏離實際事件數——這也是為什麼 burn-rate alert 通常選 5 分鐘以上的短視窗。日常 dashboard 可忽略這些誤差;但 incident review 若需要精確到個位數的分子分母,最終仍要回頭核對 completion log,而不是只信任 PromQL 算出的浮點數——這正是第 ③ 段「Prometheus 給趨勢、logs 給鑑識」的原因之一。

若你的 metric 已經如上輸出,近一小時 availability 的概念查詢可以是:

sum(
  increase(ask_sli_events_total{
    route="/ask",
    sli_definition_version="v1",
    sli_eligible="true",
    sli_result="good"
  }[1h])
)
/
sum(
  increase(ask_sli_events_total{
    route="/ask",
    sli_definition_version="v1",
    sli_eligible="true"
  }[1h])
)

這條 query 有一個重要限制。

當分母是零時,它沒有可解釋的 availability。

午夜沒有 /ask 流量,不代表「100% 成功」,也不代表「0% 成功」。它只代表這個 window 沒有 request-based 樣本。

把 zero denominator 顯示為 N/A,並另外監控是否長期完全沒有流量,通常比把它補成 1 更誠實。

若只想做近期 dashboard,rate() 可讓結果隨時間連續:

sum(
  rate(ask_sli_events_total{
    route="/ask",
    sli_definition_version="v1",
    sli_eligible="true",
    sli_result="good"
  }[5m])
)
/
sum(
  rate(ask_sli_events_total{
    route="/ask",
    sli_definition_version="v1",
    sli_eligible="true"
  }[5m])
)

短 window 適合發現剛開始的問題,卻不應拿來宣告 28 天 SLO 是否達標。

這正是 SLO window 存在的原因。

多個 replica 各自累加,查詢時才加總

Day 2 的 Lab 用 uvicorn 起單一個 API process,但真正的生產環境幾乎不會只有一個 replica。這裡有一個容易讓新手困惑的地方:ask_sli_events_total 這個 counter 是每個 process 各自獨立累加的,process A 處理了 300 筆請求,它本地的 counter 就是 300;process B 處理了 180 筆,它的 counter 是 180。Prometheus 抓取時,會把每個 replica 各自的數值當成獨立的 time series 存起來,並不會在寫入階段就先加總成一個全域數字。

這也是為什麼前面所有 PromQL 範例都在最外層包了一個 sum(...)——它負責把所有 replica 各自累積的 increase() 結果加總成一個代表整個服務的視角。如果漏了這層 sum(),查詢結果會變成「每個 replica 各自的 availability」,而不是「整個服務的 availability」;在 Grafana 上通常會看到同一條指標線分裂成好幾條、各自代表一個 pod,而不是預期中的一條彙總曲線。更糟的情況是,如果團隊誤以為某個 replica 的曲線就代表整體,可能會在某個 replica 剛好流量特別低、數字特別好看時得出「系統很健康」的錯誤結論,卻沒注意到另一個 replica 正在大量出錯。

# 錯誤示範:少了 sum(),得到每個 pod 各自的 availability,不是整體
increase(ask_sli_events_total{sli_result="good"}[1h])
/
increase(ask_sli_events_total{sli_eligible="true"}[1h])

# 正確:先分別加總分子、分母,兩個「服務層級的總數」再相除
sum(increase(ask_sli_events_total{sli_result="good"}[1h]))
/
sum(increase(ask_sli_events_total{sli_eligible="true"}[1h]))

這個細節值得提早在 Day 14 就講清楚,因為 Day 2 建立的 Lab 目前是單一 replica,這個問題暫時不會出現;但一旦服務規模成長到需要水平擴展(例如接下來系列會談到的容量規劃),少了 sum() 的舊 dashboard 會突然開始顯示一堆分裂的曲線,而排查這個問題所花的時間,往往比一開始就寫對查詢語法要長得多。

同一條查詢被抄十次,不如把它定義一次:Recording Rule

前面幾段出現的 PromQL——sum(increase(...)) / sum(increase(...))——一旦要同時用在 Grafana dashboard、burn-rate alert 規則、以及事後 incident review 的查詢裡,很容易演變成同一段查詢邏輯被複製貼上三、四次,各自維護。這不只是重複勞動的問題,而是一個更隱蔽的風險:如果哪天有人要調整 label matcher(例如把 sli_definition_version="v1" 改成 "v2"),複製出去的三、四份查詢很可能不會同步更新,變成「dashboard 顯示 v1 的比例,alert 卻還在用 v2 的定義判斷要不要 page」——這正是第 ⑤ 段「資料從哪裡來」那一題要求「同一件事不該在兩張 dashboard 有兩個答案」的具體翻版,只是這次錯誤發生在查詢定義本身,而不是資料來源。

Prometheus 的 recording rule 正是為了解決這個問題而存在:它讓你把一段常用的查詢預先計算、存成一個新的 time series,之後所有地方都只需要引用這個算好的結果,而不是各自重新展開一次完整運算式。概念上,一份 recording_rules.yml 大致長這樣:

groups:
  - name: ask_availability_sli
    interval: 30s
    rules:
      - record: ask:sli_events:good_1h
        expr: |
          sum(increase(ask_sli_events_total{
            route="/ask",
            sli_definition_version="v1",
            sli_eligible="true",
            sli_result="good"
          }[1h]))

      - record: ask:sli_events:eligible_1h
        expr: |
          sum(increase(ask_sli_events_total{
            route="/ask",
            sli_definition_version="v1",
            sli_eligible="true"
          }[1h]))

      - record: ask:availability_ratio:1h
        expr: |
          ask:sli_events:good_1h
          /
          ask:sli_events:eligible_1h

命名採用 Prometheus 官方建議的 level:metric:operations 慣例,讓任何人看到 metric 名稱就能猜到它在算什麼、用了多長視窗。dashboard、burn-rate alert、事後 review 三個地方都引用同一個 ask:availability_ratio:1h,而不是各自重新展開 sum(increase(...))。真正要調整 SLI 定義時,只需改這一份 rule 檔案,所有下游引用會自動跟著更新,不會有「有些地方改了、有些地方忘記改」的風險。

這個好處在 Day 14 討論的另一個場景下更明顯:第 ⑦ 段之後會介紹 multiwindow, multi-burn-rate alerting,同時需要 1 小時、5 分鐘、6 小時、30 分鐘、3 天、6 小時六種不同視窗長度的比例。如果每一條 alert rule 都各自重新展開完整的 sum(increase(...)) 運算式,alert 規則檔案會迅速膨脹成一堆幾乎一模一樣、只差視窗長度的重複區塊,且每個視窗都要對 Prometheus 重新計算一次原始 time series 的 increase(),在事件量大的服務上會疊加成不小的查詢負擔。用 recording rule 把每個視窗的分子分母各自算好、存成獨立 metric,alert 規則本身就只剩下一行简单的比較判斷式,可讀性與效能都比展開版本好上一截。

需要提醒的是,recording rule 本身也遵守與其他 metric 一樣的 label 紀律——它算出來的是聚合後的比例,不會、也不應該把 request_id 這類高基數欄位帶進計算結果裡。recording rule 解決的是「同一段運算式重複貼上」的維護問題,不是「該把什麼放進 label」的機制問題;兩者是本文③段與這裡各自要處理的不同層次議題,不要混為一談。

recording rule 本身也是程式碼的一種形式,一樣可以在合併前用 promtool test rules 跑單元測試,而不是部署到 Prometheus 之後才發現算錯:用一組人工構造的固定速率輸入(例如每分鐘固定新增 10 筆 good、1 筆 bad),斷言 60 分鐘後 ask:sli_events:good_1h 恰好等於 600。這不是在測 Prometheus 自己的 increase() 實作——那是 Prometheus 專案的責任——而是在確認我們自己寫的 rule 名稱與 label matcher 沒有筆誤:漏寫 sli_definition_version="v1"、或把 sli_eligible="true" 打成大小寫不符的 "True",這類低級錯誤人工讀 YAML 時很容易被眼睛自動腦補成正確,寫成一條會真的執行的測試就無所遁形。

⑦ Window、目標與 error budget 要一起出現

假設產品與 owner 最後同意下列示範目標:

SLI: valid /ask requests completed with a contract-valid good result within 10 seconds
SLO: 99.5% over a rolling 28-day window

這裡選「rolling 28-day」而不是「calendar month」不是隨口的格式選擇,兩者算出來的數字會不一樣。Calendar window 每個月初重新歸零,代表若某天發生了一場嚴重事故,budget 的影響會固定在那個月,隔月一號就重新開始——這在對外報告或跟法務對帳時比較直覺,日期邊界清楚。Rolling window 則是每天往前滑動固定天數(例如 28 天),代表一次事故造成的 budget 消耗會持續反映在往後每一天的計算裡,直到那次事故的資料滑出視窗為止。用 rolling window 的好處是團隊不會因為剛好碰上月初就被重置、錯覺自己「重新有滿額度可用」;缺點是同一次事故要在近一個月的每一份報表裡反覆解釋。兩者沒有哪個絕對正確,但選定後要寫進 spec,並保持一致——中途從 calendar 換成 rolling(或反過來),會讓「這個月 SLO 有沒有達標」在不同時間點得到不同答案,卻沒有任何技術面真的改變。

拿一個具體時間點走一遍會更清楚:假設某服務在 9 月 3 日發生一次嚴重事故,消耗了當月三分之二的 error budget。若選 calendar month,9 月 3 日的事故只會讓「九月」看起來不達標,10 月 1 日起每一份報表都是全新起點;若選 rolling 28 天,同一次事故會讓「近 28 天 SLO 未達標」這個結論一路成立到 10 月 1 日左右才自然消退。兩者沒有孰優孰劣,但選擇不同的 window,會讓「這段期間到底可不可靠」得出完全不同的敘事——這正是為什麼 window 類型必須寫進 spec 版本紀錄,而不是留給每次報告的人臨時決定。

error budget 不是另一個神祕指標。

它是 SLO 允許的失敗比例:

error budget = 1 - SLO
             = 1 - 0.995
             = 0.005
             = 0.5%

如果 28 天有 200,000 筆有效 /ask,這份目標最多容忍:

200,000 × 0.005 = 1,000 bad events

這不表示團隊「有 1,000 次可以壞」。

它表示在這個明確定義下,產品與工程已共同接受的風險上限是 1,000 個不符合承諾的使用者旅程。

同樣的 0.5% 在不同流量下,操作意義完全不同。

28 天有效請求 99.5% 允許的 bad events 解讀時要補充的事情
200 1 樣本太少,單一事件就會大幅擺動
20,000 100 可觀察趨勢,但仍要看事故集中度
200,000 1,000 可用於容量與改動風險討論
20,000,000 100,000 必須再分 user journey、region 或 tenant 觀察

所以不要只報「剩餘 budget 72%」。

還要報 window、有效請求數、失敗類型與是否由單一 incident 造成。

分類錯誤會讓 error budget 整套機制失靈

error budget 的價值建立在一個前提上:分子分母的定義本身是準確的。Google SRE Workbook 在討論 error budget policy 時特別提醒一種容易被忽略的失效模式——錯誤分類(misclassification)。這不是指團隊故意作弊,而是指某些被歸類為「不計入 error budget」的失敗,實際上已經傷害了使用者;反過來,某些對使用者毫無影響的技術性故障,卻被算進了 budget 裡。

一個具體例子:若團隊把所有 4xx 一律排除分母外,理由是「client 端問題不該算我們責任」,這規則多數情況合理——直到某次伺服器端相容性破壞性變更,讓原本合法的請求全被判成格式錯誤、回傳大量 400。此時「排除所有 4xx」會讓這次貨真價實的伺服器變更影響完全不反映在 availability 數字上:dashboard 一片綠燈,但使用者端早已全面崩潰。這正是第 ② 段情境表把「已登入使用者被系統錯誤拒絕」單獨列一行、註記「多半應進分母」的原因——籠統規則應付不了所有情境。

反過來的情況同樣常見:某個背景健康檢查因與業務邏輯無關的原因偶爾失敗,卻被計入同一個 availability counter,讓 budget 被完全不影響真實使用者的雜訊悄悄吃掉——團隊可能因此不必要地暫緩安全的 deploy,或投入資源修一個根本不存在的「可靠性問題」。

兩種失效模式的共同教訓,呼應了本文從第 ③ 段就開始強調的立場:分子分母的定義不能等到 incident 發生時才臨時決定,必須在事前、以可稽核、可重現的方式先寫清楚。error budget 這套機制的可信度,完全建立在它背後的 SLI 定義夠不夠準確之上;定義本身出錯,budget 數字再精美也只是一個看起來嚴謹的誤導。Google SRE Workbook:Error Budget Policy

⑧ 快速燒 budget,和沒達 SLO 是兩種訊號

28 天後發現 SLO 未達標,資訊通常已經太晚。burn rate 是「失敗速度相對於允許速度」的描述,可幫助團隊在 budget 快用完時處理,而不是等月底算總帳。

Google Cloud 在 2025 年 6 月 12 日的事件凸顯了這一點。一個 null pointer bug 在 Service Control 系統中觸發,影響全球 50+ Google Cloud 服務超過 2 小時:5 月 29 日發佈的新功能沒有特性開關保護,遇到被新政策污染的資料表時觸發 crash loop,透過「所有依賴它做配額驗證的服務」瞬間放大,讓 Spotify、Discord、Snapchat 等大型依賴方統一回傳 5xx。這不是某個服務的 SLO 失敗,而是隱藏依賴的故障透過級聯讓整個生態的 budget 在分鐘內燒完。burn rate 會沿著依賴鏈傳播;定義 SLI 時必須考慮「我的故障對上游的影響」,而非只看自己的可用性。

仍用 99.5% 這個示範目標:允許錯誤率是 0.005。

若近一小時觀察到錯誤率 0.05,則:

burn rate = observed error rate / allowed error rate
          = 0.05 / 0.005
          = 10

也就是以正常速度的十倍燃燒 budget。

它不自動等於 pager alert。

在低流量服務,一筆失敗就可能算出非常高的瞬間 burn rate;在高風險旅程,即使錯誤率低也可能需要立即處理。告警條件至少需要同時考慮 window、最小樣本量、影響程度與人能採取的動作。

一個適合 Lab 討論的政策表如下:

訊號 可能動作 不該直接推論
5 分鐘 bad ratio 上升 查 trace、provider error、最近部署 月 SLO 一定失敗
1 小時持續高 burn 暫停高風險 deploy、啟動 incident triage 根因已確認
28 天 SLO 低於目標 檢討可靠性工作與 release 節奏 要立刻更改 SLO 定義
semantic evaluator 失敗上升 抽樣審查 prompt、retrieval 與 dataset API availability 已下降

業界怎麼把 burn rate 變成真正的告警規則

上面的政策表只是概念性的討論;Google SRE Workbook 在《Alerting on SLOs》一章給出了更具體、可以直接照抄的起手式參數,稱為 multiwindow, multi-burn-rate alerting。核心想法是:同一個 SLO,用不同「長視窗+短視窗」的組合搭配不同的 burn rate 門檻,同時判斷「問題有多急」和「問題是否還在發生」。以 99.9% 的 SLO 為例,官方建議的起手式參數是這樣:

嚴重度 長視窗 短視窗 burn rate 門檻 消耗 budget 比例
需要立刻叫醒人(page) 1 小時 5 分鐘 14.4 2%
需要盡快處理(page,較緩) 6 小時 30 分鐘 6 5%
排進待辦、上班處理(ticket) 3 天 6 小時 1 10%

這張表最值得注意的不是數字本身,而是「短視窗」存在的理由。若只用長視窗判斷(只看 1 小時 burn rate 是否超過 14.4),故障修復後,這條長視窗的平均值要等上整整 1 小時才會慢慢降回門檻以下——值班的人已經修好問題,pager 卻還在響,逼他手動 silence。加入短視窗(5 分鐘)作為第二個必須同時滿足的條件,故障一停止,短視窗的錯誤率幾分鐘內就會降到門檻以下,alert 幾乎立刻停止觸發。Google SRE Workbook 給的經驗法則是「短視窗抓長視窗的 1/12」,1 小時配 5 分鐘、6 小時配 30 分鐘都符合這個比例。

把這套參數套進 Day 14 這個 Lab 假設的 99.5% SLO、28 天 window,概念上的 PromQL 骨架會長成這樣(真正上線前,budget 百分比與視窗長度都要依實際流量與 on-call 負擔重新校準,不能直接照搬 99.9% 的參數):

# page 級別:1 小時 burn rate 同時要 ≥ 14.4,且 5 分鐘 burn rate 也要 ≥ 14.4
(
  sum(increase(ask_sli_events_total{sli_eligible="true", sli_result="bad"}[1h]))
  /
  sum(increase(ask_sli_events_total{sli_eligible="true"}[1h]))
) > (14.4 * 0.005)
and
(
  sum(increase(ask_sli_events_total{sli_eligible="true", sli_result="bad"}[5m]))
  /
  sum(increase(ask_sli_events_total{sli_eligible="true"}[5m]))
) > (14.4 * 0.005)

0.005 是這份 Lab 示範的 error budget(1 - 0.995);換成自己服務的目標時,這個係數要跟著改。這條規則要求「長視窗、短視窗都同時超標」才觸發 page,正是為了同時滿足「確實正在發生嚴重問題」與「不是曇花一現的雜訊」兩個條件。

Google SRE 將 error budget 視為可靠性與改動速度的共同語言,而不是用來懲罰某一個團隊的 KPI。Google SRE Workbook:Implementing SLOs Google SRE Workbook:Alerting on SLOs

若 error budget 已經耗盡,合理反應可能是暫緩風險較高的 prompt、model 或 retrieval change,先處理造成 bad event 的流程。

不合理反應是把 bad 改成 good,讓 dashboard 恢復漂亮。

同一個 burn rate 門檻,在不同流量規模下對應到多少筆真實請求

14.4 這個數字本身是抽象的比率,只有換算回實際請求數,才會知道 alert 真正在保護什麼。延續第 ⑦ 段「同樣 0.5% error budget 在不同流量下解讀不同」的邏輯,把它套進 page 級門檻(1 小時視窗、14.4 倍 burn rate):

1 小時內有效請求 允許的正常 bad 事件(budget 的 1/24,因為 1 小時只是 28 天的 1/672,但 burn rate 用的是速率不是總量) 觸發 page 門檻所需的 bad 事件數(約)
100 0.5 8
1,000 5 72
10,000 50 720

(表中數字為概念性換算,14.4 × 0.005 × 有效請求數 是觸發門檻的近似 bad 事件數,實際告警系統仍以比率運算,這裡換算成整數請求數只是為了建立直覺。)

這張表最值得注意的一行是最上面:/ask 在某個離峰時段 1 小時只有 100 筆有效請求,光是 8 筆失敗就足以觸發本應保留給「嚴重、持續」問題的 page 級告警——這是「低流量服務容易被單一事件推出瞬間高 burn rate」的具體數字版本。值班工程師半夜被吵醒時,第一個該問的不是「SLO 訂錯了嗎」,而是「這個時段的樣本數夠不夠讓 burn rate 有意義」。多數團隊會在 alert 規則裡額外加一個最小樣本量門檻(有效請求數低於某個數字時改用絕對失敗數門檻)——這是低流量服務要不要被 burn rate 公式正確服務的關鍵開關。

⑨ latency SLI 不應被 availability 吃掉

10 秒內完成 已經把 latency 放進 availability SLI。

這很常見,也足以回答「使用者是否在可接受時間內完成這件事」。

但它不能取代 Day 16 會處理的 latency distribution。

以下兩個服務都有 99.6% 的「10 秒內完成」:

Service A: P50 0.8s, P95 1.5s, P99 7.0s
Service B: P50 0.8s, P95 8.9s, P99 9.8s

在 availability SLI 裡它們可能都過關。

使用者感受卻很不一樣,尤其是每一次都要等待模型串流、工具呼叫或 retrieval 的互動式流程。

「把 latency 門檻寫進 availability SLI」和「latency 另外開一條獨立 SLI」不是互斥做法,而是回答不同層次問題的兩層防護。10 秒門檻寫進 availability 定義,回答的是粗略但對體驗有直接意義的二元問題:「這次請求算不算達成期待?」天生適合算 error budget、做 burn-rate alert,因為 burn rate 的數學建立在「good/bad 二元分類」上。獨立的 latency SLI(P50/P95/P99 分布)回答更細緻的問題:「使用者實際體感的等待曲線長什麼樣子?」這層資訊在容量規劃、比較 model route 效能、判斷是否收緊門檻時才是真正有用的輸入——這也是為什麼兩者都要留下,而不是二選一。

門檻本身要設在哪裡,也牽動 SLO 百分比的意義。把目標從 99.5% 收緊到 99.9%,意味著要求絕大多數請求都落在遠低於 10 秒的位置,否則光是「壓線過關」的請求就可能吃掉大半個 0.1% 的 error budget。反過來把門檻從 10 秒放寬到 15 秒,同樣的 99.5% 目標會容易達成很多,因為悄悄放棄了「10 到 15 秒之間仍感受到明顯等待」這個問題。門檻與百分比是同一個承諾的兩個旋鈕,調整其中一個而不重新檢視另一個,很容易讓 SLO 名義上不變,但實際保護到的體驗已經悄悄鬆動。

因此同一條 /ask 可以同時有:

Availability SLI
→ valid requests that reached a usable outcome within 10 seconds

Latency SLI
→ distribution of end-to-end completion latency for eligible requests

Semantic-quality indicator
→ sampled evaluator result; not inferred from HTTP success

三者能共用 request_id 與 trace context,卻不該被壓成一個「AI 健康分數」。

latency SLI 的分母,和 availability SLI 不一定相同

容易被忽略的一點是:latency SLI 的分母,不見得等於 availability SLI 的分母。假設一筆 /ask 請求在第 3 秒就因為 provider timeout 而失敗,它在 availability SLI 裡是不折不扣的 bad event;但它有沒有資格出現在 latency 分布裡,要先想清楚。把所有失敗請求的「失敗前經過的時間」都塞進同一組 latency histogram,會讓分布形狀被大量短命的失敗請求拉向左邊,看起來像是「系統普遍很快」,但那其實是「壞掉得很快」,不是「服務得很快」。

比較乾淨的做法,是把 latency histogram 限定在成功完成的請求範圍內量測,失敗請求另外用 availability SLI 的分子分母處理。用 Python 的 prometheus-client 大致的宣告方式會是這樣:

from prometheus_client import Histogram

ASK_LATENCY_SECONDS = Histogram(
    "ask_completion_latency_seconds",
    "End-to-end latency for successfully completed /ask requests",
    ["route"],
    buckets=(0.1, 0.25, 0.5, 1, 2, 5, 8, 10, 15, 30),
)

# 只在 workflow 真正產出可用結果時才 observe,
# provider timeout、invalid contract 這類失敗事件不進這個 histogram
if is_good_event(event):
    ASK_LATENCY_SECONDS.labels(route="/ask").observe(event.latency_ms / 1000)

bucket 邊界刻意不是均勻切分,而是在「使用者關鍵決策點」附近變密——1 秒到 10 秒之間切得比較細,因為對一個互動式問答服務而言,這段區間正是使用者從「感覺流暢」滑向「開始不耐煩」的地帶;超過 10 秒之後,使用者體驗已經定調為「慢」,bucket 切得再細也不會改變任何決策,所以留給 15 秒、30 秒兩個較粗的邊界就夠。這種「在意義變化劇烈的區間加密取樣點」的設計原則,會在 Day 16 講 P50/P95/P99 與 histogram bucket 設計時再深入展開;這裡先建立一個概念:bucket 邊界不是均勻分割量表,而是使用者體驗曲線的採樣密度地圖。

從 histogram 裡把 P95 撈出來,順便看清楚 bucket 邊界的代價

有了上面的 ASK_LATENCY_SECONDS histogram,histogram_quantile() 是 PromQL 用來估計分位數的標準函式。概念上的查詢長這樣:

histogram_quantile(
  0.95,
  sum by (le) (
    rate(ask_completion_latency_seconds_bucket{route="/ask"}[5m])
  )
)

這裡刻意用 sum by (le) 而不是直接對 ask_completion_latency_seconds_bucket 取 rate() 就完事,原因跟本文第 ⑥ 段「多個 replica 各自累加」是同一個道理:每個 replica 各自維護自己的 histogram bucket 計數,sum by (le) 先把所有 replica 在同一個 le(less-than-or-equal,bucket 上界)底下的計數加總,才能算出代表整個服務、而不是單一 pod 的分位數。

histogram_quantile() 的結果是估計值,不是精確值——這一點跟本文第 ⑥ 段強調 increase() 也是估計值是同一種誠實。Prometheus 的傳統 histogram 只記錄「落在每個 bucket 邊界以內的計數」,並不知道某個請求在 bucket 內部的確切數值,因此 histogram_quantile() 是在 bucket 邊界之間做線性內插。這代表分位數的估計精確度,直接受限於 bucket 邊界切得夠不夠密:如果 P95 剛好落在 5 到 8 這個跨距較大的 bucket 之間,histogram_quantile() 給出的數字可能跟真實值有明顯落差;但如果 P95 落在 1 到 2 這種切得較密的區間,估計值會準確得多。這正好回頭印證前一段「bucket 邊界是採樣密度地圖」這句話:不是隨口的比喻,而是 histogram_quantile() 底層演算法的直接後果——bucket 切得密的地方,分位數估得準;切得疏的地方,估得粗。設計 bucket 邊界時,等於是在提前決定「未來哪個延遲區間的 P95 數字比較可信」。

⑩ 今日 DIY:從文字 spec 到可重跑的小實驗

以下所有步驟由讀者自行在自己的 Day 14 目錄完成。

本文沒有建立檔案、安裝套件、啟動服務、執行指令或驗證結果。

這個 DIY 刻意設計成「不需要跑起任何服務」就能驗證的形式,這本身也是在示範一件事:SLI 的定義品質,理論上可以在寫出第一行 Prometheus query 之前就先被檢驗。前面九個段落講了很多「應該怎麼想」,這六個步驟則是把那些想法收斂成三個可被獨立檢查的產物——一份規格文件、一組可重跑的分類測試、一組查詢草稿——分別對應「有沒有把責任寫清楚」「規則本身是否自洽」「資料拿出來時是否可解讀」三個層次的驗證。就算你不打算真的執行任何指令,往下讀完這六步的說明,也能看懂「一個及格的 SLI 落地流程」長什麼樣子;如果你想動手做,跑起來之後應該看到的結果與可能踩的坑,也都寫在對應段落裡。

1. 建立一頁可審查的 SLO spec

先建立 slo.md。不要先填一個看起來專業的百分比;保留沒有證據的欄位為 TBD。

# answer-api availability SLO

## User journey

An authenticated user submits a valid `/ask` request and receives a
contract-valid result or an explicit, product-approved next step.

## SLI definition

- Definition version: `ask-availability-v1`
- Data source: application completion counter `ask_sli_events_total`
- Denominator: events where `route="/ask"` and `sli_eligible="true"`
- Numerator: denominator events where `sli_result="good"`
- Good event: contract-valid `completed` or `insufficient_context` within 10 s
- Bad event: timeout, system rejection, 5xx, invalid response contract, or over-10-s completion
- Exclusions: malformed request payloads; load-test traffic only when explicitly tagged

## Objective

- Target: `TBD` after product and traffic review
- Window: `TBD`; evaluate rolling and calendar windows before choosing
- Error-budget policy: `TBD`

## Ownership and review

- Service owner: `TBD`
- Product owner: `TBD`
- Definition review date: `TBD`
- Change log: initial Lab draft; not a production or contractual commitment

留著 TBD 不是偷懶。

它在防止「文章裡的示範數字」被誤認成系統已承諾的 production 目標。

驗證什麼:對應第 ⑤ 段「五個問題」——寫完回頭檢查每個欄位是否都對得上使用者旅程、分母、分子、資料來源、誰能改規則其中之一。若某欄位仍含糊帶過(例如「分母:合理的請求」),代表那題還沒真的被回答,該回頭重寫。

常見的坑:反射動作是想替 TBD 填一個看起來專業的數字(見第 ①、⑤ 段的陷阱)。硬填只會讓下一個讀者誤以為這是已對齊產品、法務的正式承諾;留白才是誠實的訊號。

2. 建立分類程式與 fixture

把前面的 AskEvent、is_sli_eligible() 與 is_good_event() 放進 classify.py。

再建立 fixtures.py,放入至少六種事件:正常完成、誠實資料不足、provider timeout、HTTP 200 但 contract 無效、格式錯誤請求、系統錯誤拒絕。

請替每一筆 fixture 寫一條人可讀註解:它為什麼在分母內或外?為什麼是 good 或 bad?

這一行註解是未來 review 的入口。

驗證什麼:對應第 ④ 段「文字規格若只存在會議紀錄,半年後沒人知道 label 是誰定的」。把規則寫成程式碼跑過一輪,是在檢驗它是否真的可重現——換個人執行也該得到一樣的判定。若寫 fixture 時對某情境的 good/bad 猶豫,通常代表第 1 步的 spec 還不夠精確,該回頭補 spec,而不是在 classifier 裡用註解自我說服。這也順帶驗證了 classifier 刻意不做的判斷:requires_human_review 不在 GOOD_WORKFLOW_STATUSES 裡——即使正確轉接真人,仍算 bad,因為尚未在門檻內拿到可用結果。availability SLI 只回答「是否在時間內拿到結果」,不回答「處理方式是否恰當」。

3. 執行規格檢查

若你的 Day 14 DIY 是標準 Python 專案,可在該專案目錄自行執行:

uv run python classify.py

你應該觀察到下列結果,而不是只確認程式「沒有報錯」:

grounded_answer {'eligible': True, 'good': True}
honest_unknown {'eligible': True, 'good': True}
provider_timeout {'eligible': True, 'good': False}
http_200_but_invalid {'eligible': True, 'good': False}
malformed_payload {'eligible': False, 'good': False}
system_denied_user {'eligible': True, 'good': False}

如果你的產品不把 insufficient_context 視為可接受結果,應改 spec、fixture 與 classifier,再重新解釋為什麼。

不要只改預期輸出讓測試通過。

預期看到什麼:六筆 fixture 應一次性全數通過,且每一行都能對照到一個具體理由——這不是「跑起來不報錯就算過」的煙霧測試。若沿用既有 DIY 專案跑 scripts/validate_slo.py,應看到六筆判定結果與末尾一行通過總結。

容易誤判的陷阱:「全部通過」只代表輸出符合你事先寫好的預期值,不代表那組預期值本身正確。若最初就把某情境的判斷寫錯(例如誤以為 insufficient_context 不該算 good),錯誤的預期會被忠實驗證通過,SLI 定義卻從一開始就偏離產品真實意圖——規格測試只保證程式碼忠實實現規則,不保證規則本身是對的。

4. 將 classifier 結果映射為有限 labels

真正串回應用程式時,只匯出有限集合的 labels。

sli_eligible: true | false
sli_result: good | bad | excluded
route: /ask
sli_definition_version: v1

不要匯出:

request_id
trace_id
full_prompt
user_email
retrieved_document_id
raw_model_output

這些資料需要被保留時,請放在有存取控制、遮罩與 retention 策略的 logs 或 trace backend;不是塞進 Prometheus label。

驗證什麼:對應第 ③ 段「event contract」與「高基數 label 不是品味問題」。把清單拆成「可以進 label」與「不行進 label」,是逼自己回答「這個欄位的可能值有沒有上限」——sli_result 只有三種、有界;request_id 每筆都不同、無界。

常見的坑:為了「以後查起來方便」,把一個看似有限、實際會隨時間增長的欄位誤判為低基數。prompt_version 就是一例——早期只有兩三個值,但隨著迭代,半年後可能累積到幾十上百個。判斷依據不是它「現在」有幾個值,而是「一年後」大概有幾個值,這正是本文把它歸進 logs/traces 而非 metrics 的原因。

5. 在 Prometheus 檢視分子、分母與無樣本 window

先分開查三件事。

# eligible events in the last hour
sum(increase(ask_sli_events_total{
  route="/ask",
  sli_definition_version="v1",
  sli_eligible="true"
}[1h]))
# good eligible events in the last hour
sum(increase(ask_sli_events_total{
  route="/ask",
  sli_definition_version="v1",
  sli_eligible="true",
  sli_result="good"
}[1h]))
# bad eligible events by result; the label set should remain small
sum by (sli_result) (
  increase(ask_sli_events_total{
    route="/ask",
    sli_definition_version="v1",
    sli_eligible="true"
  }[1h])
)

先看 raw counts,再看 ratio。

若分母為零,確認你的圖表呈現 N/A 或清楚說明「沒有樣本」,而不是把安靜的夜晚畫成完美可用性。

驗證什麼:對應第 ⑥ 段「counter 只會單調遞增」與「zero denominator 不代表 100% 可用」。這個 Lab 尚未接上流量,執行這三條查詢時分母大概率是零,正好是觀察機會:確認 Grafana panel 在分母為零時是顯示危險的平 0%、更危險的平 100%,還是正確的 N/A 或不畫線——多數工具的預設並不會自動留白,值得在真正串接流量前先確認,而不是等某個離峰時段被主管指著一條滿分線追問「這是真的嗎」。

6. 寫下 failure drill 的觀測假設

不需要立刻真的製造故障。

先在 README 或 runbook 寫下預期:

Scenario: provider timeout for valid /ask requests
Expected metric: eligible count increases; bad count increases
Expected trace: provider span ends with timeout or error status
Expected log: same request_id has timeout classification and deployment version
Expected user result: clear retryable or unavailable state, not an empty 200 response
Expected SLO effect: availability ratio declines for the affected window

這份假設讓你日後做 Day 3 式 fault injection 時,知道要驗證哪一條鏈,而不是只截一張 Grafana 圖。

驗證什麼:把 Day 3 的 fault injection 手法與今天定義的 SLI 語言正式接起來。寫下五行預期,是逼自己在打壞系統之前先用分子分母語言講清楚「壞掉了會怎樣」——若連紙上談兵都講不清楚,實際跑故障時多半只會看到數字變動、說不出因果。

為何不要求現在執行:Day 14 只求先把量測規則定義清楚,量出真實 baseline 是 Day 15、16 的事。SLI 定義還沒經過 review 就急著做 fault injection,容易把「classifier 有 bug」和「服務真的故障」混在一起。先寫假設、留到規則穩定後再執行,是刻意的順序安排,不是拖延。

7. 找一個人來讀你的 spec,而不是只讀自己的程式碼

前六步都是可以獨自完成的技術工作。第七步刻意換一種形式:把 slo.md 拿給一個沒有參與撰寫的人(同事、甚至只是把自己一週後的視角當作「另一個人」),請對方不看程式碼、只憑 spec 文件回答三個問題:

1. 這條 SLI 在量測哪一個使用者旅程?能不能用自己的話重述一次?
2. 「排除條件」欄位裡的每一項,你能不能想出至少一個
   「規則沒說清楚該怎麼分類」的邊界案例?
3. 如果這個月數字不達標,你知道該去找誰、看哪份資料嗎?

如果對方在第 1 題卡住、只能複述文件裡的句子卻講不出自己的理解,通常代表 spec 寫得還是太貼近程式碼術語,沒有真正轉譯成產品語言。如果對方在第 2 題輕易就能想出 spec 沒涵蓋到的邊界情境(例如「如果使用者用 API Key 而非登入 session 呼叫,算不算 valid_request?」),這代表 fixture 集合還需要再補幾筆。如果對方連第 3 題「該找誰」都答不出來,代表 spec 裡「誰能改規則」那一欄寫得不夠具體,只留了團隊名稱而沒有實際的 owner。

驗證什麼:這是唯一「不能自己一個人完成」的步驟——第 ① 段就強調 SLI/SLO/SLA 涉及多方責任,一份只有作者自己看得懂的 spec,內部邏輯再自洽也還沒達成「可審查」的門檻。找人讀一次,是低成本地提早暴露這份文件離真正對齊產品、法務還有多遠。

8.(選用)把查詢草稿改寫成一份 recording rule 骨架

前六步的重點是規則本身要正確,這個選用步驟是把「規則正確」再往前推一步:確認同一份規則寫出來只有一個版本,不會在 dashboard、alert、事後 review 三個地方各自長出稍有出入的抄本。即使你的 Day 14 DIY 專案還沒有真的連上 Prometheus,也可以先把第 ⑥ 段介紹過的 recording rule 骨架寫成一個檔案草稿:

# Day14/DIY/observability/recording_rules.yml(草稿,尚未連上真實 Prometheus)
groups:
  - name: ask_availability_sli
    rules:
      - record: ask:sli_events:good_1h
        expr: |
          sum(increase(ask_sli_events_total{
            route="/ask", sli_definition_version="v1",
            sli_eligible="true", sli_result="good"
          }[1h]))
      - record: ask:sli_events:eligible_1h
        expr: |
          sum(increase(ask_sli_events_total{
            route="/ask", sli_definition_version="v1",
            sli_eligible="true"
          }[1h]))

驗證什麼:把第 ⑥ 段「同一段查詢不要抄三次」的原則落成可 code review 的 YAML,而不是停留在建議。回頭檢查第 5 步的三條 ad-hoc PromQL 能否直接用這兩個 record 名稱重新表達——若某條用了不同的 label matcher,這是提早發現「查詢定義悄悄分岔」的訊號,比等 alert 跟 dashboard 數字對不上才發現便宜得多。

注意事項:這份 YAML 只是草稿,要真正生效需在 Prometheus 設定裡用 rule_files: 指向它並 reload;本 Lab 尚未走到那一步。先把骨架與命名慣例寫對,是為未來接上流量做準備。

⑪ 驗收:你應能回答這些問題

  • [ ] 我能用自己的服務描述 SLI、SLO、SLA 的不同責任。
  • [ ] 我已為一條使用者旅程定義分子、分母、排除條件與資料來源。
  • [ ] 我能說明為什麼 HTTP 200 不是唯一的 good-event 規則。
  • [ ] 我已把 request_id 這類高基數資料放在 trace 或 log,而非 metric label。
  • [ ] 我有至少六個 classifier fixture,包含「200 但不合格」的反例。
  • [ ] 我能從 counter 分開看 eligible、good、bad,而不是只看一條百分比。
  • [ ] 我已把無樣本 window 與 100% availability 區分開來。
  • [ ] 我知道範例中的 99.5%、10 秒與 28 天不是本服務已驗證的 production 承諾。
  • [ ] 我有 owner、review date 與變更紀錄欄位,而不是只留一張 dashboard。
  • [ ] 我知道同一段查詢邏輯不該在 dashboard、alert、事後 review 各自維護一份抄本。
  • [ ] 如果有人提議把多條 SLI 加權平均成一個總分,我能說出這會稀釋掉哪一種真實的使用者傷害。

⑫ 常見誤用:數字沒有錯,問題是它回答錯題

把健康檢查算成使用者成功

/health 回 200 只能證明某個 process 還活著。

若 provider、retrieval 或 validator 已經失效,使用者旅程仍可能全面失敗。

health check 應保留給 deployment 與流量路由;使用者 SLI 則應由使用者可見的 completion event 量測。

兩者的區別直式寫出來會更清楚:

System health
(process 活著、能回應探測)
        ≠
User-facing availability
(使用者旅程真的走到可用結果)

這個落差不只是「健康檢查太粗」,健康檢查與 SLI 在設計上本來就回答不同的問題。多數健康檢查是二元判斷:process 活著回 200,process 掛了回 5xx 或直接不回應;而且常見的告警規則還要求「連續幾次探測都失敗」才觸發,目的是避免單次網路抖動就誤判。這個設計對「偵測全面停機」很有效,卻對「部分退化」完全視而不見:如果服務只有 1%~2% 的請求慢到逾時或回傳不合格的 response,其餘探測仍然正常,健康檢查會一路綠燈,直到退化擴大到影響絕大多數請求才會被發現。

Honeycomb 團隊分享過促使他們認真投入 SLO 的一次事件正是這種情境:一次只影響約 1%~2% 請求的部分退化(brown-out),傳統健康檢查完全沒有反應,因為監控邏輯要求「連續兩次探測失敗」才算故障,而這次的失敗是間歇性、分散的。團隊事後描述:「our SLO immediately started burning and we realized, relatively quickly, that our users were being affected」——以 good/bad event 比例為基礎的 SLO burn,幾乎即時反映了使用者受影響的程度。他們形容這是真正開始認真投入 SLO 的轉捩點,因為這套機制幫助他們看見「使用者體驗中的長尾,而不只是多數人的體驗」——恰好呼應本文第 ② 段拆出來的多條使用者旅程:若高風險旅程只占整體流量一小部分,用「多數使用者的體驗」代表整個系統,正是會把長尾裡的真實傷害蓋過去。Honeycomb:Actionable SLOs Based on What Matters Most

把「SLO 達標」誤讀成「系統沒問題」

即使一切定義都做對了,還有一種誤用發生在解讀數字的最後一步:把「這個月 availability 達到 99.6%,高於 99.5% 目標」直接等同於「系統這個月沒有問題」。這個推論忽略了三件本文一路強調的事情。第一,availability SLI 通常只涵蓋單一旅程(例如一般政策問答),如果高風險轉真人旅程(前面第 ② 段的旅程 C)另外有一條沒有被拿出來看的 SLI,兩者達標與否是分開的事,不能用其中一個代表全部。第二,達標的 99.6% 底下可能藏著集中在特定時段、特定 tenant、或特定 prompt 版本的失敗——第 ⑦ 段已經提醒「不要只報剩餘 budget 比例」,同樣的道理也適用於「達標」這個結論本身:一個月平均達標,不代表這個月裡沒有任何一小時 burn rate 衝到 10 倍以上,只是那段時間夠短,沒能拖垮月度平均。第三,SLI 定義本身有沒有涵蓋到位使用者真正在意的失敗模式,是一個持續要重新檢視的問題,不是定義一次就永遠正確——這正是第 ①、⑤ 段反覆強調版本與 review 機制的原因。

「達標」是一個必要但不充分的健康訊號。它值得被慶祝,但慶祝的方式應該是「這個月我們對使用者的承諾兌現了」,而不是「這個月系統完全沒有值得關注的問題」——後者需要搭配 latency 分布、semantic evaluation、以及對達標數字背後的分布做進一步檢視才能下結論。

為了讓圖漂亮而排除整種失敗

「429 都是使用者的問題」與「4xx 都不進分母」都是過早結論。

請先拆出被文件化 quota 限制、惡意請求、認證 provider 故障與權限規則 bug,再由產品與 owner 決定各自是否屬於承諾。

可稽核的排除是定義。

incident 後臨時排除剛好造成數字變差的流量,是改寫歷史。

讓一個 SLI 承擔所有品質判斷

availability SLI 回答「使用者是否在承諾時間內得到可用結果」。

它不會證明回答中的政策事實正確,也不會量化 citation 是否支持答案。

把 semantic evaluation、safety refusal、cost 與 latency distribution 分成不同訊號,才能在其中一項變差時知道要改哪一段 workflow。

只用單一視窗判斷 burn rate

第 ⑧ 段介紹 multiwindow, multi-burn-rate alerting 時提到,只用一個長視窗(例如固定看 1 小時)判斷 burn rate,會在故障排除後讓 pager 持續響上很長一段時間,逼值班工程師手動 silence——這是最直接的副作用。但還有一個更隱蔽的問題:只用單一短視窗(例如只看 5 分鐘)反過來會對雜訊極度敏感,尤其在流量本來就不大的服務上,一次偶發的批次失敗(例如某個下游依賴短暫抖動 30 秒)就足以讓 5 分鐘視窗的錯誤率瞬間衝破任何合理的門檻,觸發一次其實不需要叫醒任何人的 page。

這兩種偏誤方向相反,卻是同一個根因:用單一視窗長度去同時回答「問題有多急」和「問題是否仍在發生」這兩個不同的問題。長視窗適合判斷「這是不是一次持續、有意義的退化」,短視窗適合判斷「現在這一刻,問題是否還在燒 budget」;只用其中一個視窗,等於放棄了另一半的判斷力。這也是為什麼值得投入時間把 burn-rate alert 至少做成兩層——即使一開始只能先做最粗略的版本,也比完全不用 burn rate、只在月底看一次總體 SLO 達標與否要有意義得多。

宣告 SLA 前沒有可重現計算

在合約裡寫出 99.9% 之前,至少要能以同一份資料、同一套 exclusion 規則、同一個 window 算出相同結果。

若不同人從不同 dashboard 得到不同數字,現在需要的是 SLI definition review,不是更高的 SLA。

把 SLA 當成內部溝通用的 SLO

最後一種常見誤用發生在方向相反的地方:把對外承諾的 SLA 數字直接拿來當內部團隊的告警門檻——「反正 SLA 是 99.9%,就在這條線設告警」,聽起來謹慎,但 SLA 與 SLO 的目的完全不同,混用會製造兩種相反的風險。

若 SLO 目標與 SLA 承諾設成同一個數字,團隊會在「真正違反合約」那一刻才收到第一個告警,值班工程師能做的只剩事後補救。第 ① 段已強調 SLO 必須比 SLA 更嚴格,正是為了在合約被打破前留出反應空間——例如 SLA 承諾 99.9%、內部 SLO 訂 99.95%,這個差距就是「早期預警帶」。

反過來,若把 SLA 數字當成日常對外宣傳或口頭承諾的內部指標,業務同仁看到 dashboard 上寫「SLA: 99.9%」,很容易誤以為這已是簽進合約、具法律效力的數字,拿去對客戶做出未經法務審過的承諾。SLA 只該存在於正式合約與技術附錄裡,內部 dashboard 顯示的永遠是 SLO,並清楚標示「這是內部目標,不是合約條款」。

把多條 SLI 加權平均成一個「總分」

第 ① 段已拆過這個機制:稀釋。這裡的具體場景通常是:主管一句「能不能給我一個數字」,工程團隊把三、四條 SLI 用流量比例加權平均,湊出「本月 AI 服務健康分數:99.2%」。這個分數一旦出現在月報裡就會取得不該擁有的權威性——旅程 C 表現明顯惡化,因流量占比極小、加權後幾乎不受影響,主管看到的仍是「99.2%,符合預期」,沒人意識到業務風險最高的旅程正在惡化,直到演變成客訴才回頭發現數字早就在下滑。

比較誠實的做法不是拒絕給一個數字,而是換一種呈現:一排各自獨立的燈號,或一張「這個月哪條 SLI 沒達標」的簡短列表,而不是把所有訊息揉成一個資訊量反而更少的百分比。

誰能改 SLI 定義:一個常被漏掉的治理問題

前面談了很多次「SLI 定義有版本、有 changelog」,但還有一個更基本的問題沒回答:誰有權力改這份定義?若答案是「寫程式的人自己改」,前面所有版本紀錄的討論都會流於形式——同一群人既是被量測的對象,也是量測標準的制定者,任何一次私下調整都可能只是把不想面對的數字改到不被看見。

一個比較站得住腳的做法,是把「誰能提案」跟「誰能核准」分開來看:

角色 能做的事 不能做的事
開發/SRE 團隊 提案修改 SLI 定義(例如調整門檻、新增排除條件)、附上理由與預期影響 未經核准直接上線新的分類邏輯
產品/業務負責人 核准是否符合使用者實際期待、確認排除條件不是在掩蓋真實問題 片面要求放寬門檻卻不留下書面理由
稽核/法遵(如涉及 SLA) 核對 SLI 定義變更是否影響對外合約承諾 干預技術層面的量測實作細節

這張表不是要建立繁瑣的簽核流程——多數團隊規模根本不需要三個角色分開審核。重點是原則本身:修改「怎麼判斷成功」的人,不應該只有「被這個判斷結果評分」的同一群人。哪怕只是「開發者提 PR、產品負責人留言核准」這麼輕量的形式,留下「誰、什麼時候、因為什麼理由」的紀錄,就比「工程師自己覺得該改就改」站得住腳。

⑬ 本文結論

SLO 的工作不是替系統發獎牌,而是讓團隊知道何時該停止增加風險。今天定義的分子分母、fixture、burn rate 告警與版本紀錄,全部服務同一個目的:把「系統好不好」從一句模糊的感覺,變成任何人都能重新算出同樣結果的規格。這份規格還不是終點,只是讓後面的告警、事後檢討、容量規劃有共同語言可以對話——之後每一天遇到的告警閾值、SLO 目標、容量規劃,都建立在今天這份定義之上。定義本身站不住腳,後面疊上去的數字算得再精確,也只是精確地算錯。

Day 15 預告

Day 14 建立了 SLI、SLO、SLA 這套語言,但還沒有量出任何一個真正的數字。Day 15 要處理最基本、也最常被誤解的指標:availability 到底該怎麼算,才不會讓「系統正常運作中」跟「使用者拿到可用結果」變成兩件事。

延伸閱讀


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


上一篇
Day 14(上)|SLI、SLO、SLA:先量承諾,再談百分比
下一篇
Day 15(上)|Availability:不是把 200 數一數
系列文
Learning SRE for the AI Era:從 SRE Lab 到 Production AI Reliability 共 44 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言