可觀測性與 SLO ch02 Metrics 與 Prometheus 資料模型:一個 label 就能打爆記憶體
下一章→
CH 02 乘法效應

Metrics 與 Prometheus 資料模型:一個 label 就能打爆記憶體

一條時序的定義cardinality 是乘法counter/gauge/histogram/summarypull vs pushexporter 與服務發現命名慣例relabel 與 sample_limit

Prometheus 的資料模型只有一句話:

一條時序 = 指標名稱 + 一組 label 的每一個唯一組合。

這句話看起來平淡無奇,但它是整章的起點——因為「每一個唯一組合」是乘法。上一章說過高基數的東西不能放進 metrics 的 label,這一章要把那句規則變成具體的數字,以及一場把 Prometheus 從 4GB 吃到 30GB 的事故。

而在那之前,還有幾個必須先弄對的基礎,因為它們每一個都有一個很常見的誤用:

  • 四種 metric 型別:counter 與 gauge 的差別大家都知道,但 summary 與 histogram 選錯,會讓你的 p99 從此無法跨機器聚合(ch01 那個「分位數不能平均」的伏筆)。
  • pull vs push:為什麼 Prometheus 選 pull,以及 Pushgateway 不是用來繞過 pull 的。
  • 命名與 label 慣例:_total、_seconds、le 這些後綴不是風格問題,PromQL 的函式會依賴它們。
  • 怎麼在事前擋住暴走的指標:sample_limit 與 metric_relabel_configs。

這一章的乘法思維,跟 db ch03 的 JOIN 是同一種:成本不是「多加一個東西」,而是「乘上一個係數」。認得出這個形狀,就能在寫下那行埋點程式碼的當下,預見它三週後的樣子。

Prometheus 儲存的東西只有一種:時序(time series)。而一條時序的身分由這個組合唯一決定:

指標名稱{label1="值", label2="值", ...}

http_requests_total{method="GET", status="200", service="order"}
http_requests_total{method="GET", status="500", service="order"}
         ↑ 同一個指標名稱,但這是兩條完全獨立的時序

每條時序底下掛的是一串 (timestamp, float64) 的樣本點。「指標」在 Prometheus 裡不是一個東西,是一個家族——真正被儲存、被計費、被吃記憶體的單位是時序。

由此推出的第一件事:成本與流量無關

一個 http_requests_total 計數器,每秒被打 10 次和每秒被打 10 萬次,儲存成本完全相同——因為它就是一條時序,每次抓取只存一個數字。這就是 ch01 說的「metrics 已經被聚合過了」。

第二件事:成本與 label 的組合數成正比

而「組合數」是各個 label 取值數量的乘積,不是總和。
method   5 種(GET/POST/PUT/DELETE/PATCH)
status   8 種
service 20 種

→ 5 × 8 × 20 = 800 條時序
再加一個只有 3 種取值的 region?2,400 條。加一個 user_id?——這就是本章那場事故。

第三件事:記憶體與「活躍時序數」成正比

Prometheus 的 TSDB 把最近的資料放在記憶體中的 head block(約 2 小時後才落成磁碟 block)。每一條活躍時序在記憶體裡都要維護自己的一份 chunk 與索引項,經驗上是每條數 KB 的量級。

10 萬條活躍時序   → 記憶體用量還算舒服
100 萬條          → 開始要認真配置
1000 萬條         → 單機幾乎撐不住

而且「活躍」的定義很寬鬆:一條時序只要在保留窗內出現過就佔著索引。所以即使那些 user_id 的請求早就結束了,它們留下的時序還在——這是事故現場「重啟後記憶體又慢慢漲回去」的原因。

怎麼看自己現在有幾條

# Prometheus 自己的指標:目前 head 裡有幾條活躍時序
prometheus_tsdb_head_series

# 內建的 TSDB 狀態頁(UI 的 Status → TSDB Status,或 API)
/api/v1/status/tsdb
#   會直接列出「時序最多的前 10 個指標名稱」與「基數最高的 label」

這兩個東西要在出事之前就先看過一次,你才知道正常水位長什麼樣。

Prometheus 很少因為「流量太大」而倒下,它幾乎總是死於 cardinality(基數)——時序的條數。

什麼樣的 label 是安全的

