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调查部署变更与序列 churn
  5. 05安全的缓解建议
  6. 06PromQL 与 API 示例

适用场景

  1. 01调查 Prometheus 内存增长
  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() 会返回垃圾结果,往往是高得离谱的值。
  • 每次抓取有多个样本落到同一时间序列上 → 出现重复样本/乱序错误,DPM 反而会膨胀,而不是降低。
  • 这种破坏是无声的(不会出现配置错误),数据中也没有任何线索能说明问题出在哪里。几周后有人问“为什么我的 DPM 这么高/为什么 rate() 离谱?”,却无从查起。

唯一安全的补救方式是:

  1. 丢弃整个不需要的指标(对 __name__ 使用 action: drop)——丢弃的是整个指标,而不是合并不同的时间序列。
  2. 修复来源——让应用停止生成有问题的标签(这是修复无界 path、user_id 等标签的真正方法)。
  3. Adaptive Metrics——适用于无法从源头修复的时间序列结构性基数问题。它能正确聚合(感知计数器重置、经过审计且可逆)。这是降低 pod 等标签成本的正确方法。请转至 adaptive-metrics。

下文凡是提到“丢弃标签”,都应按这条原则理解:丢弃整个指标、修复来源,或使用 Adaptive Metrics——绝不要使用 labeldrop 移除用于区分时间序列的标签。


症状 → 可能原因

症状可能原因首要操作
Prometheus 被 OOMKilled,或内存持续线性增长活跃时间序列增长(通常由新增的有问题指标或标签导致)活跃时间序列排查
单个 PromQL 查询缓慢或导致查询器 OOM查询中的一个或多个指标具有高基数逐指标深入排查
远程写入滞后,WAL 持续增长样本吞吐量激增——时间序列数量或抓取间隔发生变化活跃时间序列排查 + 检查抓取间隔
429 Too Many Samples/out of bounds 错误触及 Mimir/Cortex 每租户的摄取器时间序列上限逐指标深入排查,找出新增的问题来源
Grafana Cloud 活跃时间序列账单激增新增指标、新增标签或导致数据 churn 的部署逐指标深入排查 + 检查 churn
Grafana Cloud DPM 账单激增,但活跃时间序列数平稳缩短了抓取间隔,或 remote_write 发送了重复数据DPM 相关问题——转至 dpm-finder
部署后出现 series_limit_per_user 错误应用变更引入了新的问题标签近期变更差异
时间序列数量增长后,每次重启都会重置临时标签值导致时间序列 churnChurn 诊断

步骤 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

对于稳定的应用集合,每日增长率超过几个百分点就是危险信号。

使用 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)几乎总是问题所在——它们有 14× 的乘数(桶数量 + 3)。通常应在源头(检测代码中)减少底层直方图的标签,而不是在抓取时剥除标签,也不要改动桶本身。
  • 前 5 名中有不认识的指标 → 在代码库中搜索它;它很可能是新功能标志,或是已发布到生产环境的调试指标
  • 同一指标以多个变体出现(_total、_count、_sum)——这是直方图或摘要指标;应将所有变体的数量相加,才能了解实际影响
按唯一值数量排序的标签
"labelValueCountByLabelName": [
  { "name": "url",       "value": 84210 },
  { "name": "trace_id",  "value": 41000 },
  { "name": "pod",       "value": 1820 }
]

危险信号:

  • 任何唯一值超过 10K 的标签几乎肯定是错误的。唯一例外是大型集群中有意设置的每目标标签。
  • trace_id、request_id、session_id、query、email、path、url——这些绝不应该作为标签。它们应该放在范例、日志或跟踪中。
  • pod 的值有数千个——参见变动诊断;近期频繁变动往往会抬高这个数字

第 3 步:深入分析单个指标

确定可疑指标后,找出导致问题的标签。

统计某个指标每个标签的不同值数量
# 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
查找某个标签的前几项标签值
# 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)

在外部进行比较。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. 首先,在源头减少直方图的标签——每移除一个标签,就能减少 14× 的序列。在埋点代码中精简 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_* 标签导致基数膨胀。

修复方法:使用 --metric-labels-allowlist 和 --metric-annotations-allowlist 配置 kube-state-metrics。默认情况下,它会将所有标签和注释都作为序列发出。

# 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。

不要通过 relabel replacement 规则来“规范化” path——在抓取时将 /users/123、/users/456……合并为一个 /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

创建工单,要求相关团队从代码中移除该指标。

应用发出的标签与目标标签冲突

判断方法:某个 job 的序列数量远超预期。查看单个序列时,你会发现应用发出的 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 标签,导致数量倍增。

修复方法:这通常是预期行为——联邦设计上会保留源标签。如果序列数量过高,只联邦聚合后的 recording rules,而不是原始指标:

- 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 丢弃用于区分序列的标签,也不使用归一化值的重标记。这两种做法都会合并不同的序列,并破坏 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(不同的系统,同一类问题)

这项技能专注于高压下的诊断。预防、设计和摄取后成本优化由其他工具负责。

评论

支持 Markdown