為什麼從 Metrics 談起#
監控(Monitoring)是橫跨多個世代、用來確保系統穩定運作的維運技術,而 Metrics 一直都是它的核心關注。對 Observability 來說,Metrics 同樣是不可或缺的基石。
特別是在實踐 SRE(Site Reliability Engineering)時,常見的概念都圍繞著 Metrics 在打轉:
- SLI(Service-Level Indicator):服務水準指標。
- SLO(Service-Level Objective):服務水準目標。
- SLA(Service-Level Agreement):服務水準協議。
這些概念協助我們定義「什麼是重要的指標」、「指標要達到什麼水準才算合格」、「怎麼對指標做出承諾」。彼得・杜拉克的名言「If you can’t measure it, you can’t manage it」放在這裡再貼切不過。
一個指標包含哪些要素#
以「量體溫」這個動作為例,思考指標通常需要包含哪些元素:
- 指標名稱:例如「體溫」。
- 數值:實際量到的數字。
- 時間:什麼時候量的;同一天可能會多次測量。
- 補充資訊:被測量者的姓名、組別等屬性。
把多次量測串起來,就能得到「跨時間」的體溫變化。這種隨時間累積的資料就是時間序列資料(Time Series Data)。
時間序列資料通常會存放在專為它設計的時序資料庫(Time Series Database, TSDB)中。相較於一般資料庫,TSDB 針對寫入與時間範圍查詢進行了優化,因為這類資料幾乎不會被修改,主要的存取行為就是「寫入」與「依時間範圍讀取」。
Prometheus Metrics 的四個要素#
本系列在討論 Metrics 時主要以 Prometheus Metrics 為例。一筆 Prometheus 指標由四個部分組成:
- 指標名稱:說明指標內容,常見會把單位帶進名稱中,例如
process_time、memory_usage_byte。 - 時間:以 Unix timestamp 表示,單位為毫秒。
- 數值:以 Float64 表示。
- 補充資訊:多組 Key-Value Pair Label,例如
os=ubuntu、ip=1.1.1.1。
應用程式可透過 Prometheus Client Library 自行生成 Metrics;對於沒辦法直接埋點的服務或機器,可以使用對應的 Exporter 採集後轉成 Prometheus 格式。
收集則分為 Pull Model(Prometheus 定期抓取 HTTP Endpoint)與 Push Model(透過 Pushgateway 中介),詳細運作與儲存、查詢部分留到下一章 Prometheus 中說明。
Cardinality 與 Label 設計#
前一節提到 Prometheus 指標可以掛上多組 Label。Label 是 Metrics 的維度(Dimension),讓你可以依 os、method、status_code 切片查詢。但 Label 也是 Metrics 系統最容易「爆炸」的地方,根源在於一個概念:基數(Cardinality)。
在時序資料庫(TSDB)的世界裡,每一組唯一的「指標名稱 + Label 值組合」就是一條獨立的時間序列(Time Series)。系統的負擔不是看你打了多少個 Metrics,而是看你產生了多少條時間序列:
$$ \text{時間序列總數} = \sum_{i}\left(\text{指標}_i \text{的所有 Label 值組合數}\right) $$
對單一指標而言,若各 Label 的取值彼此獨立,組合數近似於各 Label 基數的乘積:
$$ N_{\text{series}} \approx \prod_{j=1}^{k} |L_j| $$
其中 $|L_j|$ 是第 $j$ 個 Label 的不同取值數量。乘法的可怕之處在於,多加一個高基數 Label,序列數不是加上去、而是乘上去。
一個會爆炸的例子#
假設一個 HTTP 服務的請求數指標 http_requests_total,原本掛三個低基數 Label:
| Label | 取值數量 | 說明 |
|---|---|---|
method | 5 | GET / POST / PUT / DELETE … |
status | 10 | 200 / 404 / 500 … |
endpoint | 20 | 路由樣板(已正規化) |
此時序列數約為 $5 \times 10 \times 20 = 1{,}000$ 條,完全在可控範圍。
現在有人為了「方便追查特定使用者」,加上一個 user_id Label,而系統有 100 萬名活躍使用者:
$$ 5 \times 10 \times 20 \times 1{,}000{,}000 = 10^{9} \text{ 條時間序列} $$
一個原本只有一千條序列的指標,瞬間膨脹到十億條。這就是典型的基數爆炸(Cardinality Explosion)。
警告:永遠不要把無界(Unbounded)或高基數的識別碼放進 Label。 最常見的三大地雷是
user_id、request_id、trace_id——它們的取值幾乎等於請求數,等於把「每一筆請求」都變成一條獨立序列。 後果是 TSDB 的記憶體與磁碟被序列數量拖垮,查詢與告警評估全面變慢,最終整個 Metrics 後端可能 OOM 崩潰。
高基數的三重成本#
高基數 Label 同時在三個維度上付出代價:
- 儲存成本:每條序列都需要獨立的索引與資料區塊(Chunk),序列數量直接決定磁碟用量。
- 查詢成本:PromQL 查詢需要先比對 Label 索引找出符合的序列,序列越多、索引越大,查詢越慢。
- 記憶體成本:Prometheus 對活躍序列(Active Series)維護記憶體中的索引與 Head Block,這是基數爆炸最先壓垮的環節,常以 OOM 收場。
什麼算「高基數」#
基數沒有絕對門檻,但有實用的經驗線:
- 低基數(安全):取值穩定且有界,通常數十個以內,例如
method、status、region。 - 中基數(留意):數百到一兩千,例如正規化後的
endpoint、pod名稱——可接受,但要留意它與其他 Label 相乘的結果。 - 高基數(危險):約 >100–1000 個取值,且會隨流量持續增長而沒有上界,例如
user_id、email、session_id、request_id、trace_id、完整 URL(含 query string)。
判斷一個 Label 該不該存在,可以走這條決策路徑:
flowchart TD
A["想新增一個 Label"] --> B{"取值是否有界?<br/>(能列舉、不隨流量無限成長)"}
B -- "否(無界)" --> X["不要放進 Label<br/>改用 Log 欄位或 Trace 屬性"]
B -- "是(有界)" --> C{"取值數量大致 < 100~1000?"}
C -- "否" --> X
C -- "是" --> D{"乘上既有 Label 後<br/>序列總數仍可接受?"}
D -- "否" --> X
D -- "是" --> Y["可以作為 Label"]
X --> Z["若需要靠它跳轉到單筆請求,<br/>改用 Exemplar 橋接 trace_id"]用 Exemplar 橋接,而不是把 trace_id 變成 Label#
最常見的需求是:「我看到延遲尖峰,想直接跳到那筆慢請求的 Trace。」直覺做法是把 trace_id 加進 Label——但前面已經說明,這會直接引爆基數。
正確做法是使用 Exemplar。Exemplar 是附加在某個指標樣本上的「範例參照」,典型用途就是夾帶一筆代表性請求的 trace_id。它不會成為時間序列的一部分,因此不增加基數,卻能讓你從 Metrics 圖表上的某個資料點,一鍵跳轉到對應的 Trace。
- 把
trace_id當 Label:序列數 × 請求數,基數爆炸。 - 把
trace_id當 Exemplar:序列數不變,仍可從圖上點進該筆 Trace。
這正是 Metrics 與 Traces 之間的橋樑,Histogram 類型的指標(如延遲分佈)特別適合掛 Exemplar。對應的查詢與關聯細節,留待 05 應用篇的 Signal Correlation 展開。
下面用 PromQL 觀察自己系統的基數狀況,這是排查 Metrics 後端壓力時的常備工具:
# 每個指標名稱目前各有多少條時間序列(找出膨脹的「重災區」指標)
topk(10, count by (__name__)({__name__=~".+"}))
# 估算某個指標掛了哪些 Label、各自的基數有多大
count by (endpoint) (http_requests_total)
# 全系統活躍序列總數(Prometheus 自我監控指標)
prometheus_tsdb_head_series
# 對 Histogram 指標查詢時帶出 Exemplar(trace_id 即藏在其中)
# 需在 Grafana 面板開啟 Exemplars,或 API 帶上 ?exemplars=true
histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))延伸:高基數真的完全不能要嗎?
並非所有高基數需求都該被否決,而是該被「放對地方」:
- 如果你需要依高基數維度(如
user_id)做聚合分析,但不需要每個值各一條長期序列,可考慮支援高基數的後端或事件型分析系統,而非塞進 Prometheus Label。 - 如果你需要的是單筆請求的可追溯性,那它本來就屬於 Logs(結構化欄位)或 Traces(Span 屬性)的職責範圍,用 Exemplar 把三者串起來即可。
- Mimir / Cortex / Thanos 等水平擴展方案能撐住更高的序列規模,但它們解決的是「容量」問題,不會讓糟糕的 Label 設計變正確——基數紀律仍是第一道防線。
換句話說:高基數資訊不是不能存,而是不該存在「會被乘進時間序列總數」的 Label 維度上。
小結#
本章只是把 Metrics 與 Prometheus 的整體輪廓拉出來。後續章節會逐一深入:
- Prometheus 本身的設計與 PromQL。
- 各式 Exporter 怎麼補齊「沒辦法自己埋點」的監控對象。
- Mimir / Cortex / Thanos 等長期儲存方案。
- 兩個在 Metrics 領域深耕已久的工具:StatsD 與 Zabbix。
原文出處#
- 原書/iThome:https://ithelp.ithome.com.tw/articles/10321021