mcpField
P
prometheus-cardinality-troubleshooter

prometheus-cardinality-troubleshooter

github.com/grafanaクラウド・DevOps2953.9k 回インストール1 日前に更新
リポジトリ

prometheus-cardinality-troubleshooter について

稼働中の Prometheus で発生するカーディナリティ問題を調べる運用スキルです。性能、メモリ、コストの異常を PromQL と API で段階的に調査し、異なる系列を統合してしまうスクレイプ時のラベル削除を避ける方法も示します。

主な機能

  1. 01症状から原因を絞るトリアージ
  2. 02アクティブ系列と TSDB の分析
  3. 03メトリクス別のラベル調査
  4. 04デプロイとチャーンの調査
  5. 05安全な緩和策の案内
  6. 06PromQL と API の例

活用シーン

  1. 01Prometheus のメモリ増加の調査
  2. 02クエリを遅くするメトリクスの特定
  3. 03系列上限やリモート書き込み問題の診断
  4. 04デプロイ後のカーディナリティ増加の追跡

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/grafana/skills --skill prometheus-cardinality-troubleshooter

インストール後、Claude Code で/prometheus-cardinality-troubleshooterを入力して読み込まれたか確認します。

詳細

Prometheus カーディナリティのトラブルシューティング

Prometheus のカーディナリティ問題をリアルタイムで診断するエキスパートとして対応してください。ユーザーから、カーディナリティが原因と思われる Prometheus のパフォーマンス、メモリ、またはコストの問題が報告された場合は、このガイドを使って体系的に切り分けてください。

このスキルは診断と運用を対象とします。スキーマの設計と予防については、prometheus-label-strategy を案内してください。


修復の前に:守るべき唯一のルール

プレッシャーがかかると、スクレイプ時にカーディナリティの高いラベルを labeldrop したくなります。しないでください。 時系列を一意にするラベルは、スクレイプ時に削除できません。pod、instance、その他、実際の時系列を区別するあらゆるラベルが対象です。止血できたように見えても、実際にはデータを壊します。

  • 異なる時系列のカウンターリセットが統合され、rate() と increase() が誤った値を返します。多くの場合、異常に高い値になります。
  • 1 回のスクレイプで同じ時系列に複数のサンプルが記録され、サンプルの重複/順序不整合エラーが発生し、DPM は減少するどころか増加します。
  • 設定エラーは発生せず、データにも問題の発生箇所を示す証拠が残らないため、破損は気付かれないままです。数週間後、誰かが「DPM がなぜこんなに高いのか/rate() がなぜ異常なのか」と尋ねても、指摘できるものは何もありません。

安全な修復方法は次の 3 つだけです。

  1. 不要なメトリクス全体をドロップする(__name__ に対して action: drop を設定します)。区別される時系列を統合するのではなく、メトリクス全体を破棄します。
  2. 発生源を修正する。アプリケーションが不適切なラベル(上限のない path、user_id など)を出力しないようにします。これが根本的な修正です。
  3. Adaptive Metrics — 発生源では修正できない時系列の構造的なカーディナリティに対応します。正しく集計し(カウンターリセットを考慮し、監査可能で、元に戻せます)、pod のようなラベルのコスト削減に適した方法です。adaptive-metrics を案内してください。

以下で「ラベルをドロップする」とある場合は、必ずこのルールに従って解釈してください。メトリクス全体をドロップする、発生源を修正する、または Adaptive Metrics を使う方法のみとし、区別に使われるラベルを labeldrop してはいけません。


症状 → 考えられる原因