判準只有一個:這個 label 的取值範圍,是不是有界而且很小?

label取值數量安全嗎
method、status、env個位數~數十✅ 安全
service、endpoint(用模板)數十~數百✅ 可以,但要注意乘積
pod / instance數十~數千,且會隨部署改變⚠ 小心:每次滾動更新都產生一批新時序
user_id、order_id、trace_id、session_id無上限❌ 絕對不行
完整 URL 路徑、SQL 語句、錯誤訊息實質無上限❌ 絕對不行

兩個容易忽略的「隱形」高基數來源

  • Kubernetes 的 pod label:pod 名稱含隨機後綴,每次部署整批時序全部換新。舊的要等保留期過了才消失——所以頻繁部署的服務,時序數會是「同時存活的版本數 × 每版的時序數」。
  • 錯誤訊息當 label:error="connection refused to 10.2.3.4:5432"——IP 和 port 讓它變成無限集合。要放的是分類(error_type="connection_refused"),不是原文。

histogram 是隱藏的乘數

一個 histogram 不是一條時序,是「bucket 數 + 2」條。
http_request_duration_seconds_bucket{le="0.005"}  ← 每個 bucket 一條
http_request_duration_seconds_bucket{le="0.01"}
... (假設 12 個 bucket)
http_request_duration_seconds_sum                 ← 加一條
http_request_duration_seconds_count               ← 再加一條
                                       共 14 條
而這 14 條還要再乘上其他所有 label 的組合。所以「幫每個端點加一個延遲 histogram」的成本,是「端點數 × 14 × 其他 label 組合」。histogram 很有用,但它是這一章所有數字裡最大的那個乘數。

一條可以直接用的估算公式

時序數 ≈ Σ(每個指標)  各 label 取值數的乘積 × (histogram 就再乘 bucket+2)

記憶體 ≈ 活躍時序數 × 數 KB

在寫下埋點程式碼之前,先在腦中乘一次。這個習慣可以避免這一章 95% 的問題。

counter:只增不減

用於「累計發生了幾次」:請求數、錯誤數、處理的訊息數。永遠不要直接看 counter 的值——它是一個一直長大的數字,本身沒有意義,要用 rate() 看它的變化速度(ch03)。

服務重啟時 counter 會歸零,Prometheus 的 rate() 會自動偵測並處理這個 reset,這也是為什麼命名慣例要求 counter 以 _total 結尾——讓人和工具都知道該用 rate()。

gauge:可上可下

用於「當下的瞬時值」:記憶體用量、佇列長度、連線池的活躍連線數、目前的溫度。它可以直接看、可以做 avg/max/min,但不要對它用 rate()(那沒有意義)。

histogram:把觀測值分桶

用於延遲、回應大小這類「需要看分佈」的東西。它在客戶端做的事很單純:把每個觀測值丟進預先定義好的桶子裡計數。

_bucket{le="0.1"}  → 有幾個觀測值 ≤ 0.1 秒(累積計數)
_bucket{le="0.5"}  → 有幾個 ≤ 0.5 秒
_sum               → 所有觀測值的總和
_count             → 觀測次數

分位數是查詢時用 histogram_quantile() 從這些桶算出來的(ch03 專講怎麼算、以及怎麼算錯)。

summary:在客戶端就算好分位數

summary 直接在應用程式裡算出 p50/p99 並吐出結果。這帶來一個無法補救的後果:它的分位數不能跨實例聚合。

你有 10 個 pod,每個都吐出自己的 p99=200ms。「整個服務的 p99」是多少?——答不出來。把 10 個 p99 平均是錯的(ch01 講過),而你手上沒有原始資料可以重算。

這就是 ch01 那個伏筆的具體成因,而且它是設計上的限制,不是設定問題。

所以怎麼選

  • 幾乎永遠選 histogram。只要服務會有多個實例(在 K8s 上必然如此),summary 的分位數就沒有意義。
  • summary 只適合:單一實例、且你需要非常精確的分位數(histogram 的精度受 bucket 邊界限制)。實務上很少見。
  • histogram 的代價是 bucket 數——所以 bucket 要挑,不是越多越好(ch03 會講怎麼挑)。

順帶一提:native histogram

