告警系统最常见的死法不是没告警,而是告警太多以至于没人看。CPU 超过 80% 就响、响应时间超过 1 秒就响,头两周大家还认真处理,第三周开始整组人默认静音。真正的事故来的时候,它混在两百条同类通知里。
阈值告警的根本缺陷是:它对业务量不敏感。凌晨 3 点 CPU 冲到 90% 可能毫无影响,白天两个节点同时打满却可能没人发现——因为阈值只描述机器状态,不描述用户是否受影响。
1. 阈值告警的四种失效方式
| 失效方式 | 表现 | 后果 |
|---|---|---|
| 阈值固定,流量在变 | 大促期间所有阈值告警刷屏 | 真故障被淹没 |
| 只监控机器指标 | CPU 正常但接口全 500 | 故障无告警 |
| 静态阈值与业务解耦 | 用户失败率 30% 但机器指标健康 | 完全无声 |
| 无处置动作 | 收到告警不知道做什么 | 值班人忽略 |
| 告警无分级 | 磁盘 85% 和数据库挂了走同一通道 | 半夜被叫醒处理小事 |
修正方向不是"调阈值",而是换成以用户可感知的结果为中心:成功率、延迟、错误预算。
2. 四个黄金信号
| 信号 | 回答的问题 | 关键指标 | Micrometer 侧常见来源 |
|---|---|---|---|
| 延迟 Latency | 用户等多久 | P50/P95/P99 | http.server.requests 的 max/百分位 |
| 流量 Traffic | 系统在承受多少 | QPS、并发连接 | 同上(count) |
| 错误 Errors | 失败了多少 | 5xx 率、业务失败率 | http.server.requests 的 status tag |
| 饱和度 Saturation | 还有多少余量 | 线程池队列、连接池、GC 时间 | jvm.gc.pause、HikariCP 指标 |
Spring Boot 3.3 + Micrometer 打开这些指标只需要两件事:
management:
endpoints.web.exposure.include: health,metrics,prometheus
metrics:
distribution:
percentiles-histogram:
http.server.requests: true # 打开直方图,才能在 PromQL 里算 P99
slo:
http.server.requests: 100ms,300ms,1s
tags:
application: clarity-api
// 业务指标要手写,机器指标不会告诉你"下单失败"
Counter failed = Counter.builder("order.create")
.tag("result", "failed").register(meterRegistry);
failed.increment();
slo 那段会生成 http_server_requests_seconds_bucket 里的 SLO 桶,是后面算比例的关键;percentiles-histogram 不打开就只能在 Grafana 里看服务端算好的分位,PromQL 无法跨维度聚合。
3. SLI 与 SLO 的定义与取值
| 概念 | 定义 | 例子 |
|---|---|---|
| SLI(指标) | 可测量的服务质量 | 成功请求数 / 总请求数 |
| SLO(目标) | 对 SLI 的承诺值 | 30 天滚动 99.9% |
| SLA(合同) | 对外承诺,含赔付 | 99.5%,违约有条款 |
| 错误预算 | 1 − SLO | 30 天 0.1% ≈ 43 分钟 |
取值经验:SLO 定得比实际能力略低一点。上季度实测 99.95%,这季度定 99.9% 是稳的;定 99.99% 只会让团队天天救火。
# 30 天滚动成功率 SLI(按请求数加权,不要按时间点平均)
sum(rate(http_server_requests_seconds_count{status!~"5.."}[30d]))
/
sum(rate(http_server_requests_seconds_count[30d]))
# P99 延迟:必须用直方图桶算,不能用两个平均值相减
histogram_quantile(0.99,
sum by (le) (rate(http_server_requests_seconds_bucket{uri="/api/orders"}[5m])))
延迟类 SLI 有个坑:只统计成功请求,会把"超时被客户端断开"的慢请求排除掉,数字好看但用户体验差。更贴近的说法是"在阈值内完成的请求比例":
sum(rate(http_server_requests_seconds_bucket{le="0.3"}[5m])) by (uri)
/
sum(rate(http_server_requests_seconds_count[5m])) by (uri)
4. 错误预算与燃尽率告警
错误预算(Error Budget)是"这个月还能坏多少"。燃尽率(Burn Rate)是"正在以多快的速度烧掉它"。燃尽率 = 1,刚好用完;燃尽率 = 14.4,一天就烧完全月额度。
多窗口多燃尽率是当前比较通行的做法,短窗口保证灵敏,长窗口保证不误报:
| 窗口 | 燃尽率阈值 | 含义 | 处置 |
|---|---|---|---|
| 1h + 5m | 14.4 | 按此速度 2 天内烧完 30 天额度 | 立即响应(P0/P1) |
| 6h + 30m | 6 | 5 天内烧完 | 值班处理 |
| 3d + 6h | 1 | 按预算速度烧 | 工单跟踪 |
# prometheus/rules/slo-burnrate.yml
groups:
- name: slo-burnrate
rules:
- alert: ApiErrorBudgetFastBurn
expr: |
(
sum(rate(http_server_requests_seconds_count{status=~"5.."}[1h]))
/ sum(rate(http_server_requests_seconds_count[1h])) > (14.4 * 0.001)
)
and
(
sum(rate(http_server_requests_seconds_count{status=~"5.."}[5m]))
/ sum(rate(http_server_requests_seconds_count[5m])) > (14.4 * 0.001)
)
for: 2m
labels: { severity: critical }
annotations:
summary: "API 错误预算快速燃烧"
0.001 是"1 − 99.9%"。两个窗口用 and 连接是这套方法的核心:只有短窗口和长窗口同时超标,才认为是一次真实且持续的劣化,而不是一次几秒钟的抖动。
5. 告警分级
| 级别 | 通道 | 响应 | 例子 |
|---|---|---|---|
| P0 | 电话 / 强提醒 | 立刻,全组 | 核心链路不可用、数据写入错误 |
| P1 | 值班群 + 手机推送 | 15 分钟内 | 错误预算快燃、单机房降级 |
| P2 | 群消息 / 工单 | 当天工作时段 | 磁盘将满、队列积压增长 |
| P3 | 日报汇总 | 排期处理 | 非核心服务慢、配置待清理 |
判据只有一条:"如果现在不管,用户会受影响吗?" 会影响 → P0/P1;暂时不会 → P2/P3。分级混乱的典型症状是所有东西都是 P1,最后电话通道被静音。
6. 告警内容必须包含什么
| 必含项 | 作用 | 反例 |
|---|---|---|
| 现象 | 一眼知道发生什么 | "指标异常" |
| 影响面 | 判断优先级 | 不说影响多少用户/哪些接口 |
| 看板链接 | 直接进入排查 | 让人自己去翻 |
| 处置手册链接 | 值班人不必猜 | 只有"请联系某某" |
| 首次触发时间 | 判断是否持续 | 只给当前值 |
# Alertmanager 模板片段:影响面与跳转必须自动带上
- alert: ApiErrorBudgetFastBurn
annotations:
summary: "{{ $labels.application }} 5xx 率 {{ $value | humanizePercentage }}(SLO 99.9%)"
impact: "受影响接口:{{ $labels.uri }};预估影响请求数 {{ $value }}"
dashboard: "https://grafana.example.com/d/api-slo"
runbook: "https://wiki.example.com/runbook/api-5xx"
处置手册要写到"可执行"的粒度:先看什么、再做什么、判断不了找谁。只写"检查服务状态"等于没写。
7. 抑制与聚合
同一次机房网络抖动会同时打爆几十条告警。抑制(inhibit)处理"根因优先",聚合(group)处理"同类合并":
# alertmanager.yml
route:
receiver: default
group_by: [alertname, application]
group_wait: 30s # 等 30 秒攒同类,避免一条条发
group_interval: 5m
repeat_interval: 4h # 未恢复的告警 4 小时才复提醒
inhibit_rules:
- source_matchers: [severity="critical"]
target_matchers: [severity="warning"]
equal: [alertname, application] # 同一服务 critical 压住 warning
- source_matchers: [alertname="NodeDown"]
target_matchers: [alertname=~"PodNotReady|HighCpu"]
equal: [instance]
| 手段 | 解决什么 | 配置点 |
|---|---|---|
group_by |
同服务同类告警合并 | route 段 |
group_wait |
启动瞬间的抖动 | 建议 30s–1m |
repeat_interval |
未恢复时的重复轰炸 | 4h 或更长 |
inhibit_rules |
根因告警压制衍生告警 | 需谨慎设 equal |
| 静默(silence) | 计划内维护 | 必须设过期时间 |
inhibit_rules 用错会掩盖真实问题:如果 equal 条件太宽,一个无关的 critical 能把真正的 warning 全压掉。上线前用历史告警数据回放一遍更稳。
8. 值班轮值与升级路径
| 环节 | 做法 | 常见漏洞 |
|---|---|---|
| 轮值周期 | 一周一次,交接有清单 | 交接靠口头 |
| 首位响应 | 15 分钟内确认告警 | 无人认领 |
| 升级 | 未确认 15 分钟 → 备份值班 | 没配升级链 |
| 交接内容 | 未闭环告警、变更记录 | 只交接"没什么事" |
| 事后 | 每次 P0 出复盘,产出改进项 | 复盘不落地 |
升级路径要写在告警本体里,而不是团队记忆里:告警连续触发 N 次未确认就自动 @ 下一个人。值班人被半夜叫醒后还得查"该找谁",是最没必要的摩擦。
9. 排错:告警失效的八种表现
| 现象 | 原因 | 处理 |
|---|---|---|
| 大家都静音了 | 告警过多、无分级 | 按 §5 分级,P2 不发手机 |
| 故障发生但没告警 | 只监控机器,不监控业务结果 | 补 5xx 率与业务 Counter |
| 收到告警不知道做什么 | 缺处置手册 | 每条告警强制带 runbook 链接 |
| 阈值半年没复核 | 业务量变化后阈值失效 | 季度复核,改为基于 SLO 的比例告警 |
| 一次故障收到几百条 | 无抑制无聚合 | 配 inhibit_rules + group_by |
| 告警反复触发一分钟内恢复 | 没有 for 持续时间 |
加 for: 2m 等确认窗口 |
| 业务指标缺失 | 自定义 Counter 没注册 | 在关键路径补埋点 |
| 看板链接点进去是空的 | 链接里的变量与 label 不匹配 | 用告警里带上的 label 拼 URL |
最后一条很值得检查:告警模板里拼的跳转链接,应当在测试环境点一遍确认真的能落到对应面板。
10. 边界:什么时候简单阈值就够了
| 场景 | 建议 | 理由 |
|---|---|---|
| 磁盘使用率 | 静态阈值(80%/90%) | 与业务量无关,物理量 |
| 证书到期 | 天数阈值 | 时间本身就是判据 |
| 单机 CPU | 仅作 P2 参考,不告警 | 易抖动,业务未必受影响 |
| 核心链路可用性 | SLO 燃尽率 | 直接对应用户体验 |
| 资源类容量 | 饱和度 + 趋势预测 | 看"还有多久用完" |
| 定时任务 | 未在预期时间内完成即告警 | 二进制结果,不需要比例 |
一句话收口:与业务量无关的量用阈值,与用户相关的量用 SLO。
11. 自测
- 为什么固定阈值在业务量变化时必然失效?
- 四个黄金信号分别回答什么问题?
- 错误预算为 0.1% 时,30 天允许的不可用时间大约是多少?
- 多窗口多燃尽率为什么要求短窗口与长窗口同时超标?
- 一条告警至少要带上哪四类信息,值班人才不必猜?
inhibit_rules的equal条件设得太宽会有什么后果?