iT邦幫忙

2026 iThome 鐵人賽

DAY 10
0
Kubernetes

從看得到到看得懂:30 天在自架 K8s 上實踐可觀測性與告警系列 第 10

Day 10:PromQL 起步:rate、histogram_quantile 與四個黃金訊號

  • 分享至 

  • xImage
  •  

昨天做圖的時候直接貼了兩行查詢沒解釋,今天補上。

PromQL 是 Prometheus 的查詢語言。它跟 SQL 完全不像,因為它處理的東西不一樣:SQL 查的是一張表,PromQL 查的是一堆隨時間變化的曲線

這篇要從資料型別開始講,那是理解 rate 的前提。

今天的故障開關:只開故障二

kubectl set env deploy/pricing BUG_SILENT_DISCOUNT=false LEAK_KB_PER_REQUEST=0
kubectl set env deploy/catalog BUG_N_PLUS_ONE=true

最後一節要看長尾的形狀,需要故障二把它製造出來。

今天的查詢我都貼在 Prometheus 自己的網頁,不是 Grafana——介面很陽春,但是試語法最快,改一改按 Execute 就看得到結果。

kubectl port-forward -n monitoring svc/kps-kube-prometheus-stack-prometheus 9090

這個視窗要一直開著,另外開一個終端機放它。瀏覽器打開 localhost:9090,點導覽列的 Query

先分清楚三種指標型別

Prometheus 的指標有好幾種型別,先認識三種就夠用:

型別 特性 例子 怎麼看它
Counter(計數器) 只會增加,重啟時歸零 累計請求數、累計錯誤數 幾乎永遠要包一層 rate
Gauge(量測值) 可上可下 記憶體用量、佇列長度 直接看數值
Histogram(直方圖) 把數值分桶統計 請求耗時的分布 histogram_quantile

型別搞錯,寫出來的查詢就是錯的,而且 Prometheus 不會報錯,它會安安靜靜給你一個沒有意義的數字。

為什麼 counter 一定要包 rate

假設 checkout_total 現在顯示 1048576。

這個數字沒有任何意義。 它是從服務啟動到現在的累計值,我不知道它是一天累積的還是一個月累積的,也不知道現在是變快還是變慢。

有意義的是它增加得多快

rate(checkout_total[5m])

意思是「過去五分鐘內,這個計數器平均每秒增加多少」,得到的就是「每秒幾筆請求」。

rate 還順便處理了一件麻煩事:counter 會歸零。Pod 一重啟計數器就從 0 開始,如果我自己拿現在的值減去五分鐘前的值,會得到一個巨大的負數。rate 認得出這種重置並且會自動修正。

這件事在故障三特別重要。pricing 每四分鐘就 OOM 重啟一次,計數器跟著歸零,用 rate 的話圖表不會出現假的斷崖。

rate 和 increase 差在哪

  • rate(x[5m])每秒的平均增加量
  • increase(x[5m]) → 這五分鐘總共增加了多少

increase 大約等於 rate 乘上秒數。畫圖幾乎都用 rate,因為單位固定,時間視窗改了圖形也不會跳動;想回答「今天總共發生幾次」這種問題才用 increase

中括號裡的時間要填多少

[5m] 是回看視窗。有一條實用的規則:

視窗至少要是抓取間隔的四倍。

我的 ServiceMonitor 設 interval: 15s,所以視窗至少要 1m,實務上填 5m 比較穩。

為什麼?因為視窗太短的話,裡面可能只有一兩個資料點,遇到一次抓取失敗就整段變空白,圖上出現破洞。這個坑很常見,而且症狀是「圖時有時無」,第一次遇到會以為是服務出問題。

histogram_quantile 那行在幹嘛

昨天那行完整長這樣:

histogram_quantile(0.95,
  sum(rate(http_request_duration_seconds_bucket{job="gateway", path="/checkout"}[5m])) by (le)
)

由內往外拆成四層。

第一層,..._bucket histogram 型別的指標會自動產生一組以 _bucket 結尾的序列,每一條記錄「耗時小於等於某個值的請求有幾筆」,那個「某個值」放在標籤 le 裡面(le 就是 less than or equal)。所以原始資料長得像:小於 0.1 秒的有 950 筆、小於 0.5 秒的有 990 筆、小於 1 秒的有 998 筆⋯⋯

第二層,rate(...[5m]) bucket 也是 counter,所以一樣要包 rate。