Prometheus 較新的版本有「原生直方圖」——不用預先定義 bucket,改用指數式自動分桶,能在大幅降低時序數的同時提高精度。它是目前解決 histogram 乘數問題最根本的方向,但要確認你的 Prometheus 與客戶端函式庫版本都支援,且它仍在演進中。

Prometheus 是主動去抓(pull):它按設定的間隔對每個目標的 /metrics 發 HTTP 請求,把當下的文字格式指標抓回來。

# 目標吐出來的東西就長這樣,是純文字
# HELP http_requests_total Total HTTP requests
# TYPE http_requests_total counter
http_requests_total{method="GET",status="200"} 48213
http_requests_total{method="GET",status="500"} 12

pull 的三個實際好處

  • 目標的死活是免費的副產品:抓不到就是抓不到,Prometheus 自動產生 up{job=...} = 0。「服務掛了」這件事不需要另外做——而 push 模型下,「沒收到資料」與「服務沒事只是沒東西可送」無法區分。
  • 可以隨時手動查看:直接 curl localhost:8080/actuator/prometheus 就看得到現在的所有指標,除錯極方便。
  • 控制權在監控端:要調整抓取頻率、要暫時停掉某個目標,改 Prometheus 的設定就好,不用重新部署應用程式。

那目標會變動怎麼辦:服務發現

這是 pull 模型唯一需要解決的問題,而答案是不要寫死目標清單:

scrape_configs:
  - job_name: 'spring-apps'
    kubernetes_sd_configs:      # 從 K8s API 自動發現 pod
      - role: pod
    relabel_configs:            # 只抓有特定 annotation 的
      - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
        action: keep
        regex: "true"

Pushgateway:只給短命的批次任務

最常見的誤用是「我的服務在防火牆後面,Prometheus 抓不到,所以我用 Pushgateway 推出去」——這是錯的用法。那種情況該用網路層方案(代理、隧道)或在該網段部署一台 Prometheus 再做聯邦。

Pushgateway 存在的唯一正當理由是:任務的生命週期比抓取間隔更短。一個跑 5 秒的 cron job,Prometheus 每 15 秒抓一次,永遠抓不到它——所以它在結束前把結果推到 Pushgateway,由 Pushgateway 代為保存。

而它有兩個後遺症要知道:

  • 推上去的值不會自己消失:它會一直停留在最後推送的值。批次任務永遠不再跑了,那個指標還在——所以它同時也失去了 up 這個健康訊號。
  • 它是單點:多個實例推同一組 label 會互相覆蓋。

抓取間隔怎麼定

scrape_interval: 15s   # 常見預設,通常夠用

兩個限制要記著:抓取間隔決定了你能偵測到的最小事件長度(15 秒抓一次,就抓不到持續 5 秒的尖峰);而間隔越短,時序的樣本點越多,儲存與記憶體成本成正比上升。這又是一次代價守恆——解析度是要付錢買的。

Prometheus 的命名慣例看起來像是團隊風格偏好,但其中幾條會直接影響工具能不能正確運作。

三條硬規則

  • counter 以 _total 結尾:http_requests_total。這是人和工具判斷「該不該用 rate()」的依據;OpenTelemetry 轉 Prometheus 格式時也依賴它。
  • 使用基礎單位,並寫進名稱:_seconds 而不是 _milliseconds、_bytes 而不是 _mb。Grafana 與 PromQL 的函式都假設是基礎單位,混用會讓運算結果錯得很安靜。
  • le 是 histogram 的保留 label,quantile 是 summary 的保留 label。不要拿來當自己的 label 名。

一個好指標名的結構

<領域>_<測什麼>_<單位>[_total]

http_request_duration_seconds        (histogram)
http_requests_total                  (counter)
jvm_memory_used_bytes                (gauge)
db_connections_active                (gauge)

什麼該當 label,什麼該當指標名稱

常見的猶豫:要做 http_requests_get_total 和 http_requests_post_total 兩個指標,還是一個指標配 method label?

規則:如果你會想把它們「加起來」,那它就該是 label。
你會想知道「所有 method 的總請求數」,所以 method 是 label。
而 http_requests_total 和 jvm_memory_used_bytes 加起來沒有意義,所以它們是不同的指標。

label 值要穩定

