不能把 user_id 放进标签的原因
一句话总结
指标的时间序列数量等于各标签值组合数量的乘积。一个标签选择错误,就可能产生数百万条时间序列;从那一刻起,监控系统会比被观测对象更早崩溃。
为什么需要它
我们为 http_requests_total 添加标签。
http_requests_total{method, status, endpoint}
method:5 种(GET、POST、PUT、DELETE、PATCH)status:8 种endpoint:40 种
时间序列数量为 5 × 8 × 40 = 1,600 条,尚可接受。
接着有人提出“希望按用户查看”,于是增加 user_id。如果有 10 万名用户,
5 × 8 × 40 × 100,000 = 1.6 亿条。
Prometheus 会为每条时间序列在内存中维护索引。服务器会因 OOM 崩溃,于是用于观测事故的手段,会因为事故本身而消失。
引发基数爆炸的标签
以下内容绝不能放进标签。
| 标签 | 危险原因 |
|---|---|
user_id, session_id |
随用户数量增长,没有上限 |
request_id, trace_id |
每个请求都唯一,无限增长 |
email, ip |
实际上无限增长,同时包含个人信息 |
timestamp |
每个时刻产生新时间序列(把时间放进时间序列的矛盾) |
url(包含查询字符串) |
?page=1, ?page=2……无限增长 |
| 完整错误消息 | 消息中混入具体值时无限增长 |
最后一项尤其危险。如果把 error="connection to 10.0.3.17:5432 timed out" 这样包含 IP 和端口的消息作为标签,组合数量会爆炸。应使用 error="db_timeout" 这类经过分类的值。
判断标准——这个标签可能有多少种值
添加标签前,应先问自己以下问题。
- **可能值有多少种?**必须能够数出并给出答案。“很多”不是答案。
- **数量会随时间增长吗?**如果会,是否存在上限?
- **真的会根据该标签创建告警吗?**如果只是可能在监控面板上看一次,就应该放到日志或追踪中,而不是指标中。
经验目标是每个标签的值不超过 100 种,每个指标的时间序列不超过 1 万条。
URL 必须进行规范化
# 나쁨 — 주문 개수만큼 시계열
endpoint="/api/orders/8f3a91"
# 좋음 — 라우트 패턴
endpoint="/api/orders/:id"
使用框架的路由定义时通常会自动规范化。手工拼接字符串则一定会导致爆炸。这不是会不会失误的问题,而只是何时发生的问题。
分别使用三种信号
有时确实需要高基数信息,例如“为什么这个用户的请求很慢”。但这不属于指标的职责。
| 信号 | 基数要求 | 回答的问题 |
|---|---|---|
| 指标 | 必须较低 | “数量有多少、速度有多快?”用于趋势和告警 |
| 日志 | 可以较高 | “当时发生了什么?”用于单个事件 |
| 追踪 | 可以较高 | “时间花在哪里?”用于单次请求路径 |
先通过指标收到告警 → 确定时间范围 → 再用日志和追踪深入查看单个请求。三种信号不是替代关系,而是调查顺序。
已经发生爆炸时
- **找出是哪项指标。**Prometheus 的 TSDB 状态页面会列出时间序列最多的指标和标签排名。
- **在采集阶段丢弃。**通过 relabel 配置移除问题标签,或直接 drop 整项指标,无需等待应用重新部署。
- **修复应用程序。**根本解决办法是不再添加该标签。
- **调整保留期和分片。**这只能用于临时救火,不是解决方案。
生产现场中的表现
- Prometheus 周期性 OOM → 首先怀疑新增加的标签。
- 监控面板查询每次耗时 30 秒 → 时间序列过多,扫描范围太大。
- 部署后内存呈阶梯状上升 → 该次部署增加了标签。
爆炸发生后的回退顺序
基数问题通常总是在事故发生后才被发现。Prometheus 可能耗尽内存并崩溃,或者查询超时无法返回。此时需要遵循固定顺序。
**首先统计什么占用了多少。**Prometheus 状态页面(/tsdb-status)会显示产生最多时间序列的指标和标签,也可以通过命令查看。
topk(10, count by (__name__)({__name__=~".+"}))
count(app_request_duration_seconds_bucket)
count(count by (user_id)(app_request_total))
最后一行表示该标签实际拥有多少种值。如果数字达到数千,仅这一个标签就足以成为原因。
**应在写入存储之前进行阻断。**修复并重新部署应用才是正确答案,但这需要时间。在此期间,应在抓取配置中删除标签,或直接丢弃整个指标。
metric_relabel_configs:
- source_labels: [__name__]
regex: 'app_request_total'
target_label: user_id
replacement: '' # 라벨 값을 비운다
- source_labels: [__name__]
regex: 'debug_.*'
action: drop # 이 지표는 아예 저장하지 않는다
**已经存储的时间序列不会自行消失。**即使不再写入,它们仍会在整个保留期内占用内存。情况紧急时,可以通过管理 API 删除(必须启用 --web.enable-admin-api,而且删除后无法恢复)。
**直方图会悄无声息地成倍增加。**每个桶就是一条时间序列。一个包含 12 个桶的直方图再乘以三个标签,很快就会达到数万条。如果无法减少标签,应先减少桶数量。
**避免重犯错误,需要部署前检查。**从代码中提取指标名称和标签列表,只要出现不在允许清单中的标签,就在 CI 阶段阻止部署。依靠人的注意力,问题一定会在繁忙时期再次出现。
下一项检查的内容
后续测验将根据标签值的乘积计算时间序列数量,并区分指标、日志和追踪的职责。面对已经开始爆炸的场景,还要判断应先采取采集阶段缓解,还是先完成应用程序的根本修复。