概述

先说个场景:凌晨三点,你被告警吵醒,爬起来打开 Grafana,发现几十个 Dashboard 里找不到一条有用的信息。指标倒是有几百万条——但全是垃圾。

这不是个例。我见过太多团队,Prometheus 部署完就不管了,指标只增不减,告警规则越写越多,最后监控系统自己先挂了:Prometheus 内存 OOM、查询超时 30 秒、告警评估延迟 5 分钟以上。监控系统成了最大的故障源,这话说出去都讽刺。

监控数据治理解决的就是这个问题。它不是什么高深理论,说白了就一句话:搞清楚你有哪些指标、哪些有用、哪些该删、怎么管它们的一生

这篇文章从指标生命周期管理的角度,拆解监控数据治理的五个阶段:指标定义、采集策略、存储优化、质量度量、废弃淘汰。每个阶段都配实操代码和踩坑经验。

指标爆炸的根因:不是数据多了,是管不住

指标为什么会膨胀

指标膨胀不是一夜之间发生的。通常的路径是这样的:

  1. 初期:Node Exporter + cAdvisor,几百条指标,Prometheus 跑得好好的
  2. 业务接入:每个服务埋点,每个中间件装 Exporter,指标涨到几万条
  3. 高基数炸弹:有人把 user_idrequest_idsession_id 塞进了标签,时序数据量从几万条直接爆炸到百万级
  4. 灾难:Prometheus 内存飙升、磁盘写满、查询卡死

这里的核心罪魁祸首是高基数标签

Prometheus 的时间序列模型是:指标名{标签1="值1", 标签2="值2"} → 数值。每增加一个标签值组合,就多一条时间序列。举个例子:

# 低基数:3 条时间序列
http_requests_total{method="GET",status="200"} 
http_requests_total{method="POST",status="200"}
http_requests_total{method="GET",status="404"}

# 高基数炸弹:100万用户 = 100万条时间序列
http_requests_total{method="GET",status="200",user_id="12345"}
http_requests_total{method="GET",status="200",user_id="12346"}
...

第二行代码看起来人畜无害,但如果 user_id 有 100 万个值,就是 100 万条时间序列。Prometheus 单实例承载上限大约 50 万条 TimeSeries(受内存和磁盘 I/O 约束),直接超限。

高基数标签的常见来源

来源典型场景危害程度替代方案
用户 IDhttp_requests_total{user_id="..."}致命用日志记录,或聚合为百分位
请求 IDhttp_request_duration{trace_id="..."}致命用 Jaeger/Zipkin 追踪
URL 路径http_requests_total{path="/api/user/12345/orders"}严重路由模板化:path="/api/user/:id/orders"
容器名container_cpu_usage{name="k8s_pod_xyz_abc"}中等用 namespace + deployment 替代
时间戳event_time="2026-07-12T01:00:00"致命永远不要用时间戳做标签

有一个简单的判断标准:如果一个标签的可能取值超过 100 个,它就不该是标签,该是日志里的字段

指标定义规范:从源头控制

建立企业级指标目录

指标治理的第一步不是删数据,而是搞清楚你有什么。建议维护一个指标目录(Metric Catalog),记录每个指标的归属、用途、基数、保留周期。

# metric-catalog.yaml — 指标目录模板
metrics:
  - name: http_requests_total
    type: counter
    owner: platform-team
    purpose: "统计 HTTP 请求总量,用于 QPS 和错误率计算"
    labels:
      - name: method
        cardinality: low  # GET, POST, PUT, DELETE 等 ~10 个值
      - name: status
        cardinality: low  # 200, 404, 500 等 ~20 个值
      - name: handler
        cardinality: medium  # API 路由,约 50-200 个值
    retention: 90d
    status: active

  - name: http_request_duration_seconds
    type: histogram
    owner: platform-team
    purpose: "HTTP 请求延迟分布,用于 P50/P95/P99 计算"
    labels:
      - name: method
        cardinality: low
      - name: handler
        cardinality: medium
    retention: 90d
    status: active

  - name: go_goroutines
    type: gauge
    owner: platform-team
    purpose: "Go 运行时 goroutine 数量"
    labels: []
    retention: 30d
    status: active