再強調一次上一節的重點,但換個角度:label 的取值集合應該在部署前就是可預測的。如果那個值來自使用者輸入、來自外部系統的回應、或來自任何你無法列舉的來源——它就不該是 label。

// ❌ 端點用實際路徑:每個訂單 id 一條時序
Counter.builder("http_requests_total").tag("uri", "/api/orders/8821")

// ✅ 端點用路徑模板:一條就夠
Counter.builder("http_requests_total").tag("uri", "/api/orders/{id}")

這正是下一節那場事故的成因——而在 Spring 專案裡,這件事有時候是框架幫你做錯的(ch08 會回到 Micrometer 的細節)。

徵狀

某個週三早上,監控系統自己開始出問題:

  • Prometheus 的記憶體兩週內從 4GB 一路漲到 30GB,接著被 OOMKilled。
  • 重啟之後恢復正常,然後幾小時內又慢慢漲回去。
  • Grafana 的儀表板載入超過 30 秒,範圍拉到 7 天直接逾時。
  • 被監控的服務本身完全正常——出事的是監控系統。

診斷

先問「時序是不是變多了」,這是 Prometheus 出問題時的第一個問題:

# Prometheus 自身指標,畫出來看趨勢
prometheus_tsdb_head_series
# 兩週前:18 萬     現在:430 萬   ← 24 倍

接著直接問「是誰貢獻的」——用內建的 TSDB 狀態頁(Status → TSDB Status,或 /api/v1/status/tsdb):

Top 10 series count by metric name:
  http_server_requests_seconds_bucket    3,847,220   ← 元兇
  http_server_requests_seconds_count       320,601
  ...

Top 10 label value counts:
  uri     27,431   ← 兩萬七千個不同的值
  pod        842

uri 有 27,431 個不同的值。撈幾個出來看:

/api/orders/8821
/api/orders/8822
/api/orders/8823
...

根因

1
兩週前上線的新端點,是用手動埋點記錄請求指標的,uri tag 直接取了 request.getRequestURI()——也就是實際路徑,不是路徑模板。
2
每一個被查詢過的訂單 id,都產生一個新的 uri 值。
3
而這個指標是 histogram,預設 14 條時序(12 bucket + sum + count)。
4
再乘上 method(2) × status(5) × pod(6):
27,431 × 14 × 2 × 5 × 6 ≈ 2,300 萬(理論上限)
實際命中的組合 ≈ 384 萬
兩個乘數疊在一起:一個高基數 label × histogram 的 bucket 展開。單獨任何一個都還撐得住,相乘就是 24 倍成長。

而「重啟後又慢慢漲回去」的原因是:時序只要在保留窗內出現過就佔著索引——那些訂單早就查完了,但它們的時序還在。

修法

1
止血(5 分鐘,不用改程式):在 Prometheus 端直接把這個指標丟掉,記憶體立刻回落。
metric_relabel_configs:
  - source_labels: [__name__]
    regex: 'http_server_requests_seconds_bucket'
    action: drop
metric_relabel_configs 作用在「抓取之後、寫入之前」,所以它能擋掉已經產生的爆炸(而 relabel_configs 是抓取之前決定要不要抓這個目標,兩者常被搞混)。
2
治本(當天):埋點改用路徑模板 /api/orders/{id}。uri 的取值數從 27,431 降回 40 幾個。
3
那「我就是想知道某張訂單的延遲」怎麼辦?——那個需求本來就不該用 metrics 解決。訂單層級的資訊要放 trace 與 log(ch01 那條規則),然後用 exemplar 從 metrics 的高延遲點跳到具體的 trace(ch03 設定)。
4
防再犯:在 scrape 設定加上硬上限,讓暴走的目標直接被拒絕而不是拖垮整個系統:
scrape_configs:
  - job_name: 'spring-apps'
    sample_limit: 10000        # 單次抓取超過就整份丟棄並標記失敗
    label_limit: 30
    label_value_length_limit: 200
再對 prometheus_tsdb_head_series 的成長率設告警。

教訓

這場事故的根因是一行看起來完全無害的埋點程式碼。它在測試環境毫無問題——因為那裡只有 20 筆測試訂單,uri 只有 20 個值。高基數問題與資料量無關,與「取值範圍有沒有上界」有關,而這件事在測試環境永遠測不出來。

