ラベルにuser_idを入れてはいけない理由
一言でいうと
メトリクスの時系列の数は、ラベル値の組み合わせの積です。ラベルを1つ間違えると時系列が数百万本になり、その瞬間、監視システムが監視対象より先に死にます。
なぜ必要なのか
http_requests_totalにラベルを付けます。
http_requests_total{method, status, endpoint}
method: 5通り(GET、POST、PUT、DELETE、PATCH)status: 8通りendpoint: 40通り
時系列の数は5 × 8 × 40 = 1,600本です。問題ありません。
ここに「ユーザーごとに見たい」という要望が来て、user_idを追加します。ユーザーが10万人なら、
5 × 8 × 40 × 100,000 = 1億6,000万本です。
Prometheusは、時系列ごとにメモリ上にインデックスを保持します。サーバーがOOMで死に、すると障害を観測する手段が、障害のせいで失われます。
カーディナリティ爆発を起こすラベル
絶対にラベルに入れてはいけないものです。
| ラベル | なぜ危険か |
|---|---|
user_id, session_id |
ユーザー数の分だけ増えます。上限がありません |
request_id, trace_id |
リクエストごとに一意です。無限です |
email, ip |
事実上無限で、さらに個人情報です |
timestamp |
時刻ごとに新しい時系列ができます(時系列に時刻を入れるという矛盾です) |
url(クエリ文字列を含む) |
?page=1、?page=2……と無限です |
| エラーメッセージの全文 | メッセージに値が混ざると無限です |
特に最後のものです。error="connection to 10.0.3.17:5432 timed out"のようにIPやポートが入ったメッセージをラベルにすると、組み合わせが爆発します。error="db_timeout"のように、分類された値を使います。
判断基準: このラベルの値は何通りか
ラベルを追加する前に、自分に問いかけます。
- 取りうる値はいくつありますか。数えて答えられなければなりません。「たくさん」は答えではありません。
- 時間がたつと増えますか。増えるなら、上限はありますか。
- このラベルで実際にアラートを作りますか。ダッシュボードで一度見るかもしれない程度なら、メトリクスではなくログやトレースに置くべきです。
経験則として、ラベル1つあたり値100個以下、メトリクス1つあたりの時系列1万本以下を目標にします。
URLは必ず正規化する
# 나쁨 — 주문 개수만큼 시계열
endpoint="/api/orders/8f3a91"
# 좋음 — 라우트 패턴
endpoint="/api/orders/:id"
このコードブロックの韓国語コメントは、上が注文の数だけ時系列を作る悪い例、下がルートパターンを使った良い例、という意味です。
フレームワークのルート定義を使えば、自動的に正規化されます。自分で文字列を書くと、必ず爆発します。これはミスの問題ではなく、時間の問題です。
3つのシグナルを使い分ける
カーディナリティの高い情報が本当に必要なときがあります。「このユーザーのリクエストがなぜ遅かったのか」のような質問です。それはメトリクスの仕事ではありません。
| シグナル | カーディナリティ | 答える質問 |
|---|---|---|
| メトリクス | 低くなければならない | 「どれくらい多く、速く」という傾向とアラート |
| ログ | 高くてもよい | 「そのとき何が」という個別の事象 |
| トレース | 高くてもよい | 「時間はどこで消えたか」という1つのリクエストの経路 |
メトリクスでアラートを受け取り、時刻を特定して、ログやトレースで個別のリクエストを掘り下げます。3つのシグナルは代替関係ではなく、調査の順序です。
すでに爆発してしまったら
- どのメトリクスかを探します。PrometheusのTSDBステータスページに、時系列の多いメトリクスとラベルの順位が出ます。
- 収集の段階で落とします。relabel設定で問題のラベルを削除するか、そのメトリクス自体をdropします。アプリケーションのデプロイを待つ必要はありません。
- アプリケーションを直します。根本的な解決は、ラベルを付けないことです。
- 保持期間とシャーディングを調整します。急場をしのぐための手段であり、解決策ではありません。
現場での姿
- Prometheusが定期的にOOMになる → 新しく追加されたラベルを疑います。
- ダッシュボードのクエリが30秒かかる → 時系列が多すぎてスキャン範囲が大きくなっています。
- デプロイ直後にメモリが階段状に上昇する → そのデプロイでラベルが追加されています。
すでに爆発したあとの対処の順序
カーディナリティは、いつも事故が起きてから気づきます。Prometheusがメモリを使い切って死んだり、クエリがタイムアウトして返ってこなかったりします。そのときの順序があります。
まず、何がどれだけ占めているかを数えます。Prometheusのステータス画面(/tsdb-status)に、最も多くの時系列を作ったメトリクスとラベルが出ます。コマンドでも見られます。
topk(10, count by (__name__)({__name__=~".+"}))
count(app_request_duration_seconds_bucket)
count(count by (user_id)(app_request_total))
最後の行が、そのラベルの実際の値の種類数です。この数字が数千なら、そのラベル1つが原因です。
防ぐのは、保存の直前に行います。アプリケーションを直してデプロイするのが正解ですが、時間がかかります。その間は、スクレイプ設定でラベルを消すか、メトリクスごと捨てます。
metric_relabel_configs:
- source_labels: [__name__]
regex: 'app_request_total'
target_label: user_id
replacement: '' # 라벨 값을 비운다
- source_labels: [__name__]
regex: 'debug_.*'
action: drop # 이 지표는 아예 저장하지 않는다
すでに保存された時系列は、自然には消えません。新しく入ってこなくなっても、保持期間のあいだ残ってメモリを使います。急ぐなら、管理APIで消します(--web.enable-admin-apiが有効になっている必要があり、消すと元に戻せません)。
ヒストグラムは、静かに倍増します。バケット1つが、そのまま時系列1本です。バケット12個のヒストグラムにラベル3つを掛けると、あっという間に数万になります。ラベルを減らせないなら、先にバケットの数を減らします。
同じ間違いを防ぐのは、デプロイ前の検査です。メトリクス名とラベルの一覧をコードから抜き出し、許可リストにないラベルが付いたらCIで止めます。人の注意力に頼ると、忙しい週に必ずまた入り込みます。
次の確認で見ること
続くクイズでは、ラベル値の積から時系列の数を計算し、メトリクスとログ・トレースの役割を区別します。すでに爆発が始まった状況で、収集段階での緩和とアプリケーションの根本的な修正のどちらを先に適用するかも、判断してみてください。