这个目录不是写来摆着的。它有三个实际用途:

  1. 审计:定期扫描 Prometheus 实际采集的指标,与目录对比,找出"野指标"
  2. 准入:新增指标必须先在目录注册,说明用途和基数评估
  3. 淘汰:标记为 deprecated 的指标,定期从采集配置中移除

指标命名规范

好的指标名应该自解释——看到名字就知道它是什么、怎么算的。Prometheus 官方推荐命名格式:

<domain>_<subsystem>_<name>_<unit>
坏命名好命名原因
requestshttp_requests_total缺少域名前缀和类型后缀
errorshttp_requests_errors_total无法区分是 HTTP 错误还是业务错误
cpunode_cpu_seconds_total缺少单位后缀
memory_usagecontainer_memory_working_set_bytes不够精确,应该用具体内存指标
latencyhttp_request_duration_seconds缺少域名和单位

_total 后缀是 Counter 类型的约定(Prometheus 自动给 Counter 加 _total),_seconds_bytes_ratio 是常用单位后缀。这些不是 Prometheus 强制的,但社区都这么干,你跟着走就行。

指标类型选择

Prometheus 有四种指标类型,选错类型会导致查询困难和存储浪费:

类型用途什么时候用什么时候别用
Counter只增不减的计数器请求总数、错误总数、字节总量不要存可能减少的值(如当前连接数)
Gauge可增可减的瞬时值温度、内存使用、队列长度不要存累计值(如总请求数)
Histogram分布统计延迟分布、响应体大小分布只关心平均值时别用(用 Summary 或直接算)
Summary客户端预计算分位数单实例延迟百分位需要跨实例聚合时别用(用 Histogram)

一个常见错误:用 Gauge 存请求总数。Gauge 可增可减,但请求总数只增不减。用 Gauge 存的话,rate() 函数算不出来,聚合也容易出错。

采集策略:分级管理,按需采集

采集频率分级

不是所有指标都需要 15 秒采一次。盲目统一 15 秒的采集间隔,既浪费存储又增加 Prometheus 负载。

# prometheus.yml — 分级采集配置
global:
  scrape_interval: 15s      # 默认 15s
  evaluation_interval: 15s   # 告警规则评估间隔

scrape_configs:
  # 关键业务指标:15s 采集(支付成功率、核心 API 延迟)
  - job_name: 'payment-service'
    scrape_interval: 15s
    metrics_path: '/actuator/prometheus'
    static_configs:
      - targets: ['payment-svc:8080']

  # 基础设施指标:60s 采集(CPU、内存、磁盘)
  - job_name: 'node-exporter'
    scrape_interval: 60s
    static_configs:
      - targets: ['node-1:9100', 'node-2:9100']

  # 调试类指标:300s 或按需(GC 详情、线程栈)
  - job_name: 'debug-metrics'
    scrape_interval: 300s
    metric_relabel_configs:
      - source_labels: [__name__]
        regex: 'go_gc_duration_seconds|go_memstats_*'
        action: keep
    static_configs:
      - targets: ['debug-svc:8080']

通过分级采集,可以降低 30%-50% 的数据采集量。这个数字看着不大,但放在百万级指标的集群里,就是几十 GB 的存储节省和显著的内存释放。

metric_relabel_configs 过滤无用指标

很多 Exporter 暴露了几百条指标,但你只用到几十条。用 metric_relabel_configs 在采集时直接丢弃不需要的指标:

scrape_configs:
  - job_name: 'node-exporter'
    scrape_interval: 60s
    metric_relabel_configs:
      # 丢弃磁盘 IO 的细节指标,只保留汇总
      - source_labels: [__name__]
        regex: 'node_disk_.*_bytes_total'
        action: drop

      # 丢弃网络接口的详细统计(只保留 eth0)
      - source_labels: [__name__, 'device']
        regex: 'node_network_.*;(?!eth0).*'
        action: drop

      # 重命名过长的指标名
      - source_labels: [__name__]
        regex: 'node_(.+)'
        target_label: __name__
        replacement: 'node_$1'

action: drop 在采集阶段就丢弃数据,不会写入 TSDB,是最有效的减少指标数量的方式。相比之下,在查询时用 {__name__!=""} 过滤是没用的——数据已经存进去了,照样占内存和磁盘。

label_keep 和 label_drop

有时候你不是要删指标,而是要删标签。比如某个 Exporter 给每条指标都加了一个 instance_ip 标签,这个标签值有几百个,白白增加了基数:

metric_relabel_configs:
  # 删除高基数的 instance_ip 标签
  - action: label_drop
    regex: 'instance_ip'

存储优化:让 Prometheus 活得更久

内存与磁盘的关系

Prometheus 的内存使用和活跃时间序列数量直接相关。每条活跃时间序列大约占用 1-3 KB 内存(取决于标签数量和采样频率)。50 万条时间序列,光内存就要 500MB - 1.5GB,加上查询缓存和索引,轻松吃掉几个 GB。

活跃时序数预估内存预估磁盘(15天)适用场景
10 万200-400 MB5-10 GB小型集群(<50 节点)
50 万800 MB - 1.5 GB25-50 GB中型集群(200-500 节点)
100 万2-3 GB50-100 GB大型集群(500-1000 节点)
500 万8-15 GB250-500 GB超大规模,必须分片

这些是粗略估算,实际占用取决于指标类型(Histogram 比 Counter 占用更多)和采集频率。但有一个经验法则:当 Prometheus 内存使用超过可用内存的 60% 时,就该考虑分片了

数据保留策略

# prometheus.yml — 启动参数配置
# --storage.tsdb.retention.time=15d    短期保留
# --storage.tsdb.retention.size=100GB 磁盘上限(先到先触发清理)

# 推荐的生产配置:时间 + 磁盘双重限制
# 启动命令:
# prometheus --storage.tsdb.retention.time=15d \
#            --storage.tsdb.retention.size=100GB \
#            --query.max-samples=50000000 \
#            --query.timeout=2m

注意 --storage.tsdb.retention.size 这个参数。它设置磁盘使用上限,当磁盘用量超过这个值时,Prometheus 会自动删除最旧的数据。这个参数是防止磁盘写满导致 Prometheus 挂掉的最后一道防线。

长期存储:Thanos / VictoriaMetrics

Prometheus 本地存储只适合短期数据(15-30 天)。长期存储和跨实例查询需要 Thanos 或 VictoriaMetrics:

方案架构优势劣势
ThanosPrometheus Sidecar + 对象存储兼容原生 Prometheus,社区成熟组件多,部署复杂
VictoriaMetrics独立时序数据库性能好,部署简单,压缩率高生态偏小,部分 PromQL 兼容性差
Cortex/Mimir分布式 Prometheus多租户,水平扩展架构最复杂,适合超大规模

我个人的实战建议:中小团队(<500 节点)用 VictoriaMetrics,省心省力;大型团队或需要多租户的场景用 Thanos;超大厂(几千节点以上)才考虑 Cortex/Mimir。

数据质量度量:量化你的监控健康度

五个核心质量指标

监控数据治理不能只靠感觉。你需要量化指标来衡量"你的监控数据有多健康"。以下是五个核心质量度量:

质量维度衡量方式PromQL 示例健康阈值
完整性采集成功率up / count(up) * 100> 99%
时效性采集延迟scrape_duration_seconds P95< 10s
基数健康高基数指标检测count by (__name__) ({__name__=~".+"})单指标 < 1万条时序
覆盖率关键指标覆盖率自定义检查脚本核心服务 100%
废弃率无查询指标比例日志分析< 10%

用 PromQL 检测高基数指标

# 找出基数最高的 10 个指标
topk(10, count by (__name__)({__name__=~".+"}))

# 找出基数超过 10000 的指标(需要外部脚本)
# Prometheus 本身不擅长做这种聚合分析,建议用以下 Python 脚本

写个 Python 脚本定期扫描高基数指标:

#!/usr/bin/env python3
"""扫描 Prometheus 中的高基数指标"""

import requests
from collections import defaultdict

PROMETHEUS_URL = "http://localhost:9090"
CARDINALITY_THRESHOLD = 10000  # 单指标时序数阈值

def get_metric_cardinality():
    """获取每个指标的时序数量"""
    # 用 /api/v1/series 接口获取所有时间序列
    resp = requests.get(
        f"{PROMETHEUS_URL}/api/v1/series",
        params={"match[]": "{__name__=~'.+'}"},
        timeout=30
    )
    data = resp.json()["data"]

    cardinality = defaultdict(int)
    for series in data:
        metric_name = series["__name__"]
        cardinality[metric_name] += 1

    return cardinality