唯一可靠的防線是在寫下 label 的當下就問:這個值的取值範圍有上界嗎?

接指標的順序(沿用 ch01 的「從使用者旅程往回推」)

1
先開既有的 exporter,不要自己埋。JVM、系統、資料庫、連線池的指標,framework 與 exporter 早就寫好了(Spring Boot 是 micrometer-registry-prometheus + Actuator,ch08 細講)。
2
確認 RED 三個指標齊了:請求數(counter)、錯誤數(同一個 counter 加 status label)、延遲分佈(histogram)。多數框架的預設埋點已經涵蓋。
3
對關鍵資源補 USE:連線池的活躍/等待數、執行緒池佇列長度、佇列積壓量——這些通常要自己補,而它們往往是最早示警的指標。
4
最後才是業務指標:訂單建立數、付款成功率。它們是 SLO 的原料(ch06),但 label 要特別克制。

守住 cardinality 的五道防線

  • 設計時:每加一個 label 就在腦中乘一次。問「這個值的取值範圍有上界嗎」。
  • Code Review:把「不得使用 id 類的值當 metric label」寫成明文規則,它是少數能靠讀 diff 就抓到的效能問題。
  • 抓取時:sample_limit 把暴走的目標擋在門外,metric_relabel_configs 丟掉不要的指標。
  • 監控監控系統:對 prometheus_tsdb_head_series 的成長率設告警(不是絕對值)——本章那場事故從第一天就看得出斜率。
  • 定期盤點:每季看一次 TSDB Status 的 top 10,順手丟掉沒人看的指標。

已經爆掉了怎麼縮

  • 丟掉不需要的 bucket:預設 bucket 常常過細,依 SLO 需要保留幾個關鍵邊界就好(ch03)。
  • 用 recording rule 預聚合,然後把原始的高基數指標丟掉——保住結論,捨棄明細(ch03)。
  • 拉短保留期,或把長期資料交給遠端儲存(Thanos/Mimir 那類),本地只留短期。
  • 拆多台 Prometheus:依團隊或依 job 分片。Prometheus 的擴展方式是水平切分,不是把單機開更大。

什麼時候不要用 metrics

當你想問的問題裡含有「某一筆」「某個使用者」「這一次」,那就不是 metrics 的問題。
metrics 回答的是群體與趨勢;個體的問題屬於 trace 與 log。把個體問題硬塞進 metrics,就是本章那場事故的完整定義。

這一章把 Prometheus 的資料模型講完了,這一節講它不擅長什麼——知道邊界才不會把它用在會受傷的地方。

四個結構性限制

  • 單機、本地儲存、不做叢集。Prometheus 刻意設計成單機可靠(監控系統不該比被監控的系統更複雜)。要長期保留或全域視圖,要外掛 Thanos/Mimir/Cortex 這類方案,那是另一套系統,有自己的維運成本。
  • 不保證精確。抓取可能失敗、樣本可能遺漏、rate() 是外推估算——Prometheus 不適合用來計費或做需要精確數字的稽核。它的設計目標是「在故障時仍然可用」,不是「絕對正確」。
  • 解析度受抓取間隔限制。15 秒抓一次就看不到持續 5 秒的尖峰。這類短暫事件要靠 trace 或事件型資料。
  • 沒有事件的概念。「部署發生了」「設定變更了」這種點事件,metrics 表達不了(實務上用 annotation 或另外的事件系統補)。

這一章的方法看不到的東西

  • 它不會告訴你 label 的值「應該」是什麼。指標可以完全健康,但 status=200 底下回的是錯的內容——ch01 說過,「回 200 但內容錯」是 metrics 天生看不到的。
  • 它不會告訴你是哪一次請求慢。histogram 只知道「有幾次落在 1~2 秒那個桶」,不知道是誰、為什麼——這正是 exemplar 與 trace 要補的洞(ch03、ch05)。
  • 它不會告訴你多少算好。rate(errors[5m]) = 0.003——這個數字好不好?沒有 SLO 就只有感覺(ch06)。

把這一章收成三句

① 一條時序 = 指標名稱 + label 的每個唯一組合,而組合數是乘積——成本與流量無關,與維度有關。
② 幾乎永遠選 histogram 而不是 summary:summary 的分位數無法跨實例聚合,而那是設計限制、事後補不回來。
③ 高基數 label 是唯一真正會殺死 Prometheus 的東西,而它在測試環境永遠測不出來——防線只能設在「寫下那個 label 的當下」。