第三層,sum(...) by (le) 把所有 Pod 的資料加總,但是要保留 le 這個標籤。保留是必要的,因為下一步要靠不同的 le 值去推算分布,le 被加掉就沒有分布可言了。忘記寫 by (le) 是最常見的錯誤。

第四層,histogram_quantile(0.95, ...) 從這個分布推算出 95% 的位置。

這裡要注意一件事:這個結果是估算值,不是精確值。 它假設每個桶裡面的資料是平均分布的,實際上不見得。如果桶的邊界設得不好,例如全部請求都落在同一個桶裡,算出來的 P95 會離譜。桶要怎麼設是後面幾天的題目。

四個黃金訊號

有了語法,接下來的問題是該查什麼。Google 的 SRE 書提出四個指標,叫做四個黃金訊號(The Four Golden Signals):

訊號 中文 回答的問題
Latency 延遲 回應要多久?
Traffic 流量 有多少需求打進來?
Errors 錯誤 失敗的比例是多少?
Saturation 飽和度 系統離塞爆還有多遠?

前三個用我們手上的指標就查得出來:

# Traffic:每秒請求數
sum(rate(checkout_total[5m]))

# Errors:錯誤率
sum(rate(checkout_total{status="error"}[5m])) / sum(rate(checkout_total[5m]))

# Latency:P95
histogram_quantile(0.95,
  sum(rate(http_request_duration_seconds_bucket{job="gateway", path="/checkout"}[5m])) by (le))

Saturation 是四個裡面最難的,因為每種資源的飽和指標都不一樣:記憶體看用量比例、CPU 看等待時間、佇列看長度、連線池看等待中的請求數。以記憶體為例:

sum(container_memory_working_set_bytes{pod=~"pricing.*", container="pricing"})
  / sum(container_spec_memory_limit_bytes{pod=~"pricing.*", container="pricing"})

這兩個指標不是我埋的,是 kubelet 自己回報的,kube-prometheus-stack 裝好就有。故障三那個記憶體洩漏就是一個 saturation 的問題,後面設告警的時候會回來用這條查詢。

一個我做不到的細節

黃金訊號在講延遲的時候有一個很重要的提醒:成功和失敗的請求要分開算

理由是一個「很快失敗」的請求,例如 50 毫秒就回 500,會把平均延遲拉低,讓你誤以為系統變快了。實際上它只是變得更快地失敗。

但是我現在做不到這件事。因為我的 histogram 是這樣定義的:

request_duration = Histogram(
    "http_request_duration_seconds", "HTTP 請求耗時", ["service", "path"],
)

只有 servicepath 兩個標籤,沒有 status 所以我沒辦法只算成功的請求。

要做到的話,得把 status 加進標籤裡。但是這件事不是免費的,而且代價比想像中大。

因為一個 histogram 不是一條序列,是一整組。 我的桶是這樣設的:

buckets=[0.01, 0.05, 0.1, 0.2, 0.3, 0.5, 1.0, 2.0, 5.0]

9 個桶,加上一個 Prometheus 自動補的 +Inf,再加上 _sum_count 兩條,一組標籤組合就是 12 條序列。相比之下,checkout_total 那種 counter,一組標籤組合只有 1 條。

而且標籤之間是相乘不是相加。現在 gateway 有 /checkout/healthz 兩個路徑,就是 2 × 12 = 24 條。加上 status 之後,假設會出現 200、422、500 三種,變成 2 × 3 × 12 = 72 條,一口氣翻了三倍。

72 條當然沒什麼,我的叢集跑在筆電上也撐得住。真正的問題是這個乘法在真實系統裡會失控:如果哪天有人把使用者 ID 加進標籤,一百萬個使用者就是一百萬乘以 12,一千兩百萬條序列。Prometheus 的記憶體大致跟「活躍序列數」成正比,這就是 Day 3 提過的 cardinality 爆炸。

所以「要不要多加一個標籤」永遠是一個取捨:多一個切面,乘上去的成本是整組桶。這個題目後面講指標設計的時候會專門談。

三個新手常犯的錯

一、對 counter 直接取值

checkout_total              # ✗ 一個沒意義的累計值
rate(checkout_total[5m])    # ✓

二、對 gauge 用 rate

rate(container_memory_working_set_bytes[5m])   # ✗

container_memory_working_set_bytes 是「這個容器現在用了多少記憶體」,用完會釋放,所以這個數字會上會下——這種就是 gauge。跟 counter 的差別在於,counter 只進不出(累計請求數不可能變少),gauge 是當下的狀態(記憶體、佇列長度、連線數)。