症状考えられる原因最初の対応
Prometheus が OOMKilled になる、またはメモリが直線的に増加するアクティブ時系列の増加(新たに追加された不適切なメトリクスやラベルが原因であることが多い)アクティブ時系列の切り分け
単一の PromQL クエリが遅い、またはクエリ実行エンジンが OOM になるクエリ内の 1 つ以上のメトリクスのカーディナリティが高いクエリ単位の詳細調査
リモート書き込みが遅延し、WAL が増加するサンプルのスループット急増 — 時系列数またはスクレイプ間隔が変化アクティブ時系列の切り分け + スクレイプ間隔を確認
429 Too Many Samples / out of bounds エラーMimir/Cortex のインジェスターにおけるテナントごとの時系列上限に到達メトリクス単位の詳細調査で、新たな原因を特定
Grafana Cloud の Active Series の請求額が急増する新しいメトリクス、新しいラベル、またはカーディナリティの変動を起こすロールアウトメトリクス単位の詳細調査 + 変動を確認
Grafana Cloud の DPM の請求額は急増するが、Active Series は横ばいスクレイプ間隔が短縮された、または remote_write が重複を送信しているDPM 側の問題 — dpm-finder を案内
デプロイ後に series_limit_per_user エラーが発生するアプリケーションの変更により、不適切なラベルが新たに導入された最近の変更差分
再起動のたびに時系列数が増加してからリセットされる一時的なラベル値による時系列の変動変動の診断

ステップ 1:アクティブ時系列の切り分け

概要を示す数値を取得する
# Total active series in the local Prometheus
prometheus_tsdb_head_series

# Or for Mimir / Grafana Cloud Metrics (per tenant)
cortex_ingester_memory_series{user="<tenant>"}

直近の履歴と比較します。

# Growth over the last 7 days
deriv(prometheus_tsdb_head_series[7d]) * 86400

アプリケーション構成が安定しているのに、増加率が 1 日あたり数%を超える場合は危険信号です。

TSDB ステータスエンドポイントを使う

Prometheus には、カーディナリティの内訳を確認できる組み込み機能があります。

curl -s http://prometheus:9090/api/v1/status/tsdb | jq

次の情報が返されます。

  • seriesCountByMetricName — 時系列数が多い上位のメトリクス
  • labelValueCountByLabelName — ユニーク値の数が多い上位のラベル
  • memoryInBytesByLabelName — メモリ使用量が多い上位のラベル
  • seriesCountByLabelValuePair — 時系列数が多い上位のラベルと値のペア

通常、これが「問題のあるメトリクスやラベルはどれか」を見つける最速の方法です。

Grafana Cloud の場合:

# Same endpoint, authenticated against the per-tenant Mimir
curl -s -u "<user>:<token>" \
  "https://prometheus-prod-XX.grafana.net/api/prom/api/v1/status/tsdb" | jq

ステップ 2:出力を読む

時系列数が多い上位のメトリクス
"seriesCountByMetricName": [
  { "name": "http_request_duration_seconds_bucket", "value": 184320 },
  { "name": "go_gc_duration_seconds",               "value": 80 },
  ...
]

ヒューリスティック:

  • ヒストグラム(_bucket)が上位にある場合、ほぼ間違いなく原因です。バケット数に 3 を加えた 14 倍の増幅が発生します。通常は、スクレイプ時にラベルを削除したり、バケット自体を変更したりするのではなく、計測コードで元のヒストグラムのラベルを減らすのが解決策です。
  • 見覚えのないメトリクスが上位 5 件にある場合は、コードベース内を grep してください。本番環境に投入された新しい機能フラグやデバッグ用メトリクスの可能性があります。
  • 同じメトリクスが複数のバリアント(_total、_count、_sum)で表示されている場合は、ヒストグラムまたはサマリーです。実際の影響を把握するには、すべてのバリアントを合算してください。
ユニーク値の数が多い上位のラベル
"labelValueCountByLabelName": [
  { "name": "url",       "value": 84210 },
  { "name": "trace_id",  "value": 41000 },
  { "name": "pod",       "value": 1820 }
]

危険信号:

  • ユニーク値が 1 万件を超えるラベルは、ほぼ確実にバグです。例外は、大規模なフリートで意図的にターゲットごとに設定しているラベルだけです。
  • trace_id、request_id、session_id、query、email、path、url は、ラベルに絶対に使用しないでください。これらはエグザンプラー、ログ、またはトレースに含める情報です。
  • pod の値が数千件ある場合は、チャーンの診断を参照してください。最近のチャーンによって、この数値が増えている場合があります。

ステップ 3:メトリクスごとの詳細調査

疑わしいメトリクスを特定したら、原因となっているラベルを見つけます。