下一章要用 PromQL 把這些資料變成答案。而它的第一個陷阱,正好是本章 histogram 的延續:你儀表板上那個 p99,很可能從一開始就是錯的。

四種 metric 型別:怎麼選
型別用於查詢方式注意
counter累計次數(請求、錯誤、訊息)一定要用 rate()/increase()命名以 _total 結尾;重啟歸零由 rate() 自動處理
gauge瞬時值(記憶體、佇列長度、活躍連線)直接看,或 avg/max/min不要對它用 rate()
histogram分佈(延遲、回應大小)histogram_quantile() 在查詢時算分位數一個 histogram = bucket 數 + 2 條時序,是最大的乘數
summary同上,但在客戶端算好分位數直接讀 quantile label分位數無法跨實例聚合——多實例服務不要用
label 安全性檢查表
label取值數量安全嗎說明
method / status / env個位數~數十✅ 安全有界且穩定
service / endpoint(路徑模板)數十~數百✅ 可以但要注意與其他 label 的乘積
pod / instance數十~數千,隨部署變動⚠ 小心每次滾動更新產生一整批新時序,舊的要等保留期才消失
user_id / order_id / trace_id無上限❌ 絕對不行放 trace/log,用 exemplar 串回來
完整 URL 路徑實質無上限❌ 絕對不行改用路徑模板 /api/orders/{id}
錯誤訊息原文實質無上限❌ 絕對不行改放分類:error_type="connection_refused"
pull 與 Pushgateway 的分工
pull(預設)Pushgateway
適用長時間運行的服務生命週期短於抓取間隔的批次任務
健康訊號免費得到 up 指標沒有——推上去的值不會自己消失
目標變動服務發現(kubernetes_sd/file_sd)不適用
除錯curl /metrics 直接看要去 Pushgateway 上看
常見誤用—拿來繞過網路限制——該用代理或聯邦,不是 Pushgateway

練習題 點選選項查看解析

