土法炼钢兴趣小组的算法知识备份

【可观测性工程】SLO 工程:错误预算、Burn Rate、多窗口多燃烧率告警

文章导航

分类入口
architectureobservability
标签入口
#slo#sli#error-budget#burn-rate#sre#google-sre#prometheus#alerting#recording-rules

目录

SLO 工程:错误预算、Burn Rate、多窗口多燃烧率告警

某团队定了 99.9% 可用性 SLO——30 天滚动窗口内允许 0.1% 的请求失败。双十一前一周,SLO Dashboard 上的预算线已经贴地:不是一次灾难性事故,而是 Q3 连续 12 次”小发布”——每次 p99 从 120ms 漂到 130ms,每次都没触发 CPU 告警,每次都没人打开 SLO 面板。直到大促前冻结发布窗口,才发现预算早已耗尽。

这个故事概括了 SLO 工程的核心挑战:定一个数字很容易,让这个数字驱动日常决策很难。SLO 的价值不在数字本身,在于它创建了一条从”业务可接受的不可用程度”到”工程团队每天的开发节奏”的完整决策链。

本文从 SLI 选择、错误预算计算、Burn Rate 告警到组织落地,给出可直接粘贴到 Prometheus 的 Recording Rule 与 Alert Rule 模板。文中 Burn Rate 阈值来自 Google Site Reliability Workbook 第 5 章(A 级来源);PromQL 在本地用 promtool check rules 校验语法(本环境未安装 promtool/Docker 拉取镜像失败,规则以 Workbook 原文为准,部署前请在目标 Prometheus 版本上验证)。

30 天滚动窗口错误预算消耗曲线(合成示例)

一、SLO 的概念框架

1.1 四个术语,四种用途

术语 英文 定义 谁关心
SLI Service Level Indicator 对服务某维度的实测值 SRE、开发
SLO Service Level Objective SLI 在窗口内应满足的目标 工程 + 产品
SLA Service Level Agreement 违反 SLO 时的合同后果 法务、客户
错误预算 Error Budget \(1 - \text{SLO}\),窗口内允许的不合格量 TL、PM、SRE

SLI 例子:“过去 5 分钟,checkout API 成功请求占比 99.93%”。

SLO 例子:“30 天滚动窗口内,checkout 成功率 ≥ 99.9%”。

SLA 例子:“若季度可用性 < 99.5%,按合同退还 10% 月费”。SLO 通常比 SLA 更严——给自己留缓冲。

错误预算 不是”可以浪费的 downtime”。它是发布节奏控制器:预算充裕时承担发布风险;预算紧张时冻结 feature,只做可靠性工作。

1.2 错误预算的数量级

SLO 每多一个 “9”,预算缩小 10 倍:

SLO 30 天窗口允许失败比例 等价 downtime(连续不可用) 等价失败请求(假设均匀流量)
99% 1% 7.2 小时 每 100 请求 1 次失败
99.9% 0.1% 43.2 分钟 每 1000 请求 1 次失败
99.99% 0.01% 4.32 分钟 每 10000 请求 1 次失败
99.999% 0.001% 26 秒 每 100000 请求 1 次失败

选 SLO 之前先算一遍:这个窗口内我们客观上能容忍多少失败? 很多团队拍 99.99%,却从未验证过能否在现有架构下达到——结果要么永远超预算,要么 SLO 形同虚设。

1.3 SLO 与监控的区别

指标体系设计 中的 USE/RED/Golden Signals 回答”该采集什么”。SLO 回答”什么程度算不可接受、何时该停发版”。

监控告警常见反模式:CPU > 80% 就 Page——CPU 高不一定伤害用户。SLO Burn Rate 告警与用户可感知失败等价:只有正在加速消耗错误预算时才值得叫醒人(详见 告警体系)。


二、SLI 的选择与定义

2.1 一个服务 2–4 个 SLI 足够

SLI 过多等于没有 SLI——没人能同时盯 20 条曲线。Google SRE Workbook 建议按服务类型选 2–4 个:

SLI 类型 适用服务 典型定义 常见误区
可用性 在线 API 非 5xx 且非超时的请求 / 总请求 把 429 算进失败
延迟 同步 API p99 延迟 < 阈值 用均值代替分位数
吞吐 批处理/队列 单位时间处理条数 ≥ 目标 与可用性 SLI 重复
持久性 存储/DB 写入后可读且未丢失的比例 与备份 RPO 混淆
新鲜度 数据管道 数据滞后 < N 分钟 用批处理完成率代替

2.2 可用性 SLI:什么算”坏请求”

Google SRE Book 第 4 章:可用性 SLI = 好事件 / 合法事件

# 好请求:HTTP 2xx/3xx/4xx 中排除"服务责任"之外的码
sum(rate(http_requests_total{status!~"5.."}[5m]))
/
sum(rate(http_requests_total[5m]))

必须排除的请求类型

必须纳入的失败

# 更严格的可用性 SLI(5xx + 超时)
sum(rate(http_requests_total{status=~"5.."}[5m]))
+
sum(rate(http_request_duration_seconds_count{le="+Inf"}[5m])
  - rate(http_request_duration_seconds_count{le="2"}[5m]))

第二条假设 SLO 延迟阈值为 2s——超时请求计入不可用。阈值应与 SLO 文档一致。

2.3 延迟 SLI:必须用分位数

用 p50 做延迟 SLI 会在尾部退化时完全无感。SLO 延迟 SLI 标准写法:

