可觀測性與 SLO ch03 PromQL:你儀表板上那個 p99,很可能從一開始就是錯的
下一章→
CH 03 算對才有用

PromQL:你儀表板上那個 p99,很可能從一開始就是錯的

instant 與 range vectorrate 的外推與 4 倍規則先 rate 再 sumhistogram_quantile 與 bucket 設計absent 偵測沒發生的事recording rules查詢成本

上一章把資料收進來了,這一章要把它變成答案。而 PromQL 有一個很危險的性質:

寫錯的查詢幾乎從不報錯,它只是安靜地回一個「看起來很合理」的數字。

rate() 的範圍寫太短,圖上會出現斷斷續續的空洞,但你可能以為那是流量真的沒了。histogram_quantile() 漏掉一個 by (le),p99 照樣畫得出來,只是那個數字沒有任何意義。把十台機器的 p99 取平均,Grafana 會很開心地畫給你——ch01 說過那是錯的,但它不會提醒你。

這一整章的重點就是:哪些寫法會給你一個錯的數字,而你不會發現。

  • 兩種向量:搞懂 instant 與 range 的差別,一半的語法錯誤就消失了。
  • rate 的三個真相:它會外推、它需要至少 4 倍抓取間隔、它只能用在 counter。
  • sum 與 rate 的順序:這兩個寫反的後果,比你想的嚴重得多。
  • histogram_quantile:正確寫法只有一種,而 bucket 邊界要圍繞 SLO 來設計。
  • absent():ch01 說可觀測性天生看不到「沒發生的事」——這是少數的補救工具。
  • recording rules:把正確寫法固化下來,順便解決查詢太慢的問題。
  • 一個真實場景:SLO 報告月月達標、p99 顯示 180 毫秒,而客訴從來沒斷過。

這一章的例子都用上一章的指標。如果 rate、counter、histogram 的關係還不熟,先回 ch02 看一次型別那節——PromQL 的一半陷阱,根源都在型別選錯或用錯。

PromQL 的每個運算式都會回傳四種型別之一,而日常會碰到的只有兩種。搞清楚它們的差別,大部分「為什麼這個函式不能用」的問題就沒了。

instant vector:每條時序的「當下一個值」

http_requests_total
# → 每條符合的時序,各給一個當前值
#   http_requests_total{status="200"}  48213
#   http_requests_total{status="500"}     12

range vector:每條時序的「一段時間內的所有值」