那怎麼知道一個指標是哪種?/metrics 就好,Day 8 那張截圖裡每個指標上面都有一行 # TYPE

# TYPE python_gc_objects_collected_total counter
# TYPE python_info gauge

還有一個慣例可以參考:counter 的名字通常以 _total 結尾,gauge 沒有這個習慣。

用錯的後果是這樣:rate 假設輸入只會增加,所以遇到數字下降時,它會判斷成「服務重啟、計數器歸零了」,然後把那段當成重置處理。記憶體本來就會一直上上下下,於是它會不斷誤判,算出一個完全沒有意義的數字。而且不會報錯,你會得到一條看起來很正常的線。

三、忘記 by (le)

histogram_quantile(0.95, sum(rate(x_bucket[5m])))          # ✗
histogram_quantile(0.95, sum(rate(x_bucket[5m])) by (le))  # ✓

錯的那行會發生什麼事?sum(...) 沒有 by (le) 的話,會把所有桶加成一個數字。原本「小於 0.1 秒的有 950 筆、小於 0.5 秒的有 990 筆⋯⋯」這個分布被壓成一個總和,分布就不存在了。

histogram_quantile 需要靠 le 標籤才能推算百分位數,拿到一個沒有 le 的東西,它算不出來,結果是空的

注意:不是紅字錯誤,是圖上什麼都沒有。第一次遇到我以為是資料還沒進來,等了很久才發現是查詢寫錯。這三個錯的共同點也在這裡——沒有一個會告訴你哪裡錯了

練習:用 PromQL 看見埋在裡面的故障

故障二已經打開了,現在跑這三行:

histogram_quantile(0.50, sum(rate(http_request_duration_seconds_bucket{job="gateway", path="/checkout"}[5m])) by (le))
histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket{job="gateway", path="/checkout"}[5m])) by (le))
histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket{job="gateway", path="/checkout"}[5m])) by (le))

我是在 16:00 左右打開故障的,三張圖都可以看到那個轉折點。

P50

https://ithelp.ithome.com.tw/upload/images/20260910/201805708gi32pte20.png

P95

https://ithelp.ithome.com.tw/upload/images/20260910/20180570J8ZwRp3ZYC.png

P99

https://ithelp.ithome.com.tw/upload/images/20260910/20180570dSFbAn0Vi7.png

把三個數字排在一起看:

故障前 故障後 倍數
P50 0.075 秒 約 0.081 秒 1.08 倍
P95 0.10 秒 約 0.82 秒 8 倍
P99 0.10 秒 約 0.96 秒 10 倍

這就是長尾的形狀。 一半的使用者幾乎沒感覺,慢了 6 毫秒;但是最慢的那 1% 從 100 毫秒變成快一秒。

如果我只看平均值,會覺得系統「好像稍微變慢了一點」,完全不會警覺。

有一個細節值得說:P50 其實也動了一點,從 75 毫秒變成 81 毫秒。照理說 N+1 只影響超過 5 件的大購物車,也就是兩成的請求,P50 不該受影響才對。

我猜是這樣:那兩成的大購物車現在每筆要打 12 次 pricing,pricing 整體變忙了,所以連走批次路徑的小購物車也跟著慢了幾毫秒。故障會外溢,不會乖乖待在它該待的地方——這件事光看程式碼是想不到的。

另外注意這三張圖的 Y 軸:一張最大 0.083,另外兩張到 1.00。Prometheus 每張圖都自動縮放,所以三張分開看,反而看不出彼此的比例。這就是為什麼昨天要把三條線放進同一張 Grafana 面板——形狀要靠並排才看得出來。

不過看到形狀之後,我還是答不出為什麼——是哪個服務、哪一段慢。那要等 trace。

小結

  • Counter 幾乎永遠要包 rate,gauge 絕對不要
  • rate 是每秒速率,increase 是區間總量;時間視窗至少要抓取間隔的四倍
  • histogram_quantile 記得 by (le),而且它算出來的是估算值
  • 四個黃金訊號:延遲、流量、錯誤、飽和度

明天講另外兩套方法論 USE 和 RED,以及它們跟四個黃金訊號的關係。這三套講的東西有一大半重疊,但是適用的對象不一樣。


上一篇
Day 9:Grafana 接上來:做一張能回答問題的圖,而不是好看的圖
系列文
從看得到到看得懂:30 天在自架 K8s 上實踐可觀測性與告警10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言