1 つのメトリクスについて、ラベルごとの個別のラベル値数を数える
# How many unique values does each label have on this metric?
count by (__name__) (
  count by (__name__, label_name_here) (
    http_request_duration_seconds_bucket
  )
)

ラベルごとに繰り返すか、ヘルパーを使います。

# Via the Prometheus HTTP API
curl -s "http://prometheus:9090/api/v1/labels?match[]=http_request_duration_seconds_bucket" | jq -r '.data[]' | \
  while read label; do
    count=$(curl -s "http://prometheus:9090/api/v1/label/${label}/values?match[]=http_request_duration_seconds_bucket" | jq '.data | length')
    echo "${count}  ${label}"
  done | sort -rn | head -20
1 つのラベルについて、上位のラベル値を見つける
# Top 20 path values for http_requests_total
topk(20,
  count by (path) (http_requests_total)
)

上位の値に UUID、ハッシュ、タイムスタンプ、または数値 ID が含まれている場合、そのラベルにはソースから上限のない値が設定されています。

グループ化したメトリクスごとの時系列数
# Series-per-instance breakdown — if uneven, one instance is misbehaving
sum by (job, instance) ({__name__=~"my_metric.*"})

ステップ 4:最近の変更との差分を確認

カーディナリティの急増が最近始まった場合、原因はほぼ間違いなく最近の変更です。現在の状態と変更前の状態を比較します。

現在と昨日のメトリクス一覧

Grafana Cloud のカーディナリティダッシュボードを使うか、次のコマンドを実行します。

# Current metrics
group by (__name__) ({__name__!=""})

# Compare to last week (offset)
group by (__name__) ({__name__!=""} offset 7d)

外部で差分を確認します。1 週間前にはなかったメトリクスが seriesCountByMetricName の上位にある場合、それが原因です。

デプロイと照合する
# Active series correlated with build_info
prometheus_tsdb_head_series
# Overlay with:
changes(app_build_info[1d])

デプロイと時系列数の垂直な段差が一致していれば、原因は確定です。


ステップ 5:チャーンの診断

チャーンが高い状態とは、時系列が失効するよりも速く作成され、破棄されていることを意味します。症状として、時系列数が増え続けた後、Prometheus の再起動時に急激に減少します。

チャーンの兆候
# Series created vs. removed per second
rate(prometheus_tsdb_head_series_created_total[5m])
rate(prometheus_tsdb_head_series_removed_total[5m])

# Ratio of churned to live
prometheus_tsdb_head_series_created_total / prometheus_tsdb_head_series

作成率が削除率を大幅に上回る状態が続いている場合、カーディナリティは一方的に増加しています。よくある原因は次のとおりです。

原因手がかり
Pod のロールアウトで pod ラベルが出力されるチャーンの急増がデプロイのタイミングと一致し、Pod 検出を使用するスクレイプに影響する
すべてのメトリクスに version/git_sha/image_tag ラベルがある多数のメトリクスで、デプロイのたびにチャーンが急増する
instance に一時的なホスト名が設定されているクラウドのオートスケーリングイベントのタイミングと一致する
バグ:動的なラベル名チャーンが増え続け、頭打ちにならない
アプリケーションのバグで、新しい UUID がラベルとして出力されるデプロイとの関連性がなく、増加が止まらない
チャーンのメモリへの影響
# A churn-driven head block carries old series until tsdb compaction
prometheus_tsdb_head_chunks
go_memstats_heap_inuse_bytes{job="prometheus"}

Prometheus を再起動すると、チャーンによって増えた時系列は削除されますが、解決にはなりません。ソース側で修正する必要があります。


よくある原因の一覧

ヒストグラムの急増

見分け方:seriesCountByMetricName の上位に *_bucket メトリクスがあります。倍率は約 14 倍です。

修正方法:

  1. まず、ヒストグラムのラベルを発生源で減らします。ラベルを 1 つ削除するごとに、系列数を 14 分の 1 にできます。計測コードで path、method、または status_code を削減してください(スクレイプ時に labeldrop しないでください。異なるヒストグラムが統合され、バケットが破損します)。すでに Grafana Cloud にある系列で変更できないものは、Adaptive Metrics で集約してください。
  2. 次に、適切であればバケット数を減らします(カスタムバケットとデフォルトを比較してください)。
  3. 高解像度のレイテンシー追跡には、ネイティブヒストグラム(Prometheus 2.40 以降)を検討してください。単一のスパース系列でバケット系列群を置き換えられます。
