TT Lab
はじめる
学ぶ 学習パス コース

可観測性

ラベルにuser_idを入れてはいけない理由

TT Labで続きを見る

一言でいうと

メトリクスの時系列の数は、ラベル値の組み合わせの積です。ラベルを1つ間違えると時系列が数百万本になり、その瞬間、監視システムが監視対象より先に死にます。

なぜ必要なのか

http_requests_totalにラベルを付けます。

http_requests_total{method, status, endpoint}

時系列の数は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. 取りうる値はいくつありますか。数えて答えられなければなりません。「たくさん」は答えではありません。
  2. 時間がたつと増えますか。増えるなら、上限はありますか。
  3. このラベルで実際にアラートを作りますか。ダッシュボードで一度見るかもしれない程度なら、メトリクスではなくログやトレースに置くべきです。

経験則として、ラベル1つあたり値100個以下、メトリクス1つあたりの時系列1万本以下を目標にします。

URLは必ず正規化する

# 나쁨 — 주문 개수만큼 시계열
endpoint="/api/orders/8f3a91"

# 좋음 — 라우트 패턴
endpoint="/api/orders/:id"

このコードブロックの韓国語コメントは、上が注文の数だけ時系列を作る悪い例、下がルートパターンを使った良い例、という意味です。

フレームワークのルート定義を使えば、自動的に正規化されます。自分で文字列を書くと、必ず爆発します。これはミスの問題ではなく、時間の問題です。

3つのシグナルを使い分ける

カーディナリティの高い情報が本当に必要なときがあります。「このユーザーのリクエストがなぜ遅かったのか」のような質問です。それはメトリクスの仕事ではありません。

シグナル カーディナリティ 答える質問
メトリクス 低くなければならない 「どれくらい多く、速く」という傾向とアラート
ログ 高くてもよい 「そのとき何が」という個別の事象
トレース 高くてもよい 「時間はどこで消えたか」という1つのリクエストの経路

メトリクスでアラートを受け取り、時刻を特定して、ログやトレースで個別のリクエストを掘り下げます。3つのシグナルは代替関係ではなく、調査の順序です。

すでに爆発してしまったら

現場での姿

すでに爆発したあとの対処の順序

カーディナリティは、いつも事故が起きてから気づきます。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で止めます。人の注意力に頼ると、忙しい週に必ずまた入り込みます。

次の確認で見ること

続くクイズでは、ラベル値の積から時系列の数を計算し、メトリクスとログ・トレースの役割を区別します。すでに爆発が始まった状況で、収集段階での緩和とアプリケーションの根本的な修正のどちらを先に適用するかも、判断してみてください。