📦 本文基于的完整项目源码: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,工作日处理)
10 天烧完(warning)

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 查询为什么不好?

两个问题:

  1. 每个 alert evaluation 都查一次原始 metric。同一个数据被多个告警共享时,CPU 浪费。
  2. 修改 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.yamlspec.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 系统看起来完美,落地有什么坑?

四个真实成本:

  1. 组织成熟度门槛高。SLO 体系要求"error budget 用完就停发布"——如果团队没有这个权威边界,SLO 就是装饰品。某金融团队 SLO 系统跑了一年,没有一次因为 budget 用尽真停过发布;那 SLO 等于不存在。

  2. 选错 SLI 比没有 SLI 更糟。配 SLO 是给团队写一份长期合约。SLI 选错(例如把"内部 API 成功率"当 SLO),团队会跟着错指标优化,真用户体验反而退化。第一版 SLO 应该非常保守、覆盖最重要的 1-2 条用户路径,等组织习惯了再扩。

  3. histogram bucket 设计耦合 SLO。Latency SLO 阈值变(500ms → 300ms)就要改应用配置加新的 bucket,重启所有实例。这是 latency SLO 的"刚性"代价。某些团队会先用 percentile metric 监控,确认 SLO 阈值站得住才落到 bucket。

  4. 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. 挑 1 条核心用户路径(不超过 3 条)。第一版 SLO 我会建议先克制。
  2. 拉 90 天历史数据,看 P95 错误率/延迟,反推可达成的 SLO target。
  3. 写 4 个 recording rules(5m / 30m / 1h / 6h burn rate)。
  4. 写 2 个 alert(fast burn / slow burn)。
  5. 写 runbook(哪怕只有 100 字,也比 dead link 好)。
  6. runbook_url 标注到 alert annotation,severity/owner 标签和 Service Catalog 对齐。
  7. 观察一周——看 false positive 率、看告警是否被 ignore。如果第一周噪声太多,先调 long window 长度,再考虑改 SLO target;不要先改阈值。

第一周稳了,再扩第二条路径。路径慢慢配起来之后,团队才会逐步形成一套更统一的 reliability 语言。