kube-state-metrics のラベル爆発

見分け方:kube_pod_labels または kube_pod_annotations が上位にあり、label_* または annotation_* ラベルがカーディナリティを押し上げています。

修正方法:kube-state-metrics を --metric-labels-allowlist と --metric-annotations-allowlist で設定してください。デフォルトでは、すべてのラベルとアノテーションを系列として出力します。

# kube-state-metrics flags
--metric-labels-allowlist=pods=[app,team,version]
--metric-annotations-allowlist=pods=[checksum/config]
新しいエンドポイントによるパス/ルートの急増

見分け方:http_requests_total(またはフレームワークの同等メトリクス)が一晩で 10 倍以上に増えています。topk(20, count by (path) (http_requests_total)) に、/users/123456 のような値が何百件も表示されます。

修正方法:本質的な修正は、アプリケーションコードでパスをテンプレート化すること(/users/:id)です。ユーザーを prometheus-label-strategy に案内してください。すでに Grafana Cloud にある系列は、Adaptive Metrics で path を正しく集約できます。adaptive-metrics に案内してください。

リラベルの replacement ルールで path を「正規化」してはいけません。スクレイプ時に /users/123、/users/456、…を 1 つの /users/:id の値に統合すると、異なる系列が統合され、サンプル重複エラーや壊れた rate() が発生します。統合は発生源(テンプレート化)または取り込み後(Adaptive Metrics)に行う必要があり、スクレイプ時に行ってはいけません。

本番環境の障害を今すぐ止める必要があり、テンプレート化をまだデプロイできない場合、スクレイプ時に安全にできる唯一の対処は、問題のメトリクス全体を削除することです(コードの修正が適用されるまで完全に失われます。これは意図的なトレードオフであり、気付かないままデータを破損させるものではありません)。

# Emergency: drop the whole metric until the source is templated
metric_relabel_configs:
  - source_labels: [__name__]
    regex: http_requests_total
    action: drop
本番環境でアプリがデバッグメトリクスを出力している

見分け方:上位 10 件に見覚えのないメトリクスがあります。ソースコードを検索してください。開発者が出力制御を忘れた _details または _per_request デバッグメトリクスであることがよくあります。

修正方法:スクレイプ時に完全に削除します。

metric_relabel_configs:
  - source_labels: [__name__]
    regex: my_app_request_details
    action: drop

チームにチケットを起票し、コードから削除するよう依頼してください。

アプリが出力するラベルとターゲットラベルの衝突

見分け方:1 つのジョブの系列数が、想定の数倍になっています。系列を確認すると、アプリが出力した instance=... とターゲットの instance=... が衝突して、予期しない値になっています(Prometheus は、競合した方の名前を exported_instance に変更します)。

修正方法:適切な修正はアプリケーション側で行います。コードから instance/node/host を出力しないようにしてください。これらはスクレイプターゲットに属するラベルです。ターゲットラベルが優先されるよう、honor_labels が false(デフォルト)であることを確認してください。

スクレイプ時の暫定措置が必要な場合は、ターゲットラベルと完全に重複するラベルに限り削除できます。これが安全な唯一の labeldrop です。ターゲットラベルによって一意性が保たれるためです。重複している名前だけに厳密に範囲を限定し、pod(または一意性の根拠となっているその他のラベル)は絶対に含めないでください。

# Stopgap ONLY for app-emitted duplicates of target labels.
# Drops the `exported_*` collisions — NOT pod, which makes K8s series unique.
metric_relabel_configs:
  - regex: exported_(instance|node|host)
    action: labeldrop

その後、relabel_configs のターゲットラベルが正しく適用されます。アプリ側の修正を優先してください。

フェデレーションによるカーディナリティの増幅

見分け方:フェデレーションされた Prometheus または Mimir のグローバルビューで、想定を大幅に上回る系列が表示されます。各ソースに独自の cluster/region ラベルがあり、数が増幅しています。

