2722 字
约 9 分钟
15
S16-X07 · 告警设计:从阈值到 SLO 告警
2024-12-31
2026-11-08

告警系统最常见的死法不是没告警,而是告警太多以至于没人看。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. 自测

  1. 为什么固定阈值在业务量变化时必然失效?
  2. 四个黄金信号分别回答什么问题?
  3. 错误预算为 0.1% 时,30 天允许的不可用时间大约是多少?
  4. 多窗口多燃尽率为什么要求短窗口与长窗口同时超标?
  5. 一条告警至少要带上哪四类信息,值班人才不必猜?
  6. inhibit_rules 的 equal 条件设得太宽会有什么后果?
S16-X07 · 告警设计:从阈值到 SLO 告警
https://www.clxhxhhr.top/posts/5733/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。