def main():
    cardinality = get_metric_cardinality()

    print("=" * 70)
    print(f"{'指标名':<50} {'时序数':>10}")
    print("=" * 70)

    # 按基数排序,输出 Top 20
    sorted_metrics = sorted(cardinality.items(), key=lambda x: x[1], reverse=True)
    for name, count in sorted_metrics[:20]:
        flag = " ⚠️" if count > CARDINALITY_THRESHOLD else ""
        print(f"{name:<50} {count:>10,}{flag}")

    print("=" * 70)
    total = sum(cardinality.values())
    high_card = sum(1 for c in cardinality.values() if c > CARDINALITY_THRESHOLD)
    print(f"总时序数: {total:,}")
    print(f"高基数指标数 (> {CARDINALITY_THRESHOLD:,}): {high_card}")

if __name__ == "__main__":
    main()

这个脚本跑一次就知道你的 Prometheus 有多少条时间序列、哪些指标在爆炸。建议每周跑一次,或者接入告警——当总时序数超过阈值时自动通知。

采集失败监控

# 所有采集目标中失败的比例
count(up == 0) / count(up) * 100

# 按采集任务分组,找出失败最多的 job
count by (job) (up == 0)

# 采集耗时最长的目标
topk(10, scrape_duration_seconds)

把这几条 PromQL 配成告警,比等你去 Grafana 上发现问题靠谱得多:

# alerts.yml — 采集质量告警
groups:
  - name: scraping-quality
    rules:
      - alert: ScrapeTargetDown
        expr: up == 0
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "采集目标 {{ $labels.instance }} 不可达"
          description: "Job {{ $labels.job }} 中的 {{ $labels.instance }} 已经 5 分钟无法采集"

      - alert: ScrapeSlow
        expr: scrape_duration_seconds > 10
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "采集耗时过长: {{ $labels.instance }}"
          description: "采集耗时 {{ $value }}s,超过 10s 阈值"

      - alert: PrometheusHighCardinality
        # 这个需要通过 recording rule 预计算
        expr: prometheus_tsdb_head_series > 500000
        for: 10m
        labels:
          severity: critical
        annotations:
          summary: "Prometheus 时序数量过高"
          description: "当前活跃时序数 {{ $value }},超过 50 万阈值,需检查高基数指标"

废弃指标淘汰:定期清库存

指标废弃流程

指标只增不减是监控系统的通病。开发上线时埋了一堆点,后来功能下线了,指标还在那儿白白占空间。需要一个定期清理流程:

  1. 发现:扫描最近 30 天从未被查询的指标
  2. 评估:确认指标确实无用了(不是"偶尔看一次")
  3. 标记:在指标目录中标记为 deprecated
  4. 通知:通知指标所有者,给 7 天缓冲期
  5. 清理:从采集配置中移除,或用 metric_relabel_configs drop 掉

发现无查询指标的思路:开启 Prometheus 的查询日志,记录一段时间内被查询过的指标名,然后与全量指标列表做差集。

# prometheus.yml — 开启查询日志
# 启动参数加 --query.log=/var/log/prometheus-query.log
# 之后用脚本分析查询日志,提取被查询过的指标名
#!/usr/bin/env python3
"""分析 Prometheus 查询日志,找出从未被查询的指标"""

import json
import re
from collections import defaultdict

def parse_query_log(log_file):
    """解析查询日志,提取被查询过的指标名"""
    queried_metrics = set()

    with open(log_file) as f:
        for line in f:
            try:
                entry = json.loads(line)
                query = entry.get("params", {}).get("query", "")
                # 提取查询中的指标名
                # 简单正则匹配,实际可能需要解析 PromQL AST
                metrics = re.findall(r'([a-zA-Z_:][a-zA-Z0-9_:]*)\s*(?:\{|$)', query)
                queried_metrics.update(metrics)
            except json.JSONDecodeError:
                continue

    return queried_metrics

def main():
    queried = parse_query_log("/var/log/prometheus-query.log")

    # 获取全量指标名(通过 API)
    import requests
    resp = requests.get("http://localhost:9090/api/v1/label/__name__/values")
    all_metrics = set(resp.json()["data"])

    # 差集:存在但从未被查询的指标
    unused = all_metrics - queried

    print(f"总指标数: {len(all_metrics)}")
    print(f"被查询过: {len(queried)}")
    print(f"从未查询: {len(unused)}")
    print(f"废弃率: {len(unused)/len(all_metrics)*100:.1f}%")

    if unused:
        print("\n废弃指标列表(前 50 个):")
        for m in sorted(unused)[:50]:
            print(f"  {m}")