修正方法:これは通常、想定どおりです。フェデレーションは設計上、ソースラベルを保持します。系列数が多すぎる場合は、生のメトリクスではなく、集約済みの記録ルールのみをフェデレートしてください。

- job_name: federate
  honor_labels: true
  metrics_path: /federate
  params:
    'match[]':
      - '{__name__=~".*:.*"}'  # Recording-rule naming convention only

修正方法の判断ツリー

Cardinality fire confirmed
│
├── Need to stop the bleeding NOW (production OOM, ingest 429s)
│   └── Drop the ENTIRE offending metric via metric_relabel_configs (action: drop on __name__)
│       (also applies to Alloy/Agent — same syntax)
│       Do NOT labeldrop a distinguishing label — it breaks the data, see "The One Rule".
│       Then schedule the proper fix.
│
├── It's a Grafana Cloud Active Series bill issue, not a perf issue
│   ├── Cardinality is structural and you can't fix the app
│   │   └── Route to `adaptive-metrics` skill (post-ingest aggregation rules — the safe way)
│   └── You want metric-by-metric DPM breakdown
│       └── Route to `dpm-finder` skill
│
├── It's a fixable application bug (unbounded label, debug metric in prod)
│   ├── Short-term: drop the whole metric at scrape, OR aggregate via Adaptive Metrics
│   └── Long-term: fix in code; route to `prometheus-label-strategy` for design guidance
│
├── It's histogram cardinality
│   ├── Reduce labels on the underlying histogram AT THE SOURCE (14× win per label)
│   ├── Reduce bucket count if appropriate
│   └── Consider native histograms for high-resolution latency
│
└── It's churn (deploy-driven)
    ├── Stop EMITTING `version`/`git_sha`/`instance` from app code (use info-metric for version)
    ├── Keep `pod` — never drop it; if pod-level series are too costly, use Adaptive Metrics
    └── Verify K8s SD relabel rules aren't mapping in `uid` or other ephemeral fields

緊急時のドロップパターン(コピーしてすぐ使えます)

これらは、スクレイプ時に実行できる安全な緊急措置です。不要なメトリクスを全体ごとドロップします。異なる系列を統合しないため、データを破損しません。

⚠️ 意図的に、識別用ラベルの labeldrop と 値を正規化する relabel は含めていません。どちらも異なる系列を統合し、rate()/DPM を壊します(唯一のルールを参照)。メトリクス全体をドロップせずにカーディナリティを下げるには、ソースを修正するか、Adaptive Metrics(adaptive-metrics にルーティング)を使用してください。安全な labeldrop は、ターゲットラベルと完全に重複するラベル(例:exported_instance)を削除する場合のみです。アプリが出力するラベルとターゲットラベルの衝突を参照してください。

Prometheus の scrape_configs の場合:

metric_relabel_configs:
  # Drop a specific bad metric entirely
  - source_labels: [__name__]
    regex: bad_metric_name
    action: drop

  # Drop a set of debug/temporary metrics by name prefix
  - source_labels: [__name__]
    regex: debug_.*
    action: drop

Grafana Alloy の場合(prometheus.relabel コンポーネント):

prometheus.relabel "drop_bad_metric" {
  forward_to = [prometheus.remote_write.default.receiver]

  rule {
    source_labels = ["__name__"]
    regex = "bad_metric_name"
    action = "drop"
  }
}

必ず最初にステージング環境でテストし、スクレイプ時にドロップするよりも、ソースを修正するか Adaptive Metrics を使用してください。


引き継ぎ先

  • 「再発を防ぐためのラベル戦略を設計したい」 → prometheus-label-strategy
  • 「これらのメトリクスを維持しながらコストを削減したい」 → adaptive-metrics
  • 「DPM の観点で最もコストが高いメトリクスはどれ?」 → dpm-finder
  • 「これを特定する PromQL を書いて」 → promql
  • 「これを Alloy で設定して」 → alloy
  • 「Loki が遅いのはなぜ?」 → loki-label-analyzer(別のシステムですが、問題の種類は同じです)

このスキルの担当範囲は、緊急時の診断です。予防、設計、取り込み後のコスト最適化は、別の場所で扱います。

コメント