http_requests_total[5m]
# → 每條時序,各給過去 5 分鐘內的一串樣本點
#   http_requests_total{status="200"}  48180@t1 48195@t2 48213@t3 ...
規則很單純:rate()/increase()/*_over_time() 這類「需要看變化」的函式,只吃 range vector;而 sum()/avg() 這類聚合運算子,只吃 instant vector。
所以 sum(http_requests_total[5m]) 是錯的,而 sum(rate(http_requests_total[5m])) 是對的——因為 rate() 把 range vector 變回了 instant vector。

選擇器:怎麼挑出你要的時序

http_requests_total{job="order-api"}              # 完全相等
http_requests_total{status!="200"}                # 不等於
http_requests_total{status=~"5.."}                # 正則匹配(5xx)
http_requests_total{uri!~"/actuator.*"}           # 正則不匹配

{__name__=~"http_.*"}                             # 連指標名稱都能匹配(很貴,少用)
一個實務上很常見的坑:忘了限定 job 或 service。
sum(rate(http_requests_total[5m])) 會把全公司所有服務的請求數加在一起——查詢不會錯,圖也畫得出來,只是那個數字沒有任何意義。每個查詢都要問自己:這裡面混進了什麼我沒想到的東西?

staleness:時序消失之後

Prometheus 有一個 5 分鐘的過期規則:如果一條時序超過大約 5 分鐘沒有新樣本,查詢就當它不存在了(而不是一直沿用最後一個值)。

這對「pod 被刪掉」這類情境是對的行為,但它也造成一個現象:服務掛掉之後,它的指標不是變成 0,而是「消失」。於是 rate(errors[5m]) > 0 這種告警在服務完全掛掉時不會觸發——因為沒有資料可以比較。這是本章 absent() 那一節要處理的問題。

rate() 是 PromQL 用得最多的函式,也是誤用最多的。它做的事情是:在指定的時間範圍內,計算 counter 的每秒平均增長率。

rate(http_requests_total[5m])
# → 過去 5 分鐘,平均每秒增加幾次請求

真相①:它會外推,所以結果可能不是整數

最常見的疑問是:「為什麼 increase() 告訴我這 5 分鐘發生了 2.7 次錯誤?錯誤怎麼會有小數?」

因為 rate 與 increase 都會做外推(extrapolation)。範圍的兩端很少剛好落在抓取的時間點上,所以 Prometheus 會依照已知樣本的斜率,把兩端補到範圍的邊界。它算的是「估計值」,不是精確計數。

這也呼應 ch02 說的:Prometheus 的設計目標是「故障時仍然可用」,不是「絕對精確」——不要拿它當計費依據。

真相②:範圍至少要 4 倍抓取間隔

rate() 至少需要範圍內有兩個樣本點才算得出東西。抓取間隔 15 秒時,如果你寫 [30s],只要有一次抓取失敗或延遲,那個時間點就會沒有結果——圖上出現空洞。

scrape_interval: 15s

rate(x[30s])   # ❌ 太脆弱,一次抓取失敗就斷
rate(x[1m])    # ⚠ 勉強
rate(x[5m])    # ✅ 慣用值,穩定

經驗法則:範圍 ≥ 4 × 抓取間隔。代價是反應變鈍——5 分鐘的範圍會把尖峰抹平,所以告警用的範圍要在「穩定」與「靈敏」之間取捨(ch07 會再回到這件事)。

真相③:它只能用在 counter

  • 對 gauge 用 rate() 沒有意義——gauge 會上下波動,而 rate() 假設它只增不減,遇到下降就當成「counter 重啟」處理,結果完全錯誤。
  • gauge 要看變化用 delta() 或 deriv(),看趨勢用 *_over_time()。

rate 與 irate 的差別

rate(x[5m])    # 範圍內第一個到最後一個樣本,取平均 → 平滑,適合告警與長期趨勢
irate(x[5m])   # 只用最後兩個樣本 → 靈敏,圖形鋸齒,適合看瞬間尖峰

告警一律用 rate,不要用 irate——後者對單一樣本的抖動極度敏感,會製造大量假警報(ch07 的主題之一)。

counter 重啟是自動處理的

服務重新部署時 counter 歸零。rate() 會偵測到「這次的值比上次小」,判定為 reset,並自動補償——所以你不需要自己處理重啟。但這個能力有一個前提,就是下一節的主題。

這兩個寫法看起來只是順序不同,實際上其中一個是錯的:

sum(rate(http_requests_total[5m]))   # ✅ 正確
rate(sum(http_requests_total)[5m:])  # ❌ 錯誤(而且不會報錯)

為什麼順序不能反

1
rate() 依賴「counter 單調遞增」這個性質來偵測重啟——值突然變小就是 reset。
2
而 sum() 會把多條時序加起來,破壞掉這個性質:某一個 pod 重啟歸零時,總和會下降,但其他 pod 還在正常累加。
3
於是 rate() 看到一個「下降但不是歸零」的序列,會把它誤判成一次 counter reset,然後補償一個根本不存在的增量。
結果是:每次滾動更新,你的 QPS 圖上都會出現一個假的尖峰。而多數人會以為那是部署造成的真實流量變化——它不是,它是查詢寫錯了。

正確的心法

「先在每一條時序上各自算變化率,再把變化率加起來。」這個順序在數學上也才是對的:每秒請求數是可以相加的,而累計計數在跨實例聚合之後就失去意義了。

-- 每個服務的 QPS
sum by (job) (rate(http_requests_total[5m]))

-- 錯誤率(分子分母都要先 rate)
sum by (job) (rate(http_requests_total{status=~"5.."}[5m]))
  /
sum by (job) (rate(http_requests_total[5m]))

by 與 without

sum by (job, status) (...)     # 只保留這些 label,其餘全部合併
sum without (instance, pod) (...)  # 只丟掉這些 label,其餘保留
實務上 without (instance, pod) 常常比 by 更好用——因為它會自動保留你之後才加上的新 label,不會因為「忘了把新 label 加進 by 清單」而讓資料被意外合併。

順帶一個除法的坑

算比率時,如果分子在某個時間點完全沒有符合的時序(例如那五分鐘一個 5xx 都沒有),結果不是 0,而是沒有資料——因為向量除法是靠 label 匹配的,分子沒有那條時序就配不起來。

-- 錯誤率為 0 時圖上會斷掉,用 or 補一個 0
sum(rate(errors[5m])) or vector(0)

ch01 說過「分位數不能平均」,ch02 說過「histogram 保留原始 bucket 所以可以正確聚合」。這一節就是把那個「正確聚合」寫出來。

唯一正確的形狀

histogram_quantile(
  0.99,
  sum by (le) (rate(http_server_requests_seconds_bucket[5m]))
)

三件事缺一不可,而且順序固定:

1
先 rate():bucket 是 counter,要先變成每秒速率。
2
再 sum by (le):把各實例的同一個 bucket 加起來。le 一定要保留,因為 histogram_quantile 靠它來重建分佈。
3
最後才 histogram_quantile():從合併後的 bucket 分佈計算分位數。

三種寫錯的方式,以及它們的症狀

寫法症狀
漏掉 by (le)直接報錯或回空——這是最幸運的一種,因為你會發現
忘了先 rate用累計值算分位數,得到的是「服務啟動至今」的分佈,完全不反映當下
avg(histogram_quantile(...))每個實例各算一個 p99 再平均——數字看起來很正常,但它沒有任何意義

bucket 邊界決定了精度

histogram_quantile 只知道「有多少觀測值落在每個桶裡」,桶內的分佈它用線性插值猜。所以:

bucket 邊界:... 0.25, 0.5, 1.0, ...
真實的 p99 落在 0.25 與 0.5 之間

→ 回傳值是在這兩者之間線性插值出來的
→ 如果實際分佈集中在 0.26,插值可能給你 0.4
推論:bucket 邊界要圍繞你關心的門檻來設計。
如果 SLO 是「95% 的請求在 300 毫秒內完成」,那 0.3 就必須是一個 bucket 邊界——這樣「有多少比例 ≤ 0.3 秒」是直接讀出來的精確值,完全不需要插值。

而 ch06 會告訴你:與其算 p99 再跟門檻比,不如直接算「≤ 門檻的比例」——那樣連插值誤差都不存在。

還有兩個邊界情況

  • 分位數落在最後一個有限 bucket 之上(也就是掉進 +Inf):回傳值會是最後一個有限邊界,它是下限而不是真實值。看到 p99 剛好等於你最大的 bucket 邊界,那代表你的 bucket 上限不夠高,真實延遲更糟。
  • 樣本數太少:一分鐘只有 3 個請求時,p99 沒有統計意義。低流量服務看分位數要拉長時間範圍。

ch01 最後說過:可觀測性天生偏向觀測「發生過的事」,而有一整類故障的本質是什麼都沒發生。PromQL 有兩組工具可以部分補這個洞。

absent():這條時序不見了

absent(up{job="order-api"})
# 有資料 → 回空(不觸發)
# 沒資料 → 回 1(觸發告警)

它處理的是本章第一節說的 staleness 問題:服務完全掛掉時,指標不是變成 0,而是消失——所以任何 xxx > 0 的告警都不會響。

-- 這條告警在服務掛掉時「不會」觸發,因為沒有資料
rate(http_requests_total{job="order-api"}[5m]) == 0

-- 這條才會
absent(up{job="order-api"}) or up{job="order-api"} == 0

absent_over_time():一段時間內都沒出現

absent_over_time(batch_job_last_success_timestamp[25h])
# 25 小時內都沒有成功紀錄 → 告警
這是「排程沒有跑」這類故障唯一的抓法。一個每天凌晨跑的批次任務,如果它根本沒被觸發,不會有任何錯誤日誌、不會有任何失敗指標——什麼都不會發生,而那正是問題。

更常用的變形是「多久沒成功了」:
time() - batch_job_last_success_timestamp > 86400

*_over_time:在時間軸上聚合

注意這一族與 sum()/avg() 的差別:聚合運算子是「跨時序」聚合,*_over_time 是「跨時間」聚合。

max_over_time(queue_depth[1h])       # 過去一小時佇列最深到多少(gauge 的尖峰)
avg_over_time(cpu_usage[30m])        # 過去半小時的平均
min_over_time(connections_free[10m]) # 連線池最少剩幾條 —— 這種「最壞值」比平均有用得多

對 gauge 做告警時,用 max_over_time/min_over_time 往往比直接比較當下的值更穩——它天然帶有「持續了一段時間」的意味,可以避免瞬間抖動造成的假警報。

changes 與 resets:偵測不穩定

changes(process_start_time_seconds[1h]) > 3
# 一小時內重啟超過 3 次 → 崩潰迴圈(K8s 的 CrashLoopBackOff)

resets(http_requests_total[1h])
# counter 被重置幾次,同樣可以看出重啟

徵狀

某支核心 API 的月度可靠性報告連續三個月「綠燈」:

  • Grafana 上的 p99 延遲穩定在 180 毫秒,SLO 門檻是 300 毫秒。
  • 錯誤率 0.05%,遠低於 SLO。
  • 但客服每週都收到「查詢很慢、轉圈圈很久」的回報,而且集中在特定幾個大客戶。
  • 工程團隊拿著綠色的儀表板,無法解釋客訴。

診斷

先做唯一能繞過所有查詢語法問題的事:不看儀表板,直接算一個最原始的數字。

-- 過去 7 天,超過 300ms 的請求佔多少比例?(直接讀 bucket,不做分位數)
1 - (
  sum(rate(http_server_requests_seconds_bucket{le="0.3", uri="/api/search"}[7d]))
  /
  sum(rate(http_server_requests_seconds_count{uri="/api/search"}[7d]))
)
-- 結果:0.041  →  4.1% 的請求超過 300ms

4.1% 超標,而儀表板說 p99 是 180ms——兩者不可能同時為真。去看儀表板的查詢:

-- 儀表板上實際寫的
avg(histogram_quantile(0.99, rate(http_server_requests_seconds_bucket[5m])))
     ↑ 對每個實例各算一次 p99,然後把 12 個實例的 p99 平均掉

根因(兩個錯誤疊在一起)

1
分位數被平均了。缺了 sum by (le),變成每個 pod 各算一個 p99,再由外層的 avg 取平均。ch01 說過分位數不能平均——當流量不均勻(某個 pod 承接了大客戶的重查詢)時,那個 pod 的 p99 是 2 秒,但被其他 11 個健康的 pod 平均掉,最後顯示 180ms。
2
bucket 邊界沒有對齊 SLO。預設 bucket 是 ... 0.25, 0.5, 1.0 ...,沒有 0.3 這個邊界。就算查詢寫對了,p99 也是在 0.25 與 0.5 之間插值猜出來的——而 SLO 門檻正好落在那個猜測區間裡,誤差最大的地方剛好是最重要的地方。
兩個錯誤都不會報錯。查詢語法合法、圖畫得出來、數字看起來合理。而它們共同造成的效果是:一個持續三個月、影響 4% 請求的問題,被儀表板系統性地隱藏了。

修法

1
改成唯一正確的寫法:
histogram_quantile(0.99, sum by (le) (rate(http_server_requests_seconds_bucket[5m])))
修正後的 p99 是 1.4 秒。
2
bucket 邊界加上 SLO 門檻(Spring/Micrometer 可以直接指定):
management.metrics.distribution.slo.http.server.requests=100ms,300ms,1s
3
SLI 改成「超標比例」而不是分位數——直接讀 le="0.3" 的 bucket,沒有插值、沒有聚合問題(ch06 會說明為什麼這是更好的 SLI 形式)。
4
把正確寫法固化成 recording rule,儀表板只引用規則名稱,不再讓每個人自己拼查詢(下一節)。
5
加一個 per-pod 的延遲面板:這次的根因之一是「單一實例異常被平均掉」,而 max by (pod) 的視圖能立刻看出這件事。

教訓

綠色的儀表板不是證據,它只是「這個查詢算出來的數字在門檻內」。而當儀表板與使用者的實際體驗矛盾時——相信使用者,並且去檢查那個查詢本身。這是 ch01 那條「症狀優先於原因」在查詢層的版本。

上一節的結論是「不要讓每個人自己拼查詢」。recording rule 就是實現這件事的機制:它讓 Prometheus 定期把一個查詢的結果算好存成一條新的時序。

groups:
  - name: http
    interval: 30s
    rules:
      - record: job:http_requests:rate5m
        expr: sum by (job) (rate(http_requests_total[5m]))

      - record: job:http_request_duration:p99_5m
        expr: histogram_quantile(0.99, sum by (job, le) (rate(http_server_requests_seconds_bucket[5m])))

它解決三個問題

  • 正確性:正確的寫法只寫一次,所有儀表板與告警共用。上一節那個錯誤在 recording rule 的世界裡不會擴散。
  • 查詢速度:儀表板拉 30 天範圍時,不用每次重算幾百萬個樣本點,直接讀預聚合的時序。
  • 降低 cardinality:可以「先聚合、再丟掉原始高基數指標」——保住結論,捨棄明細(ch02 的縮容手段之一)。

命名慣例

level:metric:operations

job:http_requests:rate5m
     ↑聚合到哪一層  ↑原始指標  ↑做了什麼運算

這個慣例的用處是:看到名字就知道它已經被聚合到哪一層了,避免有人拿一條已經 sum by (job) 過的時序再去 sum by (pod)(那會得到空結果,然後花半小時 debug)。

查詢成本:為什麼儀表板會逾時

一個 range query 的成本大致是:

要處理的樣本點數 ≈ (時間範圍 / step) × 符合的時序條數

30 天、step=1m、5,000 條時序
  ≈ 43,200 × 5,000 = 2.16 億個樣本點
  • 拉長時間範圍時,Grafana 會自動放大 step——所以問題通常不在範圍,在時序條數(又回到 ch02 的 cardinality)。
  • 正則匹配很貴:{__name__=~".+"} 這種要掃整個索引,是查詢逾時的常見來源。
  • Prometheus 有 query.max-samples 與逾時保護,被擋下來時的錯誤訊息會直接告訴你樣本數超標——那是在提示你該用 recording rule 了。

subquery:能用但要小心

max_over_time( rate(http_requests_total[5m])[1h:1m] )
# 過去一小時內,5 分鐘速率的最高點

它很方便,但成本高(等於在查詢時跑 60 次內層查詢)。會反覆用到的 subquery,一律改成 recording rule。

這一章的五個安靜錯誤(複習用)

寫法會發生什麼
rate(gauge[5m])把下降當成 counter reset,結果完全錯誤
rate(x[30s])(抓取間隔 15s)圖上出現空洞,被誤讀成「流量沒了」
rate(sum(...))每次滾動更新產生假尖峰
avg(histogram_quantile(...))單一實例的異常被平均掉(本章失敗場景)
sum(rate(x[5m])) 沒限定 job把全公司的服務加在一起

PromQL 回答不了的問題

  • 「是哪一次請求慢」:histogram 只知道有幾次落在某個桶裡。要跳到個案,需要 exemplar 與 trace(ch05)。
  • 「為什麼慢」:PromQL 給的是相關性,不是因果。兩條曲線同時上升不代表其中一條造成另一條。
  • 「這個數字好不好」:p99 = 180ms 是好是壞?沒有 SLO 就沒有答案(ch06)——而本章那場事故正是「沒有基準所以無法反駁儀表板」的後果。
  • 精確計數:rate/increase 會外推,抓取可能失敗。不要拿 Prometheus 當計費或稽核依據。

兩個維運上的代價

  • recording rule 也要成本:它們每個 interval 都會執行一次,規則太多會讓 Prometheus 本身變慢。而且它們產生的新時序同樣佔 cardinality。
  • 查詢會隨著資料成長而變慢:今天很快的儀表板,一年後可能逾時——因為時序條數增加了。這是「儀表板腐化」的一種,而且沒有告警會通知你。

把這一章收成三句

① PromQL 寫錯幾乎不報錯,只會給你一個看起來合理的數字——所以正確寫法要靠 recording rule 固化,不能靠每個人記得。
② 分位數的正確形狀只有一種:histogram_quantile(0.99, sum by (le) (rate(..._bucket[5m]))),而 bucket 邊界要圍繞 SLO 門檻設計。
③ 儀表板與使用者的體驗矛盾時,先懷疑查詢,不要先懷疑使用者。

下一章換一本柱:日誌是三者裡最靈活、也最容易失控的一個——它的成本與流量成正比,而故障時流量最大。

常用函式:吃什麼、給什麼、什麼時候用
函式輸入用在注意
rate()range vector(counter)QPS、錯誤率、吞吐範圍 ≥ 4× 抓取間隔;會外推;只能用於 counter
irate()range vector(counter)看瞬間尖峰只用最後兩個樣本——告警不要用
increase()range vector(counter)「這段期間發生幾次」=rate × 秒數,同樣外推,所以會有小數
histogram_quantile()帶 le 的 instant vector延遲分位數必須先 rate 再 sum by (le)
absent() / absent_over_time()instant / range vector偵測指標消失、排程沒跑抓「沒發生的事」唯一的工具
max_over_time() 等range vectorgauge 的尖峰/最壞值跨『時間』聚合,不是跨時序
changes()range vector重啟次數、崩潰迴圈配 process_start_time_seconds 用
五個不會報錯的寫法
錯誤寫法正確寫法症狀
rate(gauge[5m])delta() / deriv() / *_over_time()把下降誤判成 counter reset,數字完全錯
rate(x[30s]),抓取間隔 15srate(x[5m])圖上空洞,被誤讀成流量消失
rate(sum(...))sum(rate(...))每次滾動更新出現假尖峰
avg(histogram_quantile(...))histogram_quantile(0.99, sum by (le) (rate(...)))單一實例異常被平均掉
sum(rate(x[5m])) 未限定 jobsum by (job) (rate(x{job="..."}[5m]))把不相干的服務加在一起
rate(errors[5m]) == 0 當掛掉告警absent(up{...}) or up{...} == 0服務完全掛掉時指標消失,告警不會響
bucket 邊界怎麼設
情境做法理由
有明確的 SLO 門檻把門檻值設成 bucket 邊界(例如 0.3)「≤門檻的比例」變成精確讀取,完全不需插值
p99 剛好等於最大 bucket 邊界把上限拉高那是下限不是真實值——真實延遲更糟
bucket 太多只留關鍵邊界(含 SLO 門檻)每個 bucket 一條時序,是最大的乘數(ch02)
低流量服務拉長 rate 的時間範圍一分鐘 3 個請求時,p99 沒有統計意義

練習題 點選選項查看解析

0 / 10
01 / 10
為什麼 sum(http_requests_total[5m]) 是錯的,而 sum(rate(http_requests_total[5m])) 是對的?
A 因為 sum 不支援 counter 型別
B 因為聚合運算子只吃 instant vector,而 rate() 把 range vector 轉回了 instant vector
C 因為時間範圍要寫在 sum 外面
D 兩種寫法其實都可以
解析
rate()/increase()/*_over_time() 這類「需要看變化」的函式只吃 range vector;sum()/avg() 這類聚合運算子只吃 instant vector。搞清楚這兩種型別,一半的語法錯誤就消失了。
02 / 10
increase(errors[5m]) 回傳 2.7,錯誤怎麼會有小數?
A 這是 bug,應該回報
B rate 與 increase 都會外推——範圍兩端很少剛好落在抓取時間點上,Prometheus 會依斜率補到邊界,算的是估計值
C 因為有多個實例被平均了
D 因為 counter 發生了重置
解析
這也呼應 ch02 的結論:Prometheus 的設計目標是「故障時仍然可用」而不是「絕對精確」,不要拿它當計費或稽核依據。要精確計數就要用其他系統。
03 / 10
抓取間隔是 15 秒,為什麼 rate(x[30s]) 是危險的寫法?
A 因為 30s 不是 5 的倍數
B 因為 rate 至少需要範圍內有兩個樣本,只要一次抓取失敗或延遲,那個時間點就沒有結果,圖上出現空洞
C 因為 30s 太長會抹平尖峰
D 因為 rate 不支援秒為單位
解析
經驗法則是範圍 ≥ 4 × 抓取間隔,所以 15 秒間隔常用 [5m](或至少 [1m])。代價是反應變鈍——告警的範圍要在「穩定」與「靈敏」之間取捨。
04 / 10
rate(sum(...)) 這種寫法會造成什麼具體症狀?
A 查詢直接報錯
B 每次滾動更新時 QPS 圖上出現假尖峰——因為 sum 破壞了 counter 的單調性,某個 pod 重啟造成總和下降,被 rate 誤判成 reset 而補償了不存在的增量
C 資料會延遲 5 分鐘
D 分位數變得不準確
解析
多數人會以為那個尖峰是部署造成的真實流量變化,它不是——是查詢寫錯了。正確心法是「先在每條時序各自算變化率,再把變化率加起來」,這在數學上也才對:每秒請求數可以相加,累計計數跨實例聚合後就失去意義。
05 / 10
計算 p99 延遲,唯一正確的寫法是什麼?
A avg(histogram_quantile(0.99, rate(x_bucket[5m])))
B histogram_quantile(0.99, sum by (le) (rate(x_bucket[5m])))
C histogram_quantile(0.99, sum(x_bucket))
D quantile(0.99, rate(x_bucket[5m]))
解析
三件事缺一不可且順序固定:先 rate(bucket 是 counter)、再 sum by (le)(le 必須保留,histogram_quantile 靠它重建分佈)、最後才算分位數。選項 A 是把每個實例的 p99 再平均,而分位數不能平均(ch01)。
06 / 10
SLO 是「95% 的請求在 300 毫秒內完成」,bucket 設計上最重要的一件事是什麼?
A bucket 數量越多越好
B 把 0.3 設成一個 bucket 邊界,這樣「≤300ms 的比例」是精確讀取而不是插值猜測
C 把最小的 bucket 設得越小越好
D 使用預設 bucket 即可
解析
histogram_quantile 在桶內做線性插值,如果 SLO 門檻落在兩個邊界之間,誤差最大的地方剛好是最重要的地方。ch06 會更進一步:與其算 p99 再跟門檻比,不如直接算「≤門檻的比例」,連插值誤差都不存在。
07 / 10
p99 的查詢結果剛好等於你最大的那個 bucket 邊界,這代表什麼?
A 延遲非常穩定
B 分位數落在 +Inf 桶裡,回傳值是下限而非真實值——你的 bucket 上限不夠高,真實延遲更糟
C 查詢寫錯了
D 樣本數不足
解析
這是一個容易被誤讀成「還好」的訊號,實際上它在說「我只知道至少這麼慢,但不知道有多慢」。修法是把 bucket 上限拉高,或改用能自動分桶的 native histogram。
08 / 10
服務完全掛掉時,為什麼 rate(http_requests_total[5m]) == 0 這條告警不會觸發?
A 因為告警需要 for 子句
B 因為 Prometheus 有約 5 分鐘的 staleness 規則——服務掛掉後指標不是變成 0 而是「消失」,沒有資料可以比較
C 因為 rate 不能用於告警
D 因為 counter 被重置了
解析
正確寫法是 absent(up{job="..."}) or up{job="..."} == 0。這也是為什麼 absent() 與 absent_over_time() 重要——它們是 ch01 說的「可觀測性看不到沒發生的事」少數的補救工具。
09 / 10
一個每天凌晨執行的批次任務「根本沒被觸發」,要怎麼偵測?
A 監控它的錯誤率
B 用 absent_over_time(batch_last_success[25h]),或 time() - batch_last_success > 86400——因為沒有錯誤、沒有失敗指標,什麼都不會發生
C 檢查 CPU 使用率
D 看日誌有沒有異常
解析
這類故障的本質是「什麼都沒發生」,沒有任何一筆資料會出現。對「應該出現卻沒出現」設告警,是這類故障唯一的抓法。
10 / 10
儀表板顯示 p99=180ms、SLO 門檻 300ms 月月達標,但客訴不斷。第一步該做什麼?
A 請客服提供更多細節再說
B 先用最原始的方式算一次「超過 300ms 的請求佔多少比例」(直接讀 le="0.3" 的 bucket),並檢查儀表板那個查詢本身怎麼寫的
C 增加更多監控指標
D 調高 SLO 門檻
解析
綠色的儀表板不是證據,它只是「這個查詢算出來的數字在門檻內」。本章的失敗場景是兩個錯誤疊加:分位數被 avg 平均掉(掩蓋單一實例的 2 秒延遲)+ bucket 邊界沒有 0.3(插值誤差最大處剛好是門檻)。當儀表板與使用者體驗矛盾時,先懷疑查詢。

點擊卡片翻面查看答案,共 13 張。

QUESTION
instant vector 與 range vector 的差別?它決定了什麼?
點擊翻面
ANSWER
instant 是每條時序的當下一個值,range 是每條時序一段時間內的一串樣本。rate()/increase()/*_over_time() 只吃 range;sum()/avg() 只吃 instant——搞清楚這點,一半的語法錯誤就消失了。
點擊翻回
QUESTION
rate() 的三個真相是什麼?
點擊翻面
ANSWER
①它會外推,所以 increase 可能回傳小數(估計值,不能拿來計費)②範圍至少要 4× 抓取間隔,否則圖上出現空洞 ③只能用於 counter,對 gauge 用會把下降誤判成 reset。
點擊翻回
QUESTION
rate 與 irate 的差別?告警該用哪個?
點擊翻面
ANSWER
rate 取範圍內第一個到最後一個樣本的平均(平滑);irate 只用最後兩個樣本(靈敏但鋸齒)。告警一律用 rate——irate 對單一樣本抖動極度敏感,會製造大量假警報。
點擊翻回
QUESTION
為什麼一定要「先 rate 再 sum」?寫反會怎樣?
點擊翻面
ANSWER
rate 靠「counter 單調遞增」偵測重啟,而 sum 會破壞這個性質(某個 pod 歸零時總和下降)。寫反的結果是每次滾動更新都出現一個假尖峰,而且多數人會誤以為那是真實流量變化。
點擊翻回
QUESTION
by 與 without 該選哪個?
點擊翻面
ANSWER
實務上 without (instance, pod) 常比 by 好用——它會自動保留之後才新增的 label,不會因為「忘了把新 label 加進 by 清單」而讓資料被意外合併。
點擊翻回
QUESTION
計算 p99 的唯一正確寫法是什麼?三個步驟的順序為何?
點擊翻面
ANSWER
histogram_quantile(0.99, sum by (le) (rate(x_bucket[5m])))。①先 rate(bucket 是 counter)②再 sum by (le)(le 必須保留,用來重建分佈)③最後才算分位數。
點擊翻回
QUESTION
histogram 的 bucket 邊界該怎麼設?
點擊翻面
ANSWER
圍繞 SLO 門檻設——門檻值本身要是一個邊界。因為桶內是線性插值,門檻落在兩個邊界之間時,誤差最大的地方剛好是最重要的地方。另外 bucket 越多時序越多(ch02 的最大乘數)。
點擊翻回
QUESTION
p99 剛好等於最大 bucket 邊界,代表什麼?
點擊翻面
ANSWER
分位數落進了 +Inf 桶,回傳的是下限而非真實值——bucket 上限不夠高,真實延遲比顯示的更糟。這是容易被誤讀成「還好」的訊號。
點擊翻回
QUESTION
為什麼「rate(errors[5m]) == 0」抓不到服務完全掛掉?該怎麼寫?
點擊翻面
ANSWER
Prometheus 有約 5 分鐘的 staleness 規則,服務掛掉後指標是「消失」而不是變成 0,沒有資料可比較。要用 absent(up{job="x"}) or up{job="x"} == 0。
點擊翻回
QUESTION
怎麼偵測「排程根本沒被觸發」這種故障?
點擊翻面
ANSWER
absent_over_time(batch_last_success[25h]),或 time() - batch_last_success > 86400。這類故障的本質是什麼都沒發生,沒有錯誤也沒有失敗指標——只能對「應該出現卻沒出現」設告警。
點擊翻回
QUESTION
recording rule 解決哪三個問題?命名慣例是什麼?
點擊翻面
ANSWER
①正確性:正確寫法只寫一次,錯誤不會擴散 ②查詢速度:儀表板不必每次重算 ③降低 cardinality:先聚合再丟掉原始高基數指標。命名用 level:metric:operations,例如 job:http_requests:rate5m。
點擊翻回
QUESTION
Grafana 儀表板逾時,主要跟什麼有關?
點擊翻面
ANSWER
樣本點數 ≈ (時間範圍 / step) × 時序條數。拉長範圍時 Grafana 會自動放大 step,所以問題通常不在範圍而在時序條數——又回到 ch02 的 cardinality。另外正則匹配(尤其 __name__=~)很貴。
點擊翻回
QUESTION
PromQL 回答不了哪四類問題?
點擊翻面
ANSWER
①是哪一次請求慢(要 exemplar 與 trace)②為什麼慢(它給相關性不是因果)③這個數字好不好(要 SLO)④精確計數(會外推、抓取可能失敗,不能當計費依據)。
點擊翻回