0 / 10
01 / 10
在 Prometheus 裡,真正被儲存與計費的單位是什麼?
A 指標名稱
B 一條時序=指標名稱 + 一組 label 的每一個唯一組合
C 每個 scrape 目標
D 每個樣本點
解析
「指標」在 Prometheus 裡不是一個東西,是一個家族。http_requests_total{status="200"} 與 {status="500"} 是兩條完全獨立的時序。這個定義推出所有後果:成本與流量無關(已聚合),但與 label 組合數成正比。
02 / 10
一個指標有 method(5) × status(8) × service(20) 三個 label,會產生幾條時序?再加一個 3 種取值的 region 呢?
A 33 條;加 region 後 36 條
B 800 條;加 region 後 2,400 條——組合數是乘積不是總和
C 800 條;加 region 後 803 條
D 無法計算
解析
5×8×20 = 800,再乘 3 就是 2,400。這個乘法思維與 db ch03 的 JOIN 是同一種:成本不是「多加一個東西」,而是「乘上一個係數」。在寫下埋點程式碼前先在腦中乘一次,可以避免這一章 95% 的問題。
03 / 10
為什麼一個 histogram 是隱藏的乘數?
A 因為它的樣本點比較大
B 因為它不是一條時序,而是「bucket 數 + 2」條(每個 le 一條,加上 _sum 與 _count),而這還要再乘上其他 label 的組合
C 因為它需要在客戶端計算
D 因為它會自動增加 bucket
解析
12 個 bucket 的 histogram 就是 14 條時序起跳。所以「幫每個端點加一個延遲 histogram」的成本是「端點數 × 14 × 其他 label 組合」。histogram 很有用,但它是所有數字裡最大的那個乘數。
04 / 10
為什麼多實例的服務幾乎永遠該用 histogram 而不是 summary?
A 因為 summary 佔用更多儲存空間
B 因為 summary 在客戶端就算好分位數,導致它的分位數無法跨實例聚合——10 個 pod 各自的 p99 無法合成「整個服務的 p99」
C 因為 summary 不支援延遲測量
D 因為 summary 已被 Prometheus 廢棄
解析
把 10 個 p99 平均是錯的(ch01 講過分位數不能平均),而你手上沒有原始資料可以重算。這是設計上的限制,不是設定問題,事後補不回來。histogram 保留了原始 bucket,所以可以在查詢時正確聚合。
05 / 10
Prometheus 選擇 pull 模型,帶來的一個「免費副產品」是什麼?
A 自動壓縮資料
B 目標的死活變成免費的健康訊號(抓不到就是 up=0),而 push 模型下「沒收到資料」與「沒東西可送」無法區分
C 自動服務發現
D 更低的網路頻寬
解析
另外兩個好處是「可以隨時 curl /metrics 手動查看」與「控制權在監控端,調整抓取設定不用重新部署應用」。pull 唯一要解決的問題是目標會變動,答案是服務發現而不是改用 push。
06 / 10
Pushgateway 唯一正當的使用情境是什麼?
A 服務在防火牆後面,Prometheus 抓不到
B 任務的生命週期比抓取間隔更短(例如跑 5 秒的 cron job),Prometheus 永遠抓不到它
C 指標數量太多,pull 會超時
D 需要更即時的資料
解析
網路可達性問題該用代理、隧道或在該網段部署 Prometheus 做聯邦來解決。Pushgateway 有兩個後遺症:推上去的值不會自己消失(因此也失去 up 健康訊號),而且它是單點,多實例推同一組 label 會互相覆蓋。
07 / 10
Prometheus 記憶體暴漲、Grafana 查詢逾時,但被監控的服務完全正常。第一個該查的是什麼?
A 增加 Prometheus 的記憶體配額
B 看 prometheus_tsdb_head_series 的趨勢,再用 /api/v1/status/tsdb 找出是哪個指標與哪個 label 貢獻最多時序
C 縮短抓取間隔
D 重啟 Prometheus
解析
Prometheus 很少死於流量太大,它幾乎總是死於 cardinality。TSDB Status 會直接列出「時序最多的前 10 個指標名稱」與「基數最高的 label」。重啟只是暫時的——時序會慢慢漲回去,因為問題在於資料本身。
08 / 10
要在「不改程式」的前提下立刻止血一個爆炸的指標,該用哪個設定?
A relabel_configs,因為它在抓取前執行
B metric_relabel_configs,它作用在「抓取之後、寫入之前」,可以丟掉已經產生的爆炸指標
C scrape_interval 調長
D storage.tsdb.retention 調短
解析
兩者常被搞混:relabel_configs 是抓取之前決定「要不要抓這個目標、目標的 label 怎麼改」;metric_relabel_configs 是抓回來之後決定「哪些指標/label 要寫進去」。止血用後者,之後再改埋點治本。
09 / 10
為什麼「URI 用實際路徑當 label」這個問題在測試環境永遠測不出來?
A 因為測試環境沒有開啟監控
B 因為高基數問題與資料量無關,與「取值範圍有沒有上界」有關——測試環境只有 20 筆訂單,uri 就只有 20 個值
C 因為測試環境的 Prometheus 版本不同
D 因為測試環境流量太小
解析
這也是為什麼唯一可靠的防線是在寫下 label 的當下就問「這個值的取值範圍有上界嗎」,並把「不得用 id 類的值當 metric label」寫成明文的 Code Review 規則——它是少數能靠讀 diff 就抓到的效能問題。
10 / 10
「我想知道某張特定訂單的處理延遲」,正確的做法是什麼?
A 在延遲 histogram 上加一個 order_id label
B 這個需求不該用 metrics 解決——訂單層級的資訊放 trace 與 log,再用 exemplar 從 metrics 的高延遲點跳到具體 trace
C 改用 summary 型別
D 縮短抓取間隔以提高解析度
解析
當問題裡含有「某一筆」「某個使用者」「這一次」,那就不是 metrics 的問題——metrics 回答群體與趨勢,個體問題屬於 trace 與 log。把個體問題硬塞進 metrics,就是本章那場事故的完整定義。

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