if __name__ == "__main__":
    main()

Grafana Dashboard 治理

Dashboard 是另一个重灾区。一个团队几十个 Dashboard,很多是临时创建后忘了删的。建议:

  1. 命名规范:Dashboard 标题以团队名前缀开头,如 [平台] API 延迟监控
  2. 标签管理:用 Grafana 的标签功能标记 env=prodteam=platformstatus=active
  3. 定期审查:每季度清理一次,超过 6 个月没人访问的 Dashboard 归档
  4. Dashboard as Code:用 JSON 或 Terraform 管理 Dashboard 定义,版本控制
// dashboard 标签示例
{
  "title": "[平台] API 延迟监控",
  "tags": ["team:platform", "env:prod", "status:active"],
  "folderUid": "platform-dashboards"
}

生产实践建议

分片策略

当单实例 Prometheus 承载不了时,不要急着上 Thanos。先试试分片:

# 使用 Hashmod 分片:将采集目标分散到多个 Prometheus 实例
# prometheus-shard-1.yml
scrape_configs:
  - job_name: 'k8s-pods'
    kubernetes_sd_configs:
      - role: pod
    relabel_configs:
      - source_labels: [__meta_kubernetes_pod_uid]
        modulus: 3       # 总共 3 个分片
        target_label: __tmp_hashmod
      - source_labels: [__tmp_hashmod]
        regex: "0"       # 这个实例只采集 hashmod=0 的目标
        action: keep

这样 3 个 Prometheus 实例各自采集约 1/3 的目标,单个实例负载降低到原来的 1/3。联邦集群用来做全局查询聚合。

一个完整的治理 Checklist

  • 维护指标目录,记录所有指标的归属、用途、基数
  • 检查高基数标签(user_id、request_id 等),替换为日志或追踪
  • 配置分级采集频率(15s/60s/300s)
  • 用 metric_relabel_configs 过滤无用指标
  • 监控采集成功率和采集耗时
  • 设置时序数量告警阈值
  • 定期扫描无查询指标并清理
  • Dashboard 命名规范化和定期审查
  • 长期存储方案落地(Thanos/VM)
  • 制定指标新增流程(注册 → 评估 → 采集)

总结

监控数据治理不是一次性项目,是持续工程。核心思路就三条:

从源头控制。指标定义规范、命名约定、标签策略,这些在指标进入 Prometheus 之前就该想清楚。一个 user_id 标签能炸掉整个监控系统,这种事故我见过不止一次。

分级管理。不是所有指标都值 15 秒采一次。关键业务指标高频采集,调试类指标低频或按需,废弃指标直接删掉。存储成本和运维成本都是真金白银。

量化度量。用数据治理监控数据本身——采集成功率、时序数量、无查询指标比例,这些指标要配告警。你不能等 Prometheus OOM 了才发现指标膨胀了。

最后一点经验:监控数据治理最难的不是技术,是组织协作。开发同学埋点时不会考虑基数问题,运维同学不知道哪些指标是业务必需的。建立指标目录和准入流程,让开发和运维对齐——这才是治理能落地的关键。

参考资料与致谢

本文在撰写过程中参考了以下资料,感谢原作者的贡献:

  1. Prometheus Overview — Prometheus 官方文档,介绍了 Prometheus 的数据模型和核心设计理念
  2. Comparison to alternatives | Prometheus — Prometheus 官方文档,对比了 Prometheus 与 Graphite 等其他监控系统的数据模型差异
  3. 指标洪峰与查询瓶颈:Prometheus/Grafana 监控体系的深度部署与调优 — 详细分析了大规模集群下 Prometheus 的性能瓶颈和分片策略
  4. 如何高效精简 Prometheus:从指标设计到存储优化的全链路策略 — 提供了指标命名规范和 metric_relabel_configs 过滤的实践方案
  5. Prometheus+Grafana 深度监控:从指标采集到多级告警的生产级部署 — 分析了监控体系的典型盲区告警和采集策略设计
  6. Grafana 数据治理终极指南 — 介绍了 Grafana 的元数据管理和 Dashboard 治理策略