SLO 与多窗口多 burn-rate 告警:一次 Prometheus 落地整理
目录
📦 本文基于的完整项目源码:meirongdev/shop
阈值告警(“5xx > 1% 持续 5 分钟”)的两个失败模式工程师都熟悉:阈值定低了一周告警一百条没人看,阈值定高了真出事告警来得太晚。Google SRE Workbook 第 5 章给出的思路是 error budget + multi-window multi-burn-rate——让告警的“敏感度”随事件严重程度变化。本文更像是一次整理:为什么、怎么做、以及落地时绕不开的取舍。
一、SLO 和 error budget 的底层逻辑#
Q:SLO 不就是"99.9% 可用"这种数字吗?跟告警有什么关系?
至少在工程落地里,SLO 一个很重要的用途是把 reliability 变成预算。
- SLO 99.9% 意味着每个 30 天窗口允许 0.1% 的请求失败——这就是 error budget。
- 把整个 30 天窗口的 budget 按时间切片:1 分钟内允许失败的请求量很小(少量异常就把这分钟的 budget 烧光),但 30 天内的累计可以容忍。
- 如果短窗口内的烧速大于"按比例消耗"的速度,说明这次事件如果不止住,会提前烧完整个月的预算——这就是值得告警的事件。
阈值告警是"超过 X 报警"——只看一个时间点。Burn-rate 告警是"按当前的烧速会在多久烧完月度 budget"——看的是趋势对未来的投影。
Q:burn rate 怎么算?
定义:burn rate = 观察窗口内的实际错误率 / SLO 允许的错误率。
- SLO 99.9% → 允许错误率 0.001
- 5 分钟窗口里实际错误率 0.0144 → burn rate = 14.4
burn rate = 1 表示恰好按月度预算的速率消耗;burn rate > 1 表示当前烧得过快。
二、为什么是 14.4× 和 6×#
Q:14.4 和 6 这两个数字从哪来?
来自一道关于"我希望多久烧完月度预算"的设计选择。
| 我希望"如果不止住,预算在 N 天内烧完" | 对应的 burn rate |
|---|---|
| 2 天烧完(通常要立刻 page on-call) | 14.4×(30 ÷ 2 ÷ 1.04 ≈ 14.4) |
| 5 天烧完(开 ticket,工作日处理) | 6× |
| 10 天烧完(warning) | 3× |
Google SRE Workbook 推荐的两档(fast / slow)就是 14.4× 和 6×。fast 触发就 page on-call,slow 触发就开 ticket。再细分(3 档、4 档)当然也可以,但通常会让告警结构更复杂、组织成本更高。
Q:为什么要"多窗口"?为什么不直接拿 5 分钟的 burn rate 报警?
单窗口的 5 分钟 burn rate 噪声很大:一次抓包失败、一次重启、一次客户端 retry 风暴都能把它打飞。直接告警就回到了"阈值噪声大"的老问题。
多窗口要求短窗口 AND 长窗口同时满足:
- 5 分钟 burn rate > 14.4× AND 30 分钟 burn rate > 14.4×
如果只是 5 分钟瞬时打高、30 分钟还没反应过来——不报。如果两个窗口都满足,说明这事件已经持续到了"用更长尺度看也确实异常"的程度。这是用时间一致性消噪。
Q:那 reset time 呢?真出问题的时候 30 分钟窗口什么时候才会触发?
short window 用来"快",long window 用来"准"。常见配对:
| 严重程度 | short window | long window | 触发延迟(最坏) |
|---|---|---|---|
| Fast burn (14.4×) | 5m | 30m | ~5 min |
| Slow burn (6×) | 30m | 6h | ~30 min |
5 分钟的 burn rate 已经超 14.4× 时,30 分钟窗口大概率也已经超了——short window 是"前哨"。slow burn 故意让延迟大些,因为本来就不期望立刻处理。
三、Prometheus recording rules:把 burn rate 算出来#
Q:直接在 alert 里写 PromQL 查询为什么不好?
两个问题:
- 每个 alert evaluation 都查一次原始 metric。同一个数据被多个告警共享时,CPU 浪费。
- 修改 SLO target 时要改多处。把 0.001 这个数字写进每条 alert,回头要从 99.9% 调到 99.5% 时痛苦。
我更倾向的做法是:把 burn rate 抽成 recording rule,alert 里只做阈值比较。
groups:
- name: shop.slo.checkout.recording
interval: 30s
rules:
- record: shop:checkout_errors:ratio_rate5m
expr: |
sum(rate(http_server_requests_seconds_count{instance=~"buyer-bff:.*",method="POST",uri="/buyer/v1/checkout/create",status=~"5.."}[5m]))
/
sum(rate(http_server_requests_seconds_count{instance=~"buyer-bff:.*",method="POST",uri="/buyer/v1/checkout/create"}[5m]))
- record: shop:checkout_errors:ratio_rate30m
expr: |
sum(rate(http_server_requests_seconds_count{instance=~"buyer-bff:.*",method="POST",uri="/buyer/v1/checkout/create",status=~"5.."}[30m]))
/
sum(rate(http_server_requests_seconds_count{instance=~"buyer-bff:.*",method="POST",uri="/buyer/v1/checkout/create"}[30m]))
# 同样写 1h 和 6h
命名约定:shop:<slo-name>:ratio_rate<window>。冒号是 Prometheus 推荐的层级分隔符,跟普通指标分开。
四、Alert 规则#
Q:alert 怎么写?
- name: shop.slo.checkout.alerts
rules:
- alert: CheckoutSLOFastBurn
expr: |
shop:checkout_errors:ratio_rate5m > (14.4 * 0.001)
and
shop:checkout_errors:ratio_rate30m > (14.4 * 0.001)
for: 2m
labels:
severity: critical
slo: checkout-availability
owner: buyer-team
annotations:
summary: "Checkout SLO fast burn — error budget depleting rapidly"
description: "5m checkout error rate {{ $value | humanizePercentage }} > 14.4x budget (SLO 99.9%)"
runbook_url: "docs/runbooks/SLO_BURN.md"
- alert: CheckoutSLOSlowBurn
expr: |
shop:checkout_errors:ratio_rate30m > (6 * 0.001)
and
shop:checkout_errors:ratio_rate6h > (6 * 0.001)
for: 15m
labels:
severity: warning
slo: checkout-availability
owner: buyer-team
annotations:
summary: "Checkout SLO slow burn — error budget steadily depleting"
runbook_url: "docs/runbooks/SLO_BURN.md"
几个细节:
for: 2m/for: 15m:再加一层防抖,防止瞬时尖刺把告警打飞。severity/owner/slo标签——Alertmanager 路由用,最好和 Service Catalog(catalog-info.yaml的spec.owner)保持一致。runbook_url最好真的存在——告警里给 dead link 是 SRE 的反模式。
五、Latency SLO:有点不一样#
Q:99.9% 可用性好理解,“99.5% 的请求 < 500ms” 怎么写 PromQL?
延迟 SLO 的本质更接近"超阈值的请求比例",不是分位数。直接 histogram_quantile(0.5, ...) 更适合拿来做 SLI 监控,而不是直接当 SLO。
正确写法用 histogram_bucket:
- record: shop:login_latency_slow:ratio_rate5m
expr: |
(
sum(rate(http_server_requests_seconds_count{instance=~"auth-server:.*",uri="/auth/v1/login"}[5m]))
-
sum(rate(http_server_requests_seconds_bucket{instance=~"auth-server:.*",uri="/auth/v1/login",le="0.5"}[5m]))
)
/
sum(rate(http_server_requests_seconds_count{instance=~"auth-server:.*",uri="/auth/v1/login"}[5m]))
读法:(总请求 - 落在 le="0.5" 桶里的请求) / 总请求 = 超 500ms 的请求比例。这个比例就是 latency SLO 的"错误率",跟 availability SLO 完全对称地走 burn-rate 告警。
前提:histogram bucket 边界要预先包含 0.5。Spring Boot management.metrics.distribution.percentiles-histogram.http.server.requests=true 默认有几个桶,但要精准命中 SLO 阈值需要显式配置 SLO buckets:
management:
metrics:
distribution:
slo:
http.server.requests: 100ms,200ms,500ms,1s,2s
bucket 边界没有 500ms 时,PromQL 用 le="0.5" 会拿到空结果。这是 latency SLO 落地最常见的坑。
六、SLO 选择的工程纪律#
Q:怎么决定哪些路径配 SLO?
我现在给自己的一个简单约束是:只给"用户实际能感知到的事情"配 SLO。
- 适合:登录延迟、下单成功率、首页加载、API gateway 总体可用性。
- 不适合:内部 RPC 之间的成功率(除非它直接决定上面那一类的成败)、缓存命中率、内部消息队列吞吐。
把内部指标当 SLO 是常见误区。结果是告警炸但用户没感觉,组织开始忽略告警——错误预算系统被破坏。
Q:SLO target 怎么选?99.9% 还是 99.99%?
用历史数据反推:
- 拉过去 90 天的实际错误率
- 看 P95 是多少(说明大多数时候在这个水平)
- SLO 目标设比 P95 略宽松——这样告警在"明显异于历史"时才触发
强行写 99.99% 但实际能力只有 99.9%——你不是在保护 SLO,是在制造 burn-rate 告警雪崩。
七、绕不开的 tradeoff#
Q:SLO 系统看起来完美,落地有什么坑?
四个真实成本:
-
组织成熟度门槛高。SLO 体系要求"error budget 用完就停发布"——如果团队没有这个权威边界,SLO 就是装饰品。某金融团队 SLO 系统跑了一年,没有一次因为 budget 用尽真停过发布;那 SLO 等于不存在。
-
选错 SLI 比没有 SLI 更糟。配 SLO 是给团队写一份长期合约。SLI 选错(例如把"内部 API 成功率"当 SLO),团队会跟着错指标优化,真用户体验反而退化。第一版 SLO 应该非常保守、覆盖最重要的 1-2 条用户路径,等组织习惯了再扩。
-
histogram bucket 设计耦合 SLO。Latency SLO 阈值变(500ms → 300ms)就要改应用配置加新的 bucket,重启所有实例。这是 latency SLO 的"刚性"代价。某些团队会先用 percentile metric 监控,确认 SLO 阈值站得住才落到 bucket。
-
Multi-burn-rate 不能告诉你“为什么”,只能告诉你“在烧”。runbook 还是得有人写——“checkout SLO fast burn → 检查 order-service 5xx → 检查 wallet-service downstream → …” 没有 runbook 配套,SRE 半夜被 page 起来还是不知道怎么处理。
Q:什么团队不应该上 SLO?
- 5 人以下小团队,所有人都直接看 PR——用 Sentry/Honeycomb 这类工具就够了,SLO 体系是 overkill。
- 业务还在 PMF 阶段、流量没稳定的早期产品——历史数据不足以选合理的 SLI 目标。
- 没有 on-call 机制的团队——burn-rate page 出来没人接,跟没有告警一样。
Q:什么团队更适合先上?
- 流量稳定、有 on-call、组织上能接受"budget 用完停发布"的团队。
- Postmortem 比例高于行业基线(说明告警机制有问题,需要更智能的告警结构)。
- 多团队协作、SRE 和业务团队之间需要"客观的可靠性合约"——SLO 就是那份合约。
八、最小落地清单#
按依赖顺序:
- 挑 1 条核心用户路径(不超过 3 条)。第一版 SLO 我会建议先克制。
- 拉 90 天历史数据,看 P95 错误率/延迟,反推可达成的 SLO target。
- 写 4 个 recording rules(5m / 30m / 1h / 6h burn rate)。
- 写 2 个 alert(fast burn / slow burn)。
- 写 runbook(哪怕只有 100 字,也比 dead link 好)。
- runbook_url 标注到 alert annotation,severity/owner 标签和 Service Catalog 对齐。
- 观察一周——看 false positive 率、看告警是否被 ignore。如果第一周噪声太多,先调 long window 长度,再考虑改 SLO target;不要先改阈值。
第一周稳了,再扩第二条路径。路径慢慢配起来之后,团队才会逐步形成一套更统一的 reliability 语言。