\[\text{SLI}_{\text{latency}} = \frac{\#\{r \mid \text{latency}(r) < T\}}{\#\{r\}}\]

Prometheus histogram 实现(假设 bucket 上限覆盖 SLO 阈值 \(T\)):

sum(rate(http_request_duration_seconds_bucket{le="0.2"}[5m]))
/
sum(rate(http_request_duration_seconds_count[5m]))

上式表示”p99 以下”若 bucket 为 0.2s——实际应用用 histogram_quantile报告,用 bucket 比例做SLI 合规判断

# 过去 5 分钟 p99 是否 < 200ms(用于 Dashboard,非直接 SLI 合规)
histogram_quantile(0.99,
  sum(rate(http_request_duration_seconds_bucket[5m])) by (le)
) < 0.2

SLI 合规应使用窗口内低于阈值请求占比,而非单点 p99——后者对样本量敏感。

2.4 测量窗口:1 分钟 vs 5 分钟 vs 1 小时

窗口 优点 缺点 用途
1m 反应快 GC/抖动噪声大 不推荐单独做 SLO 窗口
5m 平衡 SLI 采集、Recording Rule 默认
1h 平滑 检出慢 Burn Rate 短窗口告警
30d 滚动 与预算对齐 计算复杂 SLO 合规、预算剩余

Google SRE Workbook 推荐:SLI 用 1–5 分钟滚动窗口采集,SLO 合规用 30 天滚动窗口,告警用 1h/6h/3d 多窗口 Burn Rate

2.5 SLI 采集的三条工程路径

路径 A:应用内 Prometheus 指标(最常见)

// Go + prometheus client:按 status 计数
httpRequestsTotal.WithLabelValues(method, path, strconv.Itoa(status)).Inc()

路径 B:OpenTelemetry Span Metrics

OpenTelemetry Collector spanmetrics connector 从 trace 生成 RED 指标——与 OpenTelemetry 深入 中的 pipeline 一致。适合已全量 OTel 但未在每个服务手工埋 counter 的团队。

路径 C:Synthetic / Black-box 探测

从用户视角探测 /health 或关键 API——与 Events 与变更关联 中的 black-box 互补。缺点:覆盖路径有限,不能替代服务端 SLI。


三、错误预算的工程计算

3.1 基本公式

30 天滚动窗口,SLO = 99.9%:

\[\text{ErrorBudget}_{\text{requests}} = (1 - 0.999) \times N_{30d}\]

\[\text{ErrorBudgetRemaining} = 1 - \frac{\text{BadRequests}_{30d}}{(1 - \text{SLO}) \times N_{30d}}\]

若流量均匀,43.2 分钟连续不可用 ≈ 耗尽 30 天预算(99.9%)。

3.2 滚动窗口 vs 日历窗口

类型 行为 适用
日历月 每月 1 日重置 与财务/合同对齐
滚动 30d 任意时刻看过去 30 天 更平滑,Google 推荐

Prometheus 原生不内置”滚动 30 天 sum”——常用近似:

# 30 天失败请求(increase 近似)
sum(increase(http_requests_total{status=~"5.."}[30d]))

大窗口 increase() 在 Prometheus 2.x 可用,但长窗口 query 昂贵——生产环境用 Recording Rule 每 5 分钟预聚合。

3.3 Burn Rate 定义

燃烧率(Burn Rate)= 当前错误率相对于”刚好耗尽预算”的错误率之比。

\[\text{BurnRate} = \frac{\text{error\_rate}}{\text{SLO\_error\_rate}} = \frac{\text{error\_rate}}{1 - \text{SLO}}\]

推导 14.4 的由来(Google SRE Workbook):30 天 = 720 小时。若 1 小时内以 14.4 倍速度烧预算,则消耗 \(\frac{14.4}{720} = 2\%\) 的 30 天总预算。这是 Page 级告警的敏感度/误报率折中。

3.4 Recording Rules 模板

以下规则假设:job 标签 checkout,SLO 99.9%,指标 http_requests_total{job="checkout"}

# slo-recording-rules.yml
groups:
  - name: slo_checkout_availability
    interval: 30s
    rules:
      # 5 分钟错误率
      - record: slo:checkout:errors5m
        expr: sum(rate(http_requests_total{job="checkout",status=~"5.."}[5m]))

      - record: slo:checkout:total5m
        expr: sum(rate(http_requests_total{job="checkout"}[5m]))

      - record: slo:checkout:error_ratio5m
        expr: slo:checkout:errors5m / slo:checkout:total5m

      # Burn Rate(相对 SLO 错误率 0.001)
      - record: slo:checkout:burnrate5m
        expr: slo:checkout:error_ratio5m / 0.001

      - record: slo:checkout:burnrate1h
        expr: |
          sum(rate(http_requests_total{job="checkout",status=~"5.."}[1h]))
          / sum(rate(http_requests_total{job="checkout"}[1h]))
          / 0.001

      - record: slo:checkout:burnrate6h
        expr: |
          sum(rate(http_requests_total{job="checkout",status=~"5.."}[6h]))
          / sum(rate(http_requests_total{job="checkout"}[6h]))
          / 0.001

      - record: slo:checkout:burnrate3d
        expr: |
          sum(rate(http_requests_total{job="checkout",status=~"5.."}[3d]))
          / sum(rate(http_requests_total{job="checkout"}[3d]))
          / 0.001

      # 30 天预算剩余(百分比,近似)
      - record: slo:checkout:budget_remaining_ratio
        expr: |
          1 - (
            sum(increase(http_requests_total{job="checkout",status=~"5.."}[30d]))
            / (0.001 * sum(increase(http_requests_total{job="checkout"}[30d])))
          )

0.001 替换为 \((1 - \text{SLO})\) 即可适配其他 SLO 目标。


四、多窗口多燃烧率告警

多窗口多燃烧率告警规则组合

Google SRE Workbook 第 5 章给出 6 条规则——3 条 Page、3 条 Ticket。核心思想:短窗口 + 高燃烧率 抓突发;长窗口 + 低燃烧率 抓慢性退化。

4.1 Page 级规则(3 条)

规则 窗口 燃烧率阈值 预算消耗
A 1h ≥ 14.4× 2% / 30d
B 6h ≥ 6× 5% / 30d
C 3d ≥ 1× 10% / 30d(持续)
# slo-alert-rules.yml — Page 级
groups:
  - name: slo_checkout_pages
    rules:
      - alert: CheckoutSLOBurnRateCritical1h
        expr: slo:checkout:burnrate1h > 14.4
        for: 2m
        labels:
          severity: page
          service: checkout
        annotations:
          summary: "checkout SLO 1h 燃烧率 {{ $value | printf \"%.1f\" }}×"
          description: "1 小时内以当前速度将消耗约 2% 月错误预算。见 Runbook: ..."

      - alert: CheckoutSLOBurnRateCritical6h
        expr: slo:checkout:burnrate6h > 6
        for: 5m
        labels:
          severity: page
          service: checkout

      - alert: CheckoutSLOBurnRateCritical3d
        expr: slo:checkout:burnrate3d > 1
        for: 1h
        labels:
          severity: page
          service: checkout

for: 子句用于抑制瞬时尖峰——Workbook 建议 Page 规则加 2–5 分钟 pending。

4.2 Ticket 级规则(3 条)

规则 窗口 燃烧率阈值 预算消耗
D 6h ≥ 1× 10% / 30d
E 1d ≥ 1× 10% / 30d
F 3d ≥ 1× 10% / 30d
  - name: slo_checkout_tickets
    rules:
      - alert: CheckoutSLOBurnRateWarning6h
        expr: slo:checkout:burnrate6h > 1
        for: 15m
        labels:
          severity: ticket
          service: checkout

      - alert: CheckoutSLOBurnRateWarning1d
        expr: |
          sum(rate(http_requests_total{job="checkout",status=~"5.."}[1d]))
          / sum(rate(http_requests_total{job="checkout"}[1d]))
          / 0.001 > 1
        for: 1h
        labels:
          severity: ticket

      - alert: CheckoutSLOBurnRateWarning3d
        expr: slo:checkout:burnrate3d > 1
        for: 3h
        labels:
          severity: ticket

Ticket 告警进入 Jira/Slack, 打值班电话。

4.3 敏感性与噪音的权衡

flowchart LR
  subgraph fast [快速检出]
    A1["1h 窗口"] --> A2["燃烧率 14.4×"]
    A2 --> A3["Page"]
  end
  subgraph slow [慢性退化]
    B1["3d 窗口"] --> B2["燃烧率 1×"]
    B2 --> B3["Ticket"]
  end

反模式:短窗口 + 低燃烧率(如 5m + 2×)→ 每次 GC 都可能触发 → 告警疲劳(参见 告警体系 中”Page 只用于 Burn Rate”原则)。

反模式:长窗口 + 高燃烧率(如 3d + 14.4×)→ 几乎永不触发 → 假安全感。

4.4 Alert 标签与 Runbook 契约

每条 SLO Alert 应携带统一 labels,供 Alertmanager 路由:

labels:
  severity: page          # page | ticket
  service: checkout
  slo: availability       # availability | latency
  team: team-checkout
  runbook_url: https://wiki.example/runbooks/checkout-slo
annotations:
  summary: "{{ $labels.service }} {{ $labels.slo }} burn rate critical"
  description: |
    1h burn rate = {{ $value | printf "%.2f" }}× (threshold 14.4×).
    Dashboard: https://grafana.example/d/checkout-slo
    最近变更: 见 Panel 7 annotations

runbook_url 最低内容

  1. 打开 Grafana SLO Dashboard,确认 Burn Rate 窗口(1h/6h/3d)
  2. 检查最近 30min 部署(Panel 7)
  3. 若错误率上升:查 Traces/Logs 采样(存储成本 事故态采样)
  4. 是否 rollback:对照 Error Budget Policy §5.4 对话 3
  5. 升级条件:15min 无缓解 → TL;30min → VP Eng

无 runbook 的 Page 规则不应合并进生产——Alertmanager 只能路由,不能替人决策。


五、错误预算的策略使用

错误预算策略分区

5.1 预算分区与发布策略

剩余预算 工程策略 产品策略
> 50% 正常发布节奏 可安排大促/实验
20%–50% 冻结 feature,只允许 bugfix 推迟非关键需求
< 20% 全员可靠性改进 取消 risky 发布
0% 事故复盘 + SLO 复审 对外沟通(若触 SLA)

策略必须书面化(Error Budget Policy 文档),明确谁有权 override——通常是 VP Eng + SRE Lead 联合批准,且 override 事件本身要审计。

5.2 预算”冷藏”(Freezing)

大促、春节、重大发布前 2–4 周 进入冷藏期:

冷藏不是”不计预算”——而是降低消耗速度,把预算留给大促当天的已知风险。

5.3 预算耗尽之后

预算耗尽 ≠ 立即违约 SLA。它触发的是工程流程

  1. 冻结所有 feature 发布直至预算恢复(滚动窗口自然回升或新窗口开始)
  2. 强制事故复盘:是 SLO 定太紧、还是系统真的不可靠?
  3. 更新 SLO 或投资可靠性——不要悄悄改 SLO 数字而不复盘

5.4 预算谈判:工程与产品的三条对话

对话 1:能否加 feature?

对话 2:能否降低 SLO?

对话 3:事故后是否继续发布?

三条对话的共同点:用 Dashboard Panel 1 和 Panel 2 的截图作为会议事实来源,而非口头”感觉还行”。

5.5 与 告警体系 的分工

机制 回答的问题
SLO / Burn Rate 用户是否正在受损?预算烧多快?
Alertmanager route 谁被叫醒、通过什么渠道?
Ticket 规则 慢性问题谁跟进、何时不打扰 on-call?

SLO 规则产出 severity: page|ticket label;Alertmanager 按 label 路由——两篇文章的 YAML 应 同仓库、同 CI 审查。


六、与数据库层 SLI 的映射

服务 SLO 失败往往根因在 DB。本站 PG 监控体系 从内核机制推导六个监控维度——本节说明如何把 DB 指标映射为上游服务的 SLI 输入,而非另建一套孤立的数据库 SLO。

6.1 分层 SLO 模型

flowchart TB
  USER[用户请求] --> API[API 服务 SLO]
  API --> DB[(PostgreSQL / MySQL)]
  API --> CACHE[Redis]
  DB --> DBSLI[DB 层 SLI]
  DBSLI --> API

6.2 PG 维度 → 服务 SLI 映射表

PG 监控维度(21-monitoring) 内核故障模式 对 API SLI 的影响 是否应用作 Page
idle in transaction 过长 阻止 VACUUM、持锁 延迟 SLI 退化 否(Ticket,除非已 Burn Rate)
复制 lag 超阈值 WAL 堆积 读从库失败 → 可用性 SLI 否(DB 团队 Ticket)
XID wraparound 逼近 DB 只读 可用性 SLI 100% 失败 是(若影响用户 SLO)
pg_stat_statements 计划突变 慢查询 延迟 SLI 通过 API Burn Rate

原则:Page 只打用户 SLO Burn Rate。DB 指标发 Ticket 给 DBRE——除非 DB 故障已反映在 API Burn Rate 上。

6.3 mysqld / PG exporter 指标示例

# PG:连接使用率 > 85% → Ticket(非 Page)
sum(pg_stat_activity_count) / pg_settings_max_connections > 0.85

# MySQL:Innodb_buffer_pool 命中率骤降 → 调查延迟 SLI 根因
rate(mysql_global_status_innodb_buffer_pool_read_requests[5m])
/
(
  rate(mysql_global_status_innodb_buffer_pool_read_requests[5m])
  + rate(mysql_global_status_innodb_buffer_pool_reads[5m])
) < 0.95

DB 层告警应链到 性能调查方法论 的五层工具链,而不是独立叫醒 API on-call。

6.4 gRPC 与 HTTP 的 SLI 统一

混合协议栈中,checkout 对外 HTTP、对内 gRPC。若只对 HTTP 设 SLO,gRPC 5xx 可能通过内部调用链间接烧伤 HTTP SLI——但 gRPC 自身也应有内部 SLI 便于分解。

gRPC 可用性 SLI(Prometheus grpc-exporter 或 OTel):

sum(rate(grpc_server_handled_total{grpc_code!="OK",grpc_service="payment.Payment"}[5m]))
/
sum(rate(grpc_server_handled_total{grpc_service="payment.Payment"}[5m]))

grpc_code 映射注意

gRPC code 是否计入 SLI 失败 说明
OK
Canceled 通常否 客户端取消,类比 499
InvalidArgument 视产品 客户端错误,常排除
DeadlineExceeded 服务未在 deadline 内完成
Unavailable 依赖不可用
Internal

Dashboard 上应用 同一 Burn Rate 公式,仅替换指标名;Recording Rule 建议独立 group slo_payment_grpc 避免 PromQL 过长。

6.5 新鲜度 SLI(数据管道类服务)

ETL、CDC、搜索索引构建等服务不适合用”请求成功率”——用 数据滞后(Freshness):

\[\text{SLI}_{\text{fresh}} = \mathbb{1}[\text{now} - \text{last\_successful\_batch\_ts} < L]\]

# 最后成功批处理距今秒数
time() - pipeline_last_success_timestamp_seconds{job="search-indexer"}

SLO 示例:99% 的 5 分钟窗口内滞后 < 300s。Burn Rate 定义为”滞后超阈值的时间占比 / (1 - SLO)“——需自定义 Recording Rule,不能照搬 HTTP 模板。

6.6 持久性 SLI(存储类)

对象存储、备份系统常用 durability SLI:写入后 N 天内可读取的比例。工程上常通过 定期校验 job(随机抽样 GET + checksum)生成:

sum(rate(durability_check_success_total[1d]))
/
sum(rate(durability_check_total[1d]))

持久性 SLO 窗口常取 90d 滚动而非 30d——与 存储工程 中的 RPO/RTO 概念对齐,但数值必须单独与业务/legal 确认。


七、SLO 文化与组织落地

7.1 谁定 SLO

角色 职责
开发/架构 提出 SLI 候选、实现埋点
产品 确认”用户何时感觉坏”→ 阈值
SRE Recording Rule、告警、Dashboard、复盘
管理层 批准 Error Budget Policy

7.2 Dashboard 设计原则

SLO Dashboard 的第一受众是 PM 和 TL,不是 SRE:

7.3 月度 SLO 评审会议程

  1. SLI 数据质量:有无采集断点?故障期间 Prometheus 是否可用?
  2. 上窗口 SLO 达标情况:超预算原因分类(发布/依赖/容量/代码 bug)
  3. 告警噪音回顾:哪些 Ticket 从未 action?哪些 Page 误报?
  4. SLO 数字是否仍与业务匹配——不是”只能调严”

7.4 Grafana 面板完整规格

SLO Dashboard 建议独立 Folder(SLO / Production),与基础设施 Dashboard 分离——PM 不应在 CPU 曲线里找预算。

下面以 checkout-api(SLO 99.9% 可用性 + 99% 请求 < 200ms)为例,列出 8 个 Panel 的查询、可视化与阈值。Panel JSON 可导入 Grafana 9+;变量 $service$slo_target 在 Dashboard Settings 中定义。

Dashboard 变量

变量 类型 取值示例
$service query label_values(http_requests_total, job)
$slo_target custom 0.999(可用性)、0.99(延迟占比)
$window custom 30d

Panel 1:错误预算剩余(Stat)

slo:checkout:budget_remaining_ratio * 100

Panel 2:30 天 Burn Rate 趋势(Time series)

slo:checkout:burnrate1h
slo:checkout:burnrate6h
slo:checkout:burnrate3d

Panel 3:当前 SLI 可用性(Gauge + Stat)

1 - slo:checkout:error_ratio5m

Panel 4:延迟 SLI——低于 200ms 请求占比(Gauge)

sum(rate(http_request_duration_seconds_bucket{job="$service",le="0.2"}[5m]))
/
sum(rate(http_request_duration_seconds_count{job="$service"}[5m]))

Panel 5:30 天失败请求累计(Bar gauge)

sum(increase(http_requests_total{job="$service",status=~"5.."}[$window]))

Panel 6:预算允许失败数 vs 已消耗(Stat pair)

# 允许(30d)
0.001 * sum(increase(http_requests_total{job="$service"}[$window]))

# 已消耗
sum(increase(http_requests_total{job="$service",status=~"5.."}[$window]))

Panel 7:最近变更事件(Annotations)

Panel 8:活跃告警(Alert list)

flowchart TB
  subgraph row1 [第一行:决策]
    P1[预算剩余 Stat]
    P3[可用性 Gauge]
    P4[延迟 SLI Gauge]
  end
  subgraph row2 [第二行:趋势]
    P2[Burn Rate 1h/6h/3d]
  end
  subgraph row3 [第三行:归因]
    P7[变更 Annotations]
    P8[活跃告警]
  end
  P1 --> P2
  P2 --> P7

反模式:把 30 天 increase() 直接放在 Dashboard 主查询——每次刷新全量扫描 TSDB,大集群会拖慢 Grafana。应使用 §3.4 的 Recording Rule 预聚合。

7.5 promtool 规则校验与 CI 门禁

Recording Rule 与 Alert Rule 的 PromQL 在 Prometheus 升级、指标 rename 后容易静默失效(查询成功但返回空)。Google SRE Workbook 建议在 CI 中对规则做语法检查与固定输入/output 单元测试

本环境未安装 promtoolwhich promtool 无结果),以下配置来自 Prometheus 官方文档(A 级),部署前在目标 Prometheus 版本上执行。

步骤 1:语法检查

promtool check rules slo-recording-rules.yml slo-alert-rules.yml

步骤 2:单元测试(slo_test.yml 摘录)

# slo_test.yml — promtool test rules
rule_files:
  - slo-recording-rules.yml

evaluation_interval: 1m

tests:
  - interval: 1m
    input_series:
      - series: 'http_requests_total{job="checkout",status="200"}'
        values: '0+100x60'
      - series: 'http_requests_total{job="checkout",status="500"}'
        values: '0+0x59 100'
    alert_rule_test:
      - eval_time: 60m
        alertname: CheckoutSLOBurnRateCritical1h
        exp_alerts:
          - exp_labels:
              severity: page
              service: checkout
            exp_annotations:
              summary: 'checkout SLO 1h 燃烧率'

input_series0+100x60 表示从 0 起每分钟 +100(模拟稳定 QPS);第 60 分钟注入 100 次 5xx 模拟事故尖峰——具体是否触发 Page 取决于 burnrate1h 表达式与 for: 窗口,需在本地 promtool test 调参后固化 expected

步骤 3:GitHub Actions 片段

# .github/workflows/prometheus-rules.yml
jobs:
  prom-rules:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Download promtool
        run: |
          VER=2.51.0
          curl -sL "https://github.com/prometheus/prometheus/releases/download/v${VER}/prometheus-${VER}.linux-amd64.tar.gz" | tar xz
          echo "$(pwd)/prometheus-${VER}.linux-amd64" >> $GITHUB_PATH
      - run: promtool check rules observability/slo-recording-rules.yml
      - run: promtool test rules observability/slo_test.yml

门禁策略

变更类型 CI 要求
修改 Recording Rule check rules + 至少 1 条 test rules
修改 Alert Rule 同上 + Alertmanager amtool check-config(见 告警体系 附录 K)
升级 Prometheus minor 全量 replay test;关注 increase() 与 histogram 行为变更

:promtool test 的 eval_timefor: 叠加——Alert 的 for: 2m 意味着 test 中 eval_time 必须晚于条件首次满足至少 2 分钟,否则 exp_alerts 为空导致 CI 误报失败。


八、案例复盘:预算如何在沉默中耗尽

8.1 案例 A:Q3 十二次”小发布”(慢性退化)

背景:某电商 checkout-api,SLO 99.9%(30 天滚动),Error Budget Policy 已文档化但无人看 Dashboard。

时间线

时间 事件 5m 错误率 1h Burn Rate 预算剩余(约) 为何未 Page
W1 发布 v2.3.1,依赖库升级 0.05% 0.5× 92% < 14.4×
W3 发布 v2.3.2,连接池默认值变更 0.08% 0.8× 85% Ticket 未配置
W5–W12 每周一次 hotfix 0.06–0.12% 0.6–1.2× 降至 8% 6h/3d Ticket 规则缺失
W13 大促前 PM 要求冻结 8% 人工发现

根因(机制)

  1. 每次发布引入 p99 +10ms0.02% 额外 5xx——单独看都不触发 CPU/内存告警
  2. 只配置了 1h Page(14.4×),未配置 6h/3d Ticket(1×)——慢性退化无信号
  3. 429 曾被算进失败请求,客户端 retry 放大错误率(后按 §2.2 排除)

修复动作

教训:SLO 工程的主要敌人不是单次事故,而是低于 Page 阈值、高于可持续错误率的持续泄漏

8.2 案例 B:依赖 Redis 单点(突发 + 正确 Page)

背景:同一 checkout-api,payment 会话缓存走 Redis Cluster。

时间线

时刻 现象 API SLI 1h Burn Rate 响应
T+0 Redis 主节点网络分区 5xx 从 0.01% → 8% 18× Page A 触发
T+3min 值班 ack,切本地降级 5xx → 2% Page B 仍 firing
T+15min Redis 恢复 5xx → 0.02% 0.3× Page resolve

做对的事

做错的事(事后复盘)

8.3 案例 C:Prometheus 与业务同挂(SLI 盲区)

背景:单 AZ Prometheus,与 checkout 同 K8s 集群。

事故:AZ 网络故障,checkout 不可用,Prometheus scrape 也失败——up{job="checkout"}==0可用性 SLI 分母为零,Burn Rate 查询返回 NaN,无 Page

修复

8.4 从案例归纳的检查清单

检查项 案例 A 案例 B 案例 C
6 条 Burn Rate 规则齐全
Ticket 规则接 Slack/Jira
预算剩余 Dashboard 有人看
SLI 采集与业务故障域隔离
429/401 已从 SLI 排除
Runbook 链到依赖排查 部分

九、工程坑点

9.1 选 SLO 不算预算

99.9% vs 99.99%:预算差 10 倍。上线前用表格(§1.2)算一遍。

9.2 用均值代替分位数

p50 正常、p99 崩溃——用户投诉的是 p99。延迟 SLI 必须基于分位数或”低于阈值请求占比”。

9.3 429 与 500 混算

429 是限流,500 是故障。混算会让恶意客户端或错误 retry 策略烧干预算

9.4 短窗口 + 低燃烧率 = 告警风暴

每 5 分钟一条 Slack → 值班麻木 → 真正 Page 被忽略。

9.5 监控在故障中不可用

Prometheus 与业务同机房同故障域——业务挂时 SLI 也停采。双活采集合成探测跨 region remote write 至少选一种(见 Prometheus HA)。

9.6 每个团队各自定义 SLO 无法相加

平台级 SLO 需要从用户路径聚合,不能简单平均各微服务 SLO——需用错误预算的联合概率模型或取关键路径最弱环节。


十、落地清单


十一、关键概念回顾


十二、下一步

SLO 定好之后,需要告警体系承载 Burn Rate——Alertmanager route/inhibit、PagerDuty 排班、分级模板。下一篇 告警体系:Alertmanager、PagerDuty、分级抑制


上一篇内核追踪:ftrace、kprobe、uprobe、tracepoint 生产实战

下一篇告警体系:Alertmanager、PagerDuty、分级抑制

参考资料

  1. Google, Site Reliability Engineering, Chapters 4–5, O’Reilly, 2016
  2. Google, The Site Reliability Workbook, Chapter 2 “Implementing SLOs” & Chapter 5 “Alerting on SLOs”, O’Reilly, 2018
  3. Prometheus, Recording Rules, https://prometheus.io/docs/prometheus/latest/configuration/recording_rules/
  4. Prometheus, Alerting Rules, https://prometheus.io/docs/prometheus/latest/configuration/alerting_rules/
  5. OpenTelemetry, Span Metrics, https://opentelemetry.io/docs/collector/span-metrics/

附录 A:多服务 SLO 与依赖

微服务架构中,checkout 依赖 payment、inventory、user 三个下游。用户 facing SLO 失败时,需要知道哪条依赖链在烧预算。

A.1 关键路径 SLO

只对用户关键路径设 SLO,不对每个内部服务都设 99.99%:

服务 是否用户 facing 建议 SLO
checkout-api 99.9%
payment-internal 99.5%(内部)
legacy-report 99%(内部)

内部服务 SLO Violation 通过 Ticket 处理,除非已传播到 checkout Burn Rate。

A.2 依赖 SLI 的 RED 指标传递

下游 payment 的 http_requests_total{status=~"5.."} 应带 caller="checkout" label(基数可控时)——checkout 团队能分解自身 SLI 失败中有多少来自 payment。


附录 B:延迟 SLO 的完整 PromQL 示例

假设 checkout p99 SLO:99% 请求 < 200ms(30 天窗口)。

# Recording:5 分钟窗口内"好延迟"请求占比
sum(rate(http_request_duration_seconds_bucket{job="checkout",le="0.2"}[5m]))
/
sum(rate(http_request_duration_seconds_count{job="checkout"}[5m]))

Burn Rate 对延迟 SLI:将分子分母中的”错误”定义为 le="0.2" 之外的请求:

(
  1 - (
    sum(rate(http_request_duration_seconds_bucket{job="checkout",le="0.2"}[1h]))
    / sum(rate(http_request_duration_seconds_count{job="checkout"}[1h]))
  )
) / (1 - 0.99)

此处 SLO 为 99% 请求 < 200ms,故 \((1 - \text{SLO}) = 0.01\)


附录 C:合成探测 SLI

Black-box 探测适合无侵入验证用户路径:

# prometheus/blackbox_exporter 模块
modules:
  http_2xx:
    prober: http
    timeout: 5s
    http:
      valid_status_codes: [200]
      fail_if_not_ssl: true
probe_success{job="blackbox-checkout"}

合成 SLI 与服务器 SLI 取交集报告——合成失败但服务器指标正常,通常是 DNS/CDN/网络问题。


附录 D:SLO 与变更事件的关联

每次部署打 CloudEvents 到 Loki 或 Prometheus loki 不适用时用 ALERTS 关联:

# 部署后 15 分钟内 Burn Rate 上升 → 自动关联变更
slo:checkout:burnrate5m
and on()
  increase(deployment_timestamp[15m]) > 0

Grafana Annotation 来自 CI webhook 是更常见的工程实现。


附录 E:错误预算的数学推导(30 天窗口)

设窗口长度 \(W = 30 \times 24 = 720\) 小时,SLO 可用性 \(S = 0.999\),错误率预算 \(\epsilon = 1 - S = 0.001\)

正常消耗速率(占窗口总预算的比例/小时):\(\epsilon / W\)

\(T\) 小时内以 Burn Rate \(B\) 消耗,预算消耗比例:

\[\Delta = \frac{B \cdot \epsilon \cdot T}{W \cdot \epsilon} = \frac{B \cdot T}{W}\]

\(T = 1\) h,\(B = 14.4\)\(W = 720\)

\[\Delta = 14.4 / 720 = 0.02 = 2\%\]

与 Workbook 表格一致。


附录 F:Prometheus 高可用与 SLI 连续性

SLI 采集中断会导致”假合规”——故障期间无数据,Budget Remaining 曲线水平不变。

缓解措施:

  1. Thanos/Mimir remote write 跨 AZ
  2. 双 Prometheus scrape 同一 target,or 合并查询
  3. Synthetic SLI 独立故障域
# 双源合并
(
  sum(rate(http_requests_total{status=~"5.."}[5m]))
  /
  sum(rate(http_requests_total[5m]))
)
or
(
  sum(rate(http_requests_total{status=~"5.."}[5m] offset 1m))
  /
  sum(rate(http_requests_total[5m] offset 1m))
)

or 仅作短时 failover 示意——生产应用 recording rule 封装并在无数据时 alert absent()


附录 G:MySQL / Redis 作为依赖的 SLI 注记

依赖 SLI 不单独 Page——除非该依赖是同步阻塞且直接映射用户错误。


附录 H:SLO 文档模板(摘录)

## Service: checkout-api
- **Owners**: team-checkout, SRE liaison @sre-oncall
- **SLO Window**: 30d rolling
- **SLI-1 Availability**: non-5xx, non-timeout / total ≥ 99.9%
- **SLI-2 Latency**: fraction of requests < 200ms ≥ 99%
- **Exclusions**: 429, 401, healthcheck /metrics
- **Error Budget Policy**: see wiki/EBP-checkout
- **Dashboard**: grafana/d/checkout-slo
- **Runbook**: wiki/runbook/checkout-slo-burn

附录 I:与 incident playbook 的衔接

真实事故复盘剧本 的第一步是 SLO Burn Rate Page——值班工程师应能在 Grafana 上同时看到:

  1. 当前 Burn Rate(1h/6h)
  2. 预算剩余
  3. 最近变更事件

Runbook 中”是否 rollback”的决策点与 Error Budget Policy 的 < 20% 冻结规则对齐。


附录 J:常见问题

Q:能否对 batch job 设 SLO?

可以,用新鲜度 SLI(数据滞后 < N 小时)或吞吐 SLI(每日处理 ≥ N 条)。Burn Rate 公式相同,但窗口常取 7d 而非 30d。

Q:SLO 未达标是否扣团队 KPI?

工程建议:SLO 驱动流程(停发版),而非惩罚个人——否则团队会瞒报或调低 SLO。

Q:Serverless 如何埋 SLI?

用 API Gateway / Lambda 内置指标 + OTel span metrics;冷启动延迟纳入延迟 SLI 分母。

Q:多 region 如何聚合 SLO?

用户流量权重加权错误率,而非简单平均各 region SLI——否则低流量 region 拉低全局。


附录 K:版本与工具说明


附录 L:Burn Rate 阈值速查(SLO=99.9%,30 天 = 720h)

窗口 \(T\) 燃烧率 \(B\) 预算消耗 \(\Delta = B \cdot T / 720\) Workbook 级别
1h 14.4 2.00% Page A
6h 6 5.00% Page B
3d 1 10.00% Page C / Ticket F
6h 1 0.83% Ticket D
1d 1 3.33% Ticket E

低 QPS 服务应加最小流量门槛,避免单条 5xx 触发 Page:

(
  sum(rate(http_requests_total{job="checkout",status=~"5.."}[1h]))
  / sum(rate(http_requests_total{job="checkout"}[1h]))
  / 0.001 > 14.4
)
and
sum(rate(http_requests_total{job="checkout"}[1h])) > 0.1

RED 中的 Errors rate 是 Burn Rate 分子;Duration 用于延迟 SLI(附录 B)。指标命名与 指标体系 保持一致。


附录 M:Error Budget Policy 全文模板

以下模板可直接放入内部 wiki;{SERVICE}{SLO} 替换为实际值。

# Error Budget Policy — {SERVICE}

## 1. 适用范围
- 服务:{SERVICE}
- SLO 窗口:30 天滚动
- 可用性 SLO:{SLO}(非 5xx、非超时 / 总请求)
- 延迟 SLO(如有):99% 请求 < {LATENCY_MS}ms

## 2. 预算分区与工程动作
| 剩余预算 | 发布 | 变更审批 | On-call |
|----------|------|----------|---------|
| > 50% | 正常 | TL | 常规 |
| 20–50% | 仅 bugfix / 可靠性 | TL + SRE | 加强巡检 |
| < 20% | **冻结 feature** | VP Eng + SRE Lead | 事故态 |
| 0% | 仅 hotfix / 回滚 | 同上 + PM | 复盘 mandatory |

## 3. Override
- 谁可批准:VP Engineering + SRE Lead 双签
- 最长 override:72 小时
- 必须记录:Jira ticket、原因、预计额外预算消耗

## 4. 大促冷藏
- 开始前 4 周进入冷藏;见 §5.2

## 5. 告警与 Runbook
- Page:Burn Rate 规则见 slo-alert-rules.yml
- Runbook:{RUNBOOK_URL}
- Dashboard:{GRAFANA_URL}

## 6. 月度评审
- 参与:TL、PM、SRE、代表开发
- 输出:SLI 质量报告、超预算根因分类、下月动作项

附录 N:OpenTelemetry Span Metrics 生成 SLI

已全量 OTel、未手工维护 http_requests_total 的团队,可用 Collector spanmetrics connector 生成 RED 指标供 SLO 使用。

# otel-collector-config.yaml(摘录)
connectors:
  spanmetrics:
    histogram:
      explicit:
        buckets: [0.05, 0.1, 0.2, 0.5, 1, 2, 5]
    dimensions:
      - name: http.method
      - name: http.status_code
    dimensions_cache_size: 1000

service:
  pipelines:
    traces:
      receivers: [otlp]
      exporters: [spanmetrics]
    metrics:
      receivers: [spanmetrics]
      exporters: [prometheus]

映射到 SLI

# 由 spanmetrics 导出的 calls_total
sum(rate(calls_total{service_name="checkout",http_status_code=~"5.."}[5m]))
/
sum(rate(calls_total{service_name="checkout"}[5m]))

注意


附录 O:SLO 分阶段落地路线图(90 天)

阶段 周次 交付物 成功标准
0 对齐 W1–2 SLI 定义文档、产品签字 “坏请求”无歧义
1 可见 W3–4 Recording Rules + Grafana Panel 1–4 TL 能口述剩余预算
2 告警 W5–6 6 条 Alert + Alertmanager route 混沌注入触发 Page(演练)
3 流程 W7–8 EBP 文档、月度评审日历 一次模拟冻结发布
4 扩展 W9–12 多服务复制、DB Ticket 联动 ≥3 个关键服务同上

W5 混沌演练建议(不编造具体数字,只描述方法):


附录 P:常见误解(扩展)

误解 事实
“SLO 达标 = 用户满意” SLO 只覆盖已选 SLI;未覆盖的功能抱怨仍会发生
“预算没用完 = 发布太少” 预算是风险容量,不是 KPI 必须花完
“三个 9 和四个 9 差不多” 预算差 10 倍(§1.2)
“Burn Rate 告警替代所有监控” 仍需容量、安全、依赖 Ticket(告警体系
“一个全局 SLO 管所有微服务” 用户 facing 与内部服务分层(附录 A)

附录 Q:与存储成本的衔接

错误预算紧张时常伴随 Trace/Log 全量采集 撑爆存储——Burn Rate 高时若同时开启 100% trace sampling,会加剧故障(Collector OOM)。建议:

SLO 与成本不是对立——用 SLO 决定何时值得花 observability 预算


附录 R:多服务 Recording Rules 目录结构

规模化后不宜所有规则堆在单文件。推荐仓库布局:

observability/
  slo/
    recording/
      checkout.yaml
      payment.yaml
      search-indexer.yaml
    alerts/
      checkout-pages.yaml
      checkout-tickets.yaml
    tests/
      checkout_test.yaml
    lib/
      burnrate.lib.yaml   # 可选:jsonnet 生成

checkout.yaml 片段(含可用性 + 延迟双 SLI)

groups:
  - name: slo_checkout_recording
    interval: 30s
    rules:
      - record: slo:checkout:availability:ratio5m
        expr: |
          1 - (
            sum(rate(http_requests_total{job="checkout",status=~"5.."}[5m]))
            / sum(rate(http_requests_total{job="checkout"}[5m]))
          )
      - record: slo:checkout:latency:ratio5m
        expr: |
          sum(rate(http_request_duration_seconds_bucket{job="checkout",le="0.2"}[5m]))
          / sum(rate(http_request_duration_seconds_count{job="checkout"}[5m]))
      - record: slo:checkout:burnrate_availability:1h
        expr: |
          (1 - slo:checkout:availability:ratio5m)
          / 0.001
        # 注意:应用 1h rate 而非 5m 预聚合直接除——生产应用独立 1h 窗口 rule

命名约定slo:{service}:{sli_name}:{window},避免与业务自定义 job 指标冲突。

Thanos / Mimir 全局视图:Recording Rule 在 数据写入侧 Prometheus 执行;Global SLO Dashboard 通过 Thanos Query 对 slo:*sum 时注意 不要跨 region 重复 scrape 导致双计数——仅在一个 replica 上跑 rule 或对 federation 源做 dedup。


附录 S:与产品对齐 SLI 的会议议程(60 分钟)

SLO 工程最难的是对齐,不是 PromQL。建议首次会议议程:

时间 议题 产出
0–10min 用户旅程:何时觉得”坏了” 候选 SLI 列表
10–25min 逐 SLI 定义”好/坏”边界 排除 429/401 等书面确认
25–35min 选 SLO 数字 + 算错误预算表(§1.2) 签字 SLO 目标
35–45min 延迟阈值:p99 vs “占比” 单一延迟 SLI 口径
45–55min Error Budget Policy 冻结/override 责任人
55–60min Dashboard 首版日期、评审会 cadence 日历邀请

避免:会议中直接拍 99.99%——先用 §1.2 表格展示四个 9 的 downtime 等价,再决定。

避免:把”业务 KPI”(GMV、转化率)与 SLI 混为一谈——KPI 可关联分析,但不替代 SLI。


附录 T:Recording Rule 性能与 cardinality

每条 Recording Rule 每 30s 评估一次,规则数量 × series 数 = 额外 TSDB 写入。

实践 原因
仅对 用户 facing 服务建 slo:* rule 内部服务 hundreds 个会膨胀
interval: 30s 与 scrape 对齐 避免重复计算
sum() without 高基数 label job 聚合,不加 pod
长窗口 burnrate 用 recording 链 burnrate1h 依赖 5m 中间结果可选,但 1h 应独立 rate()[1h] 更准确

诊断慢查询prometheus_engine_query_duration_secondsrule_group=slo_checkout_recording 的 p99 升高时,检查是否误加 path label 到 http_requests_total——见 埋点哲学


附录 U:SLI 合规报告(月度模板)

# {SERVICE} SLO 报告 — {YYYY-MM}

## 摘要
- 可用性 SLO:{SLO} | 实际:{ACTUAL}% | 达标:是/否
- 延迟 SLO:{LAT_SLO} | 实际:{LAT_ACTUAL}% | 达标:是/否
- 错误预算消耗:{BURN_pct}%

## 超预算事件
| 日期 | 持续 | 根因分类 | 链接 |
|------|------|----------|------|
| ... | ... | 发布/依赖/容量/外部 | JIRA-xxx |

## 告警统计
- Page 次数:{N_page} | 误报:{N_fp}
- Ticket 次数:{N_ticket} | 未处理:{N_open}

## 下月动作
- [ ] ...

报告数据应来自 Grafana PDF 或 slo:* 指标自动导出——手工填数易与 Dashboard 不一致。


附录 V:increase() 与低 QPS 服务的陷阱

increase(http_requests_total[30d]) 在 Prometheus 中对 低 QPS 服务使用 rate() 外推,统计误差大:

最小流量门槛(与附录 L 呼应):

sum(rate(http_requests_total{job="checkout"}[1h])) > 0.1  # 至少 0.1 req/s 才评估 Burn Rate

低 QPS 内部 admin API 可改用 7d 窗口 + 更低 Page 阈值 或仅 Ticket——Workbook 6 规则主要面向 >1 req/s 的在线服务。


附录 W:参考资料补充

  1. Robey, Peter, et al., “Alerting on SLOs”, Google SRE Workbook, 2018, https://sre.google/workbook/alerting-on-slots/ (Workbook 告警章节在线版)

  2. Pyrra, https://github.com/pyrra-dev/pyrra — 开源 SLO 规则生成器,可对照本文 PromQL 模板

  3. Sloth, https://sloth.dev/ — SLO 规则生成 CLI,支持 Prometheus Operator PrometheusRule CRD

  4. CNCF, OpenSLO specification, https://openslo.com/ — 厂商中立的 SLO 定义格式,可与 Sloth/Pyrra 互操作

  5. 本站 告警体系存储与成本 — 治理链下两篇

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-13 · architecture

【系统架构设计】SLO 工程:可靠性的量化管理

SLI、SLO、SLA 不只是运维指标——它们是架构决策的定量依据。本文从 Google SRE 的 Error Budget 策略出发,拆解多窗口燃烧率告警的数学原理,讲清楚 SLO 如何在产品与工程的冲突中充当仲裁者,并给出基于 Prometheus 和 Grafana 的落地方案。

2026-04-22 · architecture / observability

可观测性工程

从 Metrics、Logs、Traces 到 Profiling、eBPF、OpenTelemetry 与 SLO 治理,面向中国工程团队的可观测性系统化手册。全 25 篇。


By .