概述
先说个场景:凌晨三点,你被告警吵醒,爬起来打开 Grafana,发现几十个 Dashboard 里找不到一条有用的信息。指标倒是有几百万条——但全是垃圾。
这不是个例。我见过太多团队,Prometheus 部署完就不管了,指标只增不减,告警规则越写越多,最后监控系统自己先挂了:Prometheus 内存 OOM、查询超时 30 秒、告警评估延迟 5 分钟以上。监控系统成了最大的故障源,这话说出去都讽刺。
监控数据治理解决的就是这个问题。它不是什么高深理论,说白了就一句话:搞清楚你有哪些指标、哪些有用、哪些该删、怎么管它们的一生。
这篇文章从指标生命周期管理的角度,拆解监控数据治理的五个阶段:指标定义、采集策略、存储优化、质量度量、废弃淘汰。每个阶段都配实操代码和踩坑经验。
指标爆炸的根因:不是数据多了,是管不住
指标为什么会膨胀
指标膨胀不是一夜之间发生的。通常的路径是这样的:
- 初期:Node Exporter + cAdvisor,几百条指标,Prometheus 跑得好好的
- 业务接入:每个服务埋点,每个中间件装 Exporter,指标涨到几万条
- 高基数炸弹:有人把
user_id、request_id、session_id塞进了标签,时序数据量从几万条直接爆炸到百万级 - 灾难: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 约束),直接超限。
高基数标签的常见来源
| 来源 | 典型场景 | 危害程度 | 替代方案 |
|---|---|---|---|
| 用户 ID | http_requests_total{user_id="..."} | 致命 | 用日志记录,或聚合为百分位 |
| 请求 ID | http_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
这个目录不是写来摆着的。它有三个实际用途:
- 审计:定期扫描 Prometheus 实际采集的指标,与目录对比,找出"野指标"
- 准入:新增指标必须先在目录注册,说明用途和基数评估
- 淘汰:标记为
deprecated的指标,定期从采集配置中移除
指标命名规范
好的指标名应该自解释——看到名字就知道它是什么、怎么算的。Prometheus 官方推荐命名格式:
<domain>_<subsystem>_<name>_<unit>
| 坏命名 | 好命名 | 原因 |
|---|---|---|
requests | http_requests_total | 缺少域名前缀和类型后缀 |
errors | http_requests_errors_total | 无法区分是 HTTP 错误还是业务错误 |
cpu | node_cpu_seconds_total | 缺少单位后缀 |
memory_usage | container_memory_working_set_bytes | 不够精确,应该用具体内存指标 |
latency | http_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 MB | 5-10 GB | 小型集群(<50 节点) |
| 50 万 | 800 MB - 1.5 GB | 25-50 GB | 中型集群(200-500 节点) |
| 100 万 | 2-3 GB | 50-100 GB | 大型集群(500-1000 节点) |
| 500 万 | 8-15 GB | 250-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:
| 方案 | 架构 | 优势 | 劣势 |
|---|---|---|---|
| Thanos | Prometheus 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 万阈值,需检查高基数指标"
废弃指标淘汰:定期清库存
指标废弃流程
指标只增不减是监控系统的通病。开发上线时埋了一堆点,后来功能下线了,指标还在那儿白白占空间。需要一个定期清理流程:
- 发现:扫描最近 30 天从未被查询的指标
- 评估:确认指标确实无用了(不是"偶尔看一次")
- 标记:在指标目录中标记为
deprecated - 通知:通知指标所有者,给 7 天缓冲期
- 清理:从采集配置中移除,或用
metric_relabel_configsdrop 掉
发现无查询指标的思路:开启 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,很多是临时创建后忘了删的。建议:
- 命名规范:Dashboard 标题以团队名前缀开头,如
[平台] API 延迟监控 - 标签管理:用 Grafana 的标签功能标记
env=prod、team=platform、status=active - 定期审查:每季度清理一次,超过 6 个月没人访问的 Dashboard 归档
- 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 了才发现指标膨胀了。
最后一点经验:监控数据治理最难的不是技术,是组织协作。开发同学埋点时不会考虑基数问题,运维同学不知道哪些指标是业务必需的。建立指标目录和准入流程,让开发和运维对齐——这才是治理能落地的关键。
参考资料与致谢
本文在撰写过程中参考了以下资料,感谢原作者的贡献:
- Prometheus Overview — Prometheus 官方文档,介绍了 Prometheus 的数据模型和核心设计理念
- Comparison to alternatives | Prometheus — Prometheus 官方文档,对比了 Prometheus 与 Graphite 等其他监控系统的数据模型差异
- 指标洪峰与查询瓶颈:Prometheus/Grafana 监控体系的深度部署与调优 — 详细分析了大规模集群下 Prometheus 的性能瓶颈和分片策略
- 如何高效精简 Prometheus:从指标设计到存储优化的全链路策略 — 提供了指标命名规范和 metric_relabel_configs 过滤的实践方案
- Prometheus+Grafana 深度监控:从指标采集到多级告警的生产级部署 — 分析了监控体系的典型盲区告警和采集策略设计
- Grafana 数据治理终极指南 — 介绍了 Grafana 的元数据管理和 Dashboard 治理策略