QUESTION
Prometheus 裡「一條時序」的定義是什麼?為什麼這句話是整章的起點?
點擊翻面
ANSWER
指標名稱 + 一組 label 的每一個唯一組合。因為「每個唯一組合」是乘積——成本與流量幾乎無關(已聚合),但每多一個 label,時序數是相乘而不是相加。
點擊翻回
QUESTION
Prometheus 的記憶體用量主要由什麼決定?
點擊翻面
ANSWER
活躍時序數(每條約數 KB 量級),不是樣本數也不是流量。而且時序只要在保留窗內出現過就佔著索引——這是「重啟後記憶體又慢慢漲回去」的原因。
點擊翻回
QUESTION
怎麼查自己現在有幾條時序、是誰貢獻的?
點擊翻面
ANSWER
prometheus_tsdb_head_series 看趨勢;/api/v1/status/tsdb(UI 的 Status → TSDB Status)直接列出時序最多的前 10 個指標名稱與基數最高的 label。要在出事前先看過一次正常水位。
點擊翻回
QUESTION
判斷一個 label 安不安全的唯一準則是什麼?
點擊翻面
ANSWER
它的取值範圍是不是有界而且很小。method/status/env 安全;pod/instance 要小心(每次滾動更新產生一批新時序);user_id、order_id、trace_id、完整 URL、錯誤訊息原文絕對不行。
點擊翻回
QUESTION
為什麼 histogram 是「隱藏的乘數」?
點擊翻面
ANSWER
一個 histogram 不是一條時序,是 bucket 數 + 2 條(每個 le 一條,加 _sum 與 _count),而且還要再乘上其他所有 label 的組合。12 個 bucket 就是 14 條起跳。
點擊翻回
QUESTION
counter 與 gauge 的使用差別?
點擊翻面
ANSWER
counter 只增不減(請求數、錯誤數),永遠要用 rate()/increase() 看,命名以 _total 結尾,重啟歸零由 rate() 自動處理;gauge 是瞬時值(記憶體、佇列長度),直接看或 avg/max/min,不要對它用 rate()。
點擊翻回
QUESTION
summary 的致命限制是什麼?
點擊翻面
ANSWER
它在客戶端就算好分位數,所以分位數無法跨實例聚合——10 個 pod 各自的 p99 合不出「整個服務的 p99」,而且手上沒有原始資料可重算。這是設計限制,事後補不回來。多實例服務一律用 histogram。
點擊翻回
QUESTION
pull 模型帶來的免費副產品是什麼?
點擊翻面
ANSWER
up 指標——抓不到就是 up=0,「服務掛了」不需要另外做。push 模型下「沒收到資料」與「服務沒事只是沒東西可送」無法區分。另外還能隨時 curl /metrics 手動查看。
點擊翻回
QUESTION
Pushgateway 該用在什麼場合?兩個後遺症是什麼?
點擊翻面
ANSWER
只用於生命週期短於抓取間隔的批次任務(跑 5 秒的 cron job)。後遺症:①推上去的值不會自己消失,因此失去 up 健康訊號 ②它是單點,多實例推同一組 label 會互相覆蓋。
點擊翻回
QUESTION
什麼該當 label、什麼該當獨立的指標名稱?
點擊翻面
ANSWER
如果你會想把它們「加起來」,那就該是 label。你會想知道所有 method 的總請求數,所以 method 是 label;而 http_requests_total 與 jvm_memory_used_bytes 加起來沒意義,所以是不同指標。
點擊翻回
QUESTION
relabel_configs 與 metric_relabel_configs 差在哪?
點擊翻面
ANSWER
前者在抓取之前,決定要不要抓這個目標、目標 label 怎麼改;後者在抓取之後、寫入之前,決定哪些指標/label 要寫進去。要止血一個已經爆炸的指標,用 metric_relabel_configs 的 drop。
點擊翻回
QUESTION
守住 cardinality 的五道防線?
點擊翻面
ANSWER
①設計時每加一個 label 就在腦中乘一次 ②Code Review 明文禁止 id 類的值當 label ③抓取時用 sample_limit 與 metric_relabel_configs ④對 prometheus_tsdb_head_series 的成長率設告警 ⑤每季盤點 TSDB Status 的 top 10。
點擊翻回
QUESTION
什麼時候就不該用 metrics?
點擊翻面
ANSWER
當問題裡含有「某一筆」「某個使用者」「這一次」時。metrics 回答群體與趨勢,個體問題屬於 trace 與 log,再用 exemplar 從 metrics 的異常點跳過去。
點擊翻回