关于 prometheus-label-strategy
用于评估和改进 Prometheus 标签设计的指导技能,面向负责应用埋点与抓取配置的人员。重点是在数据产生源头控制基数,同时避免删除区分时间序列的标签。
主要功能
- 01逐项评估标签基数与用途
- 02区分目标标签和样本标签
- 03提供标签一致性检查
- 04说明直方图基数影响
- 05建议高变动元数据使用 info 指标
适用场景
- 01审查 Prometheus 指标标签方案
- 02规划应用埋点以避免高基数
- 03诊断标签删除导致的数据错误
- 04改进直方图和版本元数据设计
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ grafana/ skills --skill prometheus-label-strategy安装后在 Claude Code 中输入/prometheus-label-strategy确认已加载。
详细介绍
Prometheus 标签策略评估器
你是 Prometheus 标签策略方面的专家。当用户要求评估、审计、设计或改进 Prometheus 标签架构,或询问如何从源头防止高基数时,请使用本指南提供结构化、可操作的建议。
本技能旨在从源头防止不良标签——在应用埋点和抓取目标标签中防止它们出现,让它们永远不会进入存储。它不是关于在指标发出后剥离标签:在抓取时删除一个能让序列具有唯一性的标签会悄无声息地破坏数据(见下文唯一规则)。若要降低 Grafana Cloud 中现有序列的成本,请引导用户使用 adaptive-metrics 技能。若要诊断正在发生的基数激增,请引导用户使用 prometheus-cardinality-troubleshooter。
唯一规则:绝不要删除能让序列具有唯一性的标签
抓取时不能删除任何能让序列具有唯一性的标签。 无论是 pod、instance,还是任何能区分不同真实序列的标签都不行。这包括使用 action: labeldrop 的 metric_relabel_configs,以及 Alloy 中与之等效的 prometheus.relabel 规则。
看起来似乎能降低基数。事实并非如此——它会悄无声息且永久地破坏数据:
- 计数器重置会混在一起。 两个 Pod 的计数器合并成一个序列后,它们各自独立的重启会交错出现在合并后的序列中。此时
rate()和increase()会返回垃圾数据——数值往往高得离谱,因为每个 Pod 重启看起来都像一次计数器重置。 - DPM 会膨胀,而不是下降。 多个样本现在会在同一次抓取中落到同一序列上——产生重复样本、乱序错误,并使每分钟样本数膨胀。几周后,人们会来问“为什么我的 DPM 这么高?”或“为什么
rate()返回的数值离谱?”——而且数据中已经没有任何证据能表明问题出在哪里。 - 聚合结果是错误的,而不只是粗略。 对已删除标签执行
sum,会根据合并方式悄无声息地重复计数或少计数。
问题在于,这些情况在配置时都不会报错。流水线会继续运行;只是数值悄然出错,而且事后无法看出问题从何处开始。
按以下顺序使用正确的工具:
- 一开始就不要发出不良标签——修复应用代码。这是唯一一种可以移除标签而不会造成后果的方式,因为该序列从未因该标签而具有唯一性。
- 对于已经流入 Grafana Cloud、且无法从源头修复的序列 → 使用 Adaptive Metrics。 这正是它的用途:它会正确地聚合序列——识别计数器重置、记录审计跟踪,并且可撤销——而不是盲目地剥离标签。请将用户引导至
adaptive-metrics技能。
metric_relabel_configs 有少数几种范围有限且安全的用法(丢弃整个不需要的指标;移除一个与目标标签完全重复的标签)——详见源端预防——但通过删除用于区分序列的标签来降低基数,绝不属于其中一种。
核心概念
序列是 Prometheus 中的基本单位。指标名称与标签键值对的每种唯一组合都会创建一个新的活跃序列。序列过多 = 内存压力、查询缓慢、摄取压力、账单高昂。
基数 = 一个标签可以具有的唯一值数量。某个指标的总序列数 ≈ 各标签基数的乘积。一个包含 path(100 个值)、status_code(10 个值)、method(5 个值)和 instance(50 个值)的指标 = 每个指标 250,000 个序列。再增加一个高基数标签,通常会使数量增加 10–100 倍。
双重影响规则:高基数标签会同时影响两条路径:
- 摄取路径:更多活跃序列 → 更大的头块、更大的 WAL、更多内存、更大的 remote_write 负载、更高的 Grafana Cloud 账单(活跃序列 + DPM)
- 查询路径:PromQL 运算符(
sum by、rate、联接)必须在内存中物化匹配的序列。高基数会使查询内存占用和延迟激增
序列 churn 是隐形杀手。如果某个标签值频繁变化(部署版本、Pod 名称、临时 ID),每次变化都会创建一个新序列,而旧序列还会继续等待过期。每日 churn 达到 100% 意味着,为满足保留要求,你大约要承载稳态序列数量的 2 倍。
评估任何拟议标签时的关键问题:“使用此指标的查询是否会可靠地指定此标签或按此标签聚合?”如果不会 → 就不应该将它设为标签。
标签评估框架
审查标签集时,请根据以下标准评估每个标签。
基数评分
| 标签示例 | 基数 | 结论 |
|---|---|---|
env(prod/staging/dev) | 2–5 个值 | ✅ 良好 |
job(Prometheus 抓取作业) | 5–50 个值 | ✅ 良好 |
cluster、region | 数十个 | ✅ 良好 |
namespace(K8s) | 数十到数百个 | ✅ 可接受 |
service、workload、container | 数十到数百个 | ✅ 可接受 |
instance(host:port) | 数百到数千个 | ⚠️ 需要评估 — 用于实例级指标时没问题,用于聚合指标时有风险 |
pod(K8s) | 数千个 + 临时值 = 高 churn | ⚠️ 对 K8s 监控和序列唯一性而言是必需的 — 请保留。如果 pod 级序列开销太高,请使用 Adaptive Metrics 减少它们;切勿在抓取时丢弃 |
path / route(HTTP) | 使用模板时有界;使用原始 URL 时无界 | ⚠️ 仅限模板化值(/users/:id) |
version、image_tag、git_sha | 每次部署都会增长 → churn | ⚠️ 谨慎使用;可考虑 info 指标模式 |
user_id、request_id、trace_id | 无界 | ❌ 切勿作为标签 — 请使用 exemplars |
customer_id、tenant_id | 通常无界 | ❌ 仅在租户数量较少且固定时可接受 |
error_message、query、sql | 无界文本 | ❌ 切勿使用 |
访问模式匹配
对于每个标签,请思考:
- 针对该指标的查询是否会可靠地按此标签聚合或筛选?
- 该标签是否以符合用户思维方式的逻辑对指标进行划分?
- 移除此标签是否会迫使用户改用 exemplars、日志或追踪?对于少见的查找场景,这是否可以接受?
静态与动态标签值
- 静态 / 目标标签(通过
relabel_configs为每个抓取目标设置一次,例如env=prod、cluster=us-east、team=payments)增加的基数与目标数量成正比,而非请求数量。开销低、价值高,可放心使用。 - 动态 / 样本标签(由应用程序在每次测量时生成,例如
status_code、method、cache_hit)会按值的数量倍增基数。请将可能的值控制在个位数或几十个以内。应用程序代码才是事实来源 — 应在那里修复,而不是在 Prometheus 中修复。
一致性检查
- 各服务之间的标签名称是否一致?(
status、status_code和http_status会产生三个独立的标签系列,导致关联失效) - 标签值是否经过规范化?(
200与"200"、GET与get、Error与error) - 命名约定是否一致?Prometheus 对指标名和标签名都采用
snake_case命名方式 - 相同概念在各服务中是否使用相同名称?(
service、svc和app_name)
直方图桶规范(关键,常被忽略)
每个直方图指标都会将其基础基数乘以 (桶数 + 3)——桶通过 _bucket{le="..."} 表示,另加 _sum、_count 和 _created(Prometheus 2.39+)。
- 默认的
prometheus.DefBuckets包含 11 个桶 → 14 倍乘数 - 一个包含
method、path、status的直方图,原本已有 1,000 个系列,添加直方图基数后会变成 14,000 个系列 - 始终先精简直方图标签的基数——与计数器/仪表相比,标签对直方图的影响大 14 倍
- 考虑使用原生直方图(Prometheus 2.40+),它使用单个稀疏系列,而不是每个桶使用一个系列——这能大幅降低高分辨率延迟跟踪的基数
信息指标模式(适用于频繁变动的元数据)
如果你想了解某个标签(例如 version、git_sha、`image_tag)的值,但不想在每个指标上都承担相应开销,可以使用信息指标:
# A single low-cardinality counter/gauge of value 1, with the metadata attached
app_build_info{app="payment-api", version="2.4.1", git_sha="a1b2c3"} 1
然后在查询时进行关联。经典做法是使用 group_left 进行向量匹配:
sum by (version) (
rate(http_requests_total{app="payment-api"}[5m])
* on (app) group_left (version) app_build_info
)
version 标签只存在于每个构建对应的一个系列上,而不会出现在每个指标上。
info() 函数(更简便的关联方式)
PromQL 的 info() 函数(实验性功能,Prometheus 3.0+;通过 --enable-feature=promql-experimental-functions 启用)会自动执行信息指标关联,无需手动编写 * on (...) group_left (...) 匹配:
info(
rate(http_requests_total{app="payment-api"}[5m]),
{version=~".+"}
)
info(v, [labelselector]) 接受一个范围向量/瞬时向量 v,并为其中的每个系列查找匹配的信息指标,然后添加这些指标的标签。可选的第二个参数是一个标签匹配器,用于限制要附加的信息标签(此处仅附加 version)。默认情况下,info() 会关联常规的 target_info 指标,并根据标识标签(例如 instance、job)进行匹配,因此尤其适用于 OpenTelemetry 风格的 target_info。对于 app_build_info 这类自定义信息指标,上面的显式 group_left 写法仍然具有最佳的可移植性。
如果你使用的是 Prometheus 3.x,且关联的是 target_info,优先使用 info();对于较旧版本、自定义信息指标,或未启用实验性功能标志的情况,则使用显式的 group_left 匹配。
评估输出格式
审计标签集时,请按以下结构生成报告:
## Prometheus Label Strategy Audit
### Summary
[1-2 sentence overall assessment — total estimated active series, biggest risks]
### Per-Label Analysis
| Metric Family | Label | Cardinality | Used in Queries? | Verdict | Action |
|---|---|---|---|---|---|
| http_requests_total | path | Unbounded (raw URLs) | Sometimes | ❌ Remove | Template in code: `/users/:id` not `/users/12345` |
| http_requests_total | pod | High + churn | Rarely | ⚠️ Keep — makes the series unique | If too expensive, aggregate away with Adaptive Metrics; query by `workload` for the common case |
### Histogram-Specific Findings
[Highlight any histograms with high label cardinality — these are 14×+ amplified]
### Estimated Impact
- Active series reduction: [X series → Y series]
- DPM reduction: [X DPM → Y DPM] (samples-per-minute = series × ~6 at 10s scrape)
- Memory impact: [if measurable]
### Recommended Label Set
[Final recommended labels per metric family]
### Implementation Plan
1. [Code changes — instrumentation hygiene: stop emitting bad labels at the source]
2. [Scrape target labels — relabel_configs (additive: env, cluster, team, workload)]
3. [Post-ingest cost reduction on series you can't fix at the source — Adaptive Metrics]
4. [Recording rules to materialize useful aggregates]
推荐的通用目标标签
这些标签应设为目标标签(通过抓取作业中的 relabel_configs 设置,而非由应用程序发出)——它们针对每个目标,基数低,查询价值高:
| 标签 | 用途 | 备注 |
|---|---|---|
job | Prometheus 抓取作业名称 | 由 Prometheus 自动设置 |
instance | 目标端点(host:port) | 自动设置;如有需要,可通过 relabel_configs 将其重命名为更易读的值 |
env | 环境(prod、staging、dev) | 通过 static_configs 标签或服务发现设置 |
cluster | 用于区分多个集群 | 对联邦集群/Mimir 多租户至关重要 |
region | 地理区域 | |
team / squad | 归属团队,也有助于访问控制 | |
service | 逻辑服务标识 | 一个服务可能跨多个作业 |
应用程序不应重新发出这些标签。如果应用程序发出 cluster 标签,它就会与目标标签重复并产生冲突,还会迫使你做出不必要的 honor_labels 选择。
Kubernetes 模式
推荐标签(来自 kubernetes_sd_configs)
| 标签 | 来源 | 备注 |
|---|---|---|
namespace | Pod 元数据 | 始终保留 |
container | Pod 规格 | 基数较低,对多容器 Pod 很有用 |
workload | 派生:{controller_kind}/{controller_name} | 将其作为稳定的聚合键添加到 pod 旁边——静态且可预测。这是新增项,不是替代项:不要以此为由删除 pod |
service | K8s Service | 通过 Service 抓取时使用 |
处理 pod 标签
pod 的基数很高且具有临时性——每次部署和重启都会变化,因此它会主导变化幅度和时间序列数量。但它也是使 K8s 时间序列保持唯一的标签,Kubernetes 监控(按 Pod 归属资源、kube-state-metrics 关联查询)依赖它。适用唯一规则:不要在抓取时删除 pod。 将多个 Pod 合并为一个时间序列会混淆它们的计数器重置,并破坏 rate()。
请改为:
- 通过
relabel_configs将workload({controller_kind}/{controller_name})作为目标标签添加,这样仪表板和告警可以按稳定的工作负载标识聚合(sum by (workload)),而不必动pod。这是增量操作——不会删除任何内容。 - 不要在应用代码中输出
pod——让它由 Kubernetes 服务发现提供,从而确保只有一个事实来源(见下文)。 - 如果在 Grafana Cloud 中按
pod划分的时间序列确实成本过高,请使用 Adaptive Metrics 对其进行削减:它会以正确的方式(摄取后处理、感知计数器重置、可逆)聚合掉pod,而不是在抓取时破坏原始数据。请转到adaptive-metrics技能。
切勿将临时字段映射为标签
uid 会在每次重建 Pod 时重新生成,且没有合理的查询用途。解决方法是永远不要将它映射为标签——不要将它加入 relabel_configs。(除非显式指定,否则它不会出现在默认的 kubernetes_sd_configs 输出中。)不要事后尝试用 labeldrop 删除它——届时它已在区分时间序列,删除它会像删除其他任何唯一标签一样破坏数据。
目标身份的唯一事实来源
instance、pod、node 和 host 应来自抓取目标标签,而非应用代码。如果应用也输出自己的 instance/node,就会出现重复和 honor_labels 冲突。解决方法在于应用本身——停止输出这些标签,而不是在抓取时使用 labeldrop。(删除与目标标签完全重复的标签是唯一的例外;见 metric_relabel_configs。)
kube-state-metrics 标签传播 ⚠️
kube_pod_labels{label_app_kubernetes_io_*=...}可能携带数十个元数据标签- 每种唯一的 Pod 标签组合都会生成一个新的时间序列
- 使用 kube-state-metrics 的
--metric-labels-allowlist在源头加以限制——它控制实际输出的内容,因此这属于预防措施,而非事后破坏性删除
源头预防:应该在哪里修复什么
有五种手段,按优先顺序排列:
1. 在应用中修复(最佳)
应用输出的不良标签是根本原因。例如:
- HTTP 路径:使用模板路由(
/users/:id),而不是原始路径 - 错误指标:使用有限的枚举值(
error_type="timeout"),而不是错误消息字符串 - 按用户划分的指标:不要包含
user_id——使用 exemplars 指向日志/追踪 - 自由格式输入:绝不将用户提供的字符串作为标签值输出
如果你能控制代码,这始终是正确的修复方式。这样可以降低所有下游系统(Prometheus、remote_write、Mimir、Grafana Cloud)的成本。
2. relabel_configs(目标时间重标记)
在抓取之前运行。用于:
- 为发现的目标设置标签(
env、cluster、team) - 丢弃你不想抓取的整个目标
- 将
instance改写为易读的值 - 从服务发现元数据中添加身份信息
scrape_configs:
- job_name: my-app
kubernetes_sd_configs:
- role: pod
relabel_configs:
# Set workload from controller metadata
- source_labels: [__meta_kubernetes_pod_controller_kind, __meta_kubernetes_pod_controller_name]
target_label: workload
separator: /
# Set env from a pod label
- source_labels: [__meta_kubernetes_pod_label_env]
target_label: env
# Only scrape pods explicitly opted in
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
regex: "true"
action: keep
3. 自适应指标(Grafana Cloud——摄取后安全降低基数的方式)
当基数问题源于结构,且你无法在源头修复时——标签确实存在并使时间序列保持唯一,但你只是不需要每个值都保持完整分辨率——自适应指标才是正确的工具,也是降低现有时间序列成本的唯一安全方式。
它在摄取之后运行,作为应用于 Grafana Cloud 的聚合规则。关键在于,它会正确地聚合时间序列:
- 它能正确处理计数器重置,因此
rate()和increase()仍然准确。 - 它会记录聚合的内容,留下审计轨迹——之后你可以回答“为什么发生了这个变化?”
- 它可逆:删除规则后,完整分辨率的时间序列就会恢复。
这就是“数据现在成本更低”(自适应指标)与“数据现在不正确”(抓取时使用 labeldrop)之间的区别。请引导用户使用 adaptive-metrics 技能来设计规则。
4. metric_relabel_configs(仅限范围狭窄且安全的用途)
在抓取之后、存储之前运行。
⚠️ 不要使用
metric_relabel_configs(或 Alloyprometheus.relabel)丢弃区分时间序列的标签——pod、instance、user_id、path,或任何其他标签。 请参阅唯一规则。这看起来像是在修复基数问题,实际却会悄无声息地破坏rate()、增加 DPM,并使聚合结果失真。请改用应用代码(手段 1)或自适应指标(手段 3)。在抓取时规范化标签值(例如将status_code合并为2xx)也同样需要谨慎——这会合并不同的时间序列并产生重复样本错误;应在代码中或通过自适应指标处理,绝不要在此处处理。
真正安全的用途包括:
- 丢弃你永远不想存储的整个指标——你丢弃的是整个指标,而不是将不同的时间序列合并为一个:
metric_relabel_configs: - source_labels: [__name__] regex: my_app_request_details action: drop - 移除与目标标签完全重复的标签。 如果应用程序发出的
cluster/instance已由抓取目标提供,那么目标标签仍能保证唯一性,因此移除重复项不会造成影响。优先修复应用程序,但这是一种安全的临时措施。
安全用途就这些。如果你想用 metric_relabel_configs 来减少时间序列数量,几乎可以肯定应该改用自适应指标。
5. 记录规则(查询时降低基数)
将开销较大的时间序列预聚合为基数较低的记录时间序列。存储的数据点密度相同,但时间序列数量要少得多。
groups:
- name: http-requests-aggregates
interval: 30s
rules:
# Drop pod/instance dimension; keep only service-level rollup
- record: service:http_requests:rate5m
expr: sum by (service, env, cluster, status_code) (rate(http_requests_total[5m]))
针对汇总序列的查询成本会大幅降低。原始时间序列仍然存在——记录规则不会降低摄取成本(应使用自适应指标,而不是抓取时的 labeldrop)。它们降低的是查询成本。
埋点规范(面向应用开发者)
如果用户正在编写埋点代码,请遵循以下规则:
| 规则 | 原因 |
|---|---|
| 切勿将无界用户输入用作标签值 | email、user_id、query string、error message——这是最常见的基数问题 |
| 记录前先对 HTTP 路径进行模板化 | /users/{id},而不是 /users/12345。大多数框架会通过路由元数据实现这一点 |
| 使用小型枚举限制错误标签的取值范围 | error_type="timeout",而不是 error="connection to db-shard-7 timed out at 14:32:09" |
不要在每个指标上都添加 version / git_sha / build_id | 使用 info 指标,并在查询时进行关联 |
不要在代码中输出 pod / node / host | 这些来自抓取目标——重复添加会造成冲突 |
| 避免动态构造标签名称(键) | metric{[user]=1} 无法限制取值范围——请使用固定键 |
| 谨慎使用直方图,并优先精简标签 | 基数扩大 14 倍 |
| 优先使用 exemplars 进行跟踪关联,而非标签 | Exemplars 可携带 trace_id,而不会扩大基数 |
Exemplars(应急方案)
Exemplars 将 trace_id(或任意键值对)附加到特定样本上,而不会将其变成标签维度。这是存放高基数关联数据的理想位置。
需要使用 OpenMetrics 格式、Prometheus 2.26+,并配置抓取配置:
scrape_configs:
- job_name: my-app
enable_protobuf_negotiation: true
# Or for text-format:
follow_redirects: true
并在 Prometheus 服务端进行以下配置:
storage:
exemplars:
max_exemplars: 100000
使用 exemplars 处理以下数据:
trace_id关联(Tempo、Jaeger)- 用于特定调试查询的
request_id - 任何稀疏的“需要时有用”的键
通过 Grafana 的图表 exemplars 功能查询 exemplars,而不要通过 PromQL 聚合查询。
80/20 法则
影响最大的改进几乎总是来自以下五项更改:
- 在应用层移除无界标签——
path(未模板化)、user_id、error_message。这是收益最大的一项。 - 优先精简直方图标签基数——每个直方图的基数都会扩大 14 倍。
- 不要从应用代码中输出
pod/instance/node——让它们来自抓取目标,并添加稳定的workload目标标签用于聚合。(切勿为了降低基数而在抓取时丢弃真实的pod——如果pod级时间序列成本过高,请使用 Adaptive Metrics。) - 使用 info 指标处理
version/git_sha/image_tag——消除部署导致的数据变动。 - 通过
relabel_configs设置目标标签,而不是在应用代码中设置——env、cluster、team、service不应由应用输出。
请先专注于这些改进。
应避免的标签——速查表
| 标签 | 原因 | 替代方案 |
|---|---|---|
user_id、customer_id(租户基数较大时) | 无界 | Exemplars;按 tenant_tier 聚合 |
request_id、trace_id | 无界 | Exemplars |
path / route(原始 URL) | 无界 | 在代码中模板化:/users/:id |
error_message、query、sql | 无界文本 | 取值受限的 error_type 枚举 |
version、git_sha、image_tag(添加到每个指标上) | 每次部署都会变动 | Info 指标模式 |
应用输出的 pod(与 SD 重复) | 应来自 K8s 服务发现,而不是代码 | 停止在代码中输出;保留发现到的 pod。切勿为了降低基数而丢弃真实的 pod——请使用 Adaptive Metrics |
uid(K8s) | 无界;重启后会重新生成 | 一开始就不要将其映射为标签(从 relabel_configs 中排除) |
应用输出的 instance、node、host | 应来自抓取目标 | 停止在代码中输出(抓取时,只有移除与目标标签完全相同的重复标签才是安全的) |
| 动态命名的标签键 | 无法限制取值范围 | 使用键名固定且值受限的标签 |
直方图上的原始 status_code | 基数扩大 14 倍 | 归类为 status_class(2xx、4xx、5xx) |
何时转到其他技能
- “降低我的 Grafana Cloud 账单” / “降低已摄取序列的基数” → 使用
adaptive-metrics技能(摄取后聚合规则——安全且能识别计数器重置的方式;切勿在抓取时通过labeldrop删除具有区分作用的标签) - “哪些指标导致我的 DPM 偏高?” → 使用
dpm-finder技能 - “我的 Prometheus 内存不足,或抓取当前失败” → 使用
prometheus-cardinality-troubleshooter技能 - “如何编写查询来找出有问题的指标?” → 使用
promql技能 - “如何在 Alloy 中配置重新标记规则?” → 使用
alloy技能
此技能负责策略与设计。其他技能负责诊断和运维补救。