"为了排查,我们把指标加到了一千多个;告警还是天天误报。"这句抱怨里藏着两个用户在争同一份数据:告警系统要的是判据,排查的人要的是上下文。前者的诉求是低基数、高确定性、可自动判定;后者的诉求是高基数、可任意切分、能一路下钻到单个租户。把两者压进同一套指标、同一个聚合层级、同一个保留窗口,机制必然偏向一方,另一方被静默牺牲。
要回答的不是"该采什么指标",而是:共用一套机制时,被牺牲的是哪一个,以及它为什么不会在任何评审里被发现。
1. 两个用户,两套判据
告警的判据是可执行性:收到之后要做什么动作。答不出动作的告警不是"噪音大",而是从一开始就没有判据。排查的判据是能否缩小范围:看一眼数据能不能把嫌疑面砍掉一半。两条判据都不落在"指标数量"上,加指标对二者都不直接加分。
| 比较项 | 告警用户 | 排查用户 |
|---|---|---|
| 判据 | 收到后做什么(可执行性) | 能否缩小范围(可分性) |
| 时间尺度 | 分钟级窗口,持续 N 个周期 | 事件前后数小时到数天 |
| 对维度的态度 | 越少越好,每个标签都要有值域边界 | 越多越好,缺一个维度就断链 |
| 精度要求 | 高确定性,误报的代价是警报疲劳 | 高保真,容忍查询慢 |
| 需要的形态 | 预聚合值、跨实例合并 | 原始分桶、原始标签 |
| 失败形态 | 误报与漏报,立刻可见 | 排查时"维度不够",只能猜 |
关键的不对称在这里:**告警失败立刻可见(有人被叫醒),排查失败永远不可见(人只是多花两小时,然后再没人提)。**这是后面所有错配的根因,也是它难以被发现的原因。
判据是可以写进配置的,写不进去就说明还没想清楚:
// 有效的告警:判据 + 动作 + 边界,缺一项就不该存在
─── - alert: 支付失败率异常
─── expr: svc:http_5xx_ratio:5m{service="pay"} > 0.02
─── for: 3m // 持续时间是判据的一部分,不是保险
─── annotations:
─── action: 先看 downstream 依赖面板,再决定回滚或摘流
─── boundary: 大促期间基线另设,沿用日常阈值必然误报
2. 维度诉求相反:低基数与高基数
一条时间序列的身份是「指标名 + 标签集」。标签是有限资源,两个用户的要价完全相反:
// 告警要的形态:标签域小、取值可枚举、每个组合都有稳定语义
http_server_requests_seconds_count{service="order", status="5xx", uri="/api/pay"}
// 排查要的形态:能按 tenant_id / upstream_host 切开
http_server_requests_seconds_count{service="order", uri="/api/pay", tenant_id="T_8821"}
// ─── 把两种诉求放进同一个指标,就是基数爆炸的入口
| 标签 | 对告警的价值 | 对排查的价值 | 基数风险 |
|---|---|---|---|
service / status |
高,判据靠它 | 中,只能定位到服务 | 低 |
uri(已归一) |
高 | 中,定位到接口 | 低 |
uri(含路径参数、UUID) |
负,判据随流量抖动 | 高 | 中–高 |
tenant_id / user_id |
无,判据不关心 | 极高 | 极高 |
trace_id / request_id |
无 | 极高 | 不应进指标,进日志或追踪 |
结论很直接:**user_id、request_id 属于排查,不属于告警。**为了排查把它们写进指标,告警侧的序列数就不是"多加几个指标",而是各标签值域相乘:
// 序列数的估算:不是加法,是乘法
基础指标 http_server_requests_seconds × 1
+ status {2xx,4xx,5xx} × 3
+ uri 归一 {约 200 个接口} × 200 → 千级
+ tenant_id {约 5000 个租户} × 5000 → 百万级
// ─── 千级时告警评估是秒级;百万级时单次评估可能超过评估间隔本身
3. 共用一套指标的三个后果
| 后果 | 触发条件 | 表面现象 | 谁被牺牲 |
|---|---|---|---|
| 误报 | 聚合被细分后每个切片样本量不足 | 长尾切片偶发抖动即触发 | 告警可信度 |
| 漏报 | 为压基数做降维,异常并进"其他" | 异常淹在 uri="other" 里 |
排查能力 |
| 排查断链 | 写入时就丢掉了标签,事后无法恢复 | "只能看总量,看不出是哪个租户" | 排查能力 |
基数被拖垮的机制并不神秘:序列数等于各标签值域之积。uri 归一不彻底(典型量级:路径参数、UUID、时间戳进了标签),再叠一个 tenant_id,单个服务的活跃序列数从千级跳到百万级是常见的量级变化。后果不是"存储变贵",而是告警规则的评估周期被拉长、查询超时、部分序列被静默丢弃——最后一点最危险:数据不再完整,而没人知道缺的是哪一段。
写入侧降维配置(字段名按所用 Prometheus 版本核对)
─── metric_relabel_configs:
─── - source_labels: [__name__, uri]
─── regex: 'http_server_requests_seconds_count;.*/user/[0-9]+.*'
─── action: drop // 丢弃 = 判断力永久消失,事后补不回来
─── - source_labels: [tenant_id]
─── action: labeldrop // 降基数与保排查,只能二选一
这里必须说清被牺牲的是哪一个:如果为了告警压低基数,被牺牲的是排查,代价是"维度不够"——它只体现为排查变慢,不体现为任何故障;如果为了排查保留高基数,被牺牲的是告警,代价是误报与评估延迟——它至少还会被抱怨。两种牺牲里,前一种更常见,也更难被发现,因为没有任何一个时刻会有人为"少了一个维度"负责。
| 度量 | 归属 | 说明 |
|---|---|---|
| 告警可执行率 | 告警用户 | 能回答"收到后做什么"的告警占比 |
| 误报率 | 告警用户 | 按同类事件统计,不按告警条数 |
| 排查时维度命中率 | 排查用户 | 下钻所需标签存在的比例 |
| 活跃序列数增长率 | 共同的预算 | 与流量增长脱钩即为异常 |
4. 一套采集,两套视图
分离两个用户不等于建两套采集。可持续的做法是写入侧一次采集,读侧两套视图:
// 视图 A:告警用。预计算、低基数、序列名稳定
─── recording rule(评估间隔按实际核对):
─── - record: svc:http_5xx_ratio:5m
─── expr: sum(rate(http_server_requests_seconds_count{status="5xx"}[5m]))
─── / sum(rate(http_server_requests_seconds_count[5m]))
// 视图 B:排查用。保留原始分桶与完整标签,保留期短,不参与告警
─── 原始 histogram + 完整标签集,只在下钻时查询
| 取舍 | 告警侧要求 | 排查侧要求 | 冲突点 |
|---|---|---|---|
| 聚合层级 | 预聚合值,跨实例合并 | 原始分桶与实例级标签 | 预聚合抹掉分片差异 |
| 保留期 | 长而粗(判据看趋势) | 短而细(事件窗口) | 同一存储两头不讨好 |
| 采样 | 可接受,判据看统计量 | 危险,边缘样本可能就是证据 | 采样率应按排查侧定 |
| 收敛 | 必须去重、抑制、分组 | 有损,被抑制的那条可能正是根因 | 收敛规则要能回答被吃掉的是什么 |
同一错配的镜像形态是用日志做告警、用指标做排查:日志告警的成本随流量线性上涨(写入、索引、扫描),判据也不稳定;指标做排查则维度天生不够。这个分工不是偏好问题,是判据决定的。
把两个视图真正分开,读侧的差别只有几行:
// 告警视图:只查预聚合序列,查询代价与实例数无关
svc:http_5xx_ratio:5m{service="pay"}
// 排查视图:查询时才展开标签,代价高但只在人在场时发生
topk(10, sum by (tenant_id, uri) (
rate(http_server_requests_seconds_count{service="pay", status="5xx"}[10m])))
// ─── 这条查询不能进告警规则:它慢、结果不稳定、且随时可能超时
这里有一条容易被忽略的约束:**排查视图的保留期可以短,但它的标签必须在写入时就已存在。**事后想补维度,只能重跑历史流量;而历史流量不会重演。
5. 排错:五种常见情形
| 现象 | 真实原因 | 处理方向 |
|---|---|---|
| 告警"每次都能看出问题但没人处理" | 判据缺失,只描述现象不指向动作 | 每条告警补一行"第一步做什么",补不出的下线 |
| 同一故障反复告警几十条 | 无分组抑制,或分组键用了高基数标签 | 分组键限定在基数可控的标签上 |
| 排查时只能看到"总计" | 写入侧做过 labeldrop 或预聚合 |
细分需求下沉到排查视图,不能反向改告警指标 |
| 采集序列数突然翻倍 | 高基数标签进了历史指标 | 先定位是哪个标签的哪个值域,再决定丢弃或改写 |
| 告警比故障晚十几分钟 | 评估间隔与 for 被高基数拖长 |
核对单次评估耗时,拆规则或缩窗口 |
6. 边界:什么时候不值得分离
| 场景 | 结论 | 理由 |
|---|---|---|
| 系统极小,告警只有三条 | 不分离 | 三条判据人眼可维护,两套视图的运维成本高于收益 |
| 排查靠直连机器、现场看日志 | 不分离 | 排查用户不吃指标,维度诉求不冲突 |
| 单租户、单实例 | 不分离 | 标签域几乎无基数,两个用户诉求重合 |
| 峰值与均值差异小、无尖峰 | 不分离 | 低基数聚合已足够表达判据 |
判断这套分离是否值得,只看四个量:告警可执行率、误报率、排查时维度命中率、活跃序列数增长率。前两个衡量告警用户,第三个衡量排查用户,第四个是两者共同的预算线。
自测
- 一条告警答不出"收到之后做什么",它缺的是哪一类判据?
user_id为什么不该出现在告警指标的标签里,代价具体落在哪一步?- 基数爆炸先伤到的是存储成本,还是告警规则的评估?
- 一份指标要同时服务两个用户,分离点放在写入侧还是读侧?
- 告警收敛规则在什么情况下会吃掉排查需要的那条记录?
- 什么规模与什么数据形态下,"两个用户"这个划分本身不成立?