韧性工程与错误预算:从 SLO 到故障演练

韧性工程(Resilience Engineering)的落地方法:SLI/SLO/SLA 的关系与错误预算计算、多窗口多燃烧率告警、发布门禁与预算冻结策略、预算消耗归因、混沌工程与游戏日演练流程、超时/重试/熔断/隔离舱等韧性模式与降级兜底设计,以及度量与组织配套。

“系统要 100% 可用"是一句听起来正确、实际上有害的话。它既不可能达成(网络会抖、依赖会挂),又会让团队陷入"不敢发布、不敢变更"的瘫痪。韧性工程(Resilience Engineering)给出的解法是:用可量化的错误预算,把"可靠性"和"交付速度"放进同一个可计算的框架里,再用故障演练主动验证系统真的能扛住故障。本文讲清 SLO 与错误预算的算法、如何把它变成发布门禁,以及故障演练该怎么做。

1. 韧性不是"不出故障”

1.1 韧性与高可用的区别

两者常被混用,但关注点不同:

维度高可用(HA)韧性(Resilience)
目标少出故障、快速恢复出了故障仍能提供核心价值
手段冗余、多副本、故障转移降级、隔离、限流、演练
假设故障是异常故障是常态
度量可用率降级后的核心可用率

高可用是"防",韧性是"扛"。完整的做法是 高可用与容错 打底 + 韧性工程兜底:前者保证多数故障自动转移,后者保证转移失败时仍有降级路径。

1.2 为什么需要可量化的目标

没有量化目标时,可靠性讨论会退化成"感觉"。工程上需要三个层次的定义:

  • SLI(Service Level Indicator):可测量的指标,如"成功请求占比";
  • SLO(Service Level Objective):SLI 的目标值,如"30 天窗口成功率 ≥ 99.9%";
  • SLA(Service Level Agreement):对外的合同承诺,通常比 SLO 宽松,带赔偿条款。
SLA  ≥ 99.0%(对外合同,含赔偿)
SLO  ≥ 99.9%(内部目标,留缓冲)
SLI  = 成功请求 / 总请求(实测值)

SLO 必须比 SLA 严,留出缓冲,否则一旦触及 SLA 就是违约。SLO 与容量规划之间也有直接联系——目标越严,所需的冗余与容量成本越高。


2. 错误预算:把可靠性变成可花的钱

2.1 错误预算的定义

错误预算(Error Budget)= 1 − SLO。若 SLO 是 99.9%,则错误预算是 0.1%——即 30 天窗口内允许的"不达标额度"。

窗口:30 天 = 43,200 分钟
SLO:99.9%
错误预算:0.1% × 43,200 = 43.2 分钟

含义:这 43.2 分钟可以"花"在发布、实验、故障上

这个视角的妙处在于:可靠性不再是"越高越好",而是"够用即可,省下的预算用来交付"。

2.2 不同 SLO 对应的预算

SLO30 天预算每百万请求允许失败适用场景
99%7.2 小时10,000内部工具
99.9%43.2 分钟1,000一般对外服务
99.95%21.6 分钟500关键业务
99.99%4.3 分钟100支付/核心链路

选 SLO 的方法:从用户真实体验出发,而不是拍脑袋定 99.99%。问"用户能感知到的最慢响应是多少",再反推目标。多数业务 99.9% 就够,盲目追 99.99% 会让成本指数级上升。

2.3 燃烧率(Burn Rate)

只看"30 天用掉多少"反应太慢——真出大故障时,等你月底才发现就晚了。燃烧率衡量"预算被消耗的速度相对于可持续速度的倍数":

燃烧率 = 当前错误率 / (1 − SLO)

例:SLO=99.9%,错误预算率=0.1%
    当前 5 分钟错误率 = 2%
    燃烧率 = 2% / 0.1% = 20 倍
    含义:按此速度,30 天预算将在 43.2/20 ≈ 2.16 分钟内烧穿

Google SRE 推荐多窗口多燃烧率告警,兼顾灵敏与抗噪:

告警级别长窗口短窗口燃烧率预算消耗动作
紧急1h5m14.42%立即呼叫
紧急6h30m65%立即呼叫
警告1d2h310%工单
警告3d6h110%工单

短窗口是为了快速恢复后立即停止告警,避免误报持续打扰。

2.4 Prometheus 实现

用 sli:requests:rate5m 之类的录制规则计算 SLI,再算燃烧率:

# 录制规则:5 分钟错误率
groups:
  - name: sli
    rules:
      - record: sli:http_errors:rate5m
        expr: |
          sum(rate(http_requests_total{code=~"5.."}[5m]))
          /
          sum(rate(http_requests_total[5m]))
# 告警:1h/5m 窗口燃烧率 > 14.4
- alert: ErrorBudgetBurnFast
  expr: |
    sli:http_errors:rate1h > (14.4 * 0.001)
    and
    sli:http_errors:rate5m > (14.4 * 0.001)
  labels: { severity: critical }
  annotations:
    summary: "错误预算快速燃烧,1 小时消耗 >2%"

两个窗口同时超阈值才告警,是"多窗口"告警的精髓——既快又不吵。


3. 从预算到行动:发布门禁

3.1 预算策略

错误预算不是看的,是用来做决策的。典型策略:

预算充足(剩余 > 50%):正常发布,可做激进实验
预算收紧(剩余 10%~50%):谨慎发布,暂停非必要变更
预算耗尽(剩余 < 10%):冻结功能发布,只允许修复性变更

3.2 与发布流程绑定

把预算状态接入 CI/CD,作为发布门禁:

# 发布流水线中的预算检查
- name: check-error-budget
  run: |
    budget=$(curl -s "$SLO_API/budget?service=order&window=30d" | jq '.remaining_pct')
    if [ "$budget" -lt 10 ]; then
      echo "错误预算仅剩 ${budget}%,冻结功能发布"
      exit 1
    fi

这样"能不能发"由客观数据决定,而不是靠"我觉得没问题"。它把可靠性从"运维的事"变成"每个开发都要面对的事"。

3.3 预算消耗的归因

预算被花掉时要归因到具体变更,否则无法改进:

消耗来源占比对策
变更引发60%灰度、金丝雀、自动回滚
依赖故障20%熔断、降级、隔离
容量不足12%扩容、压测、容量规划
基础设施8%多云/多可用区

归因数据会揭示一个规律:大多数故障由变更引发——这正是要投资金丝雀发布与自动回滚的原因。


4. 故障演练:主动制造故障

4.1 为什么必须演练

“我们的系统能扛住故障"这句话,只有在真被故障验证过之后才成立。演练的价值:

  • 验证降级路径真的可用(而非纸面存在);
  • 暴露隐藏的单点与超时配置错误;
  • 训练团队的应急响应肌肉记忆;
  • 把"人肉救火"变成"预案执行”。

这与 混沌工程 的核心思想一致:用受控实验建立对系统的信心。

4.2 演练的类型

类型做法强度频率
桌面推演假设故障,走流程低每月
游戏日(GameDay)真实环境注入故障,人工响应中每季度
自动混沌实验持续注入,自动验证稳态高持续
全链路演练跨团队、跨系统最高每半年

4.3 演练的流程

1. 定义稳态假设:正常时 SLI 应满足什么(如成功率 >99.9%)
2. 设计实验:注入什么故障、范围多大、爆炸半径多小
3. 小范围验证:先在预发或单实例上跑
4. 生产灰度:从小流量开始,逐步扩大
5. 观察:SLI 是否保持在稳态范围内
6. 复盘:若退化,找出薄弱点并修复
7. 中止条件:任何时候可一键停止(kill switch)

安全护栏不可省:爆炸半径控制(只影响 1% 流量)、自动中止(SLI 跌破阈值即停)、时间窗口(避开大促)。

4.4 演练清单

一份可执行的演练清单:

experiments:
  - name: 实例故障
    action: kill-pod
    target: order-service
    blast_radius: 1 replica
    hypothesis: "剩余副本接管,成功率保持 >99.9%"
  - name: 依赖超时
    action: inject-latency
    target: payment-gateway
    latency: 3s
    hypothesis: "熔断器打开,订单走降级路径"
  - name: 区域故障
    action: block-az
    target: az-b
    hypothesis: "流量切到 az-a/az-c,无用户可见影响"

每次演练都要有明确的假设——没有假设的演练只是"搞破坏"。


5. 韧性设计模式

5.1 四种基础模式

模式作用关键参数
超时(Timeout)防止无限等待拖垮调用方必须 < 上游超时预算
重试(Retry)应对瞬时故障指数退避 + 抖动 + 上限
熔断(Circuit Breaker)快速失败,防止雪崩错误率阈值、半开探测
隔离舱(Bulkhead)限制故障传播范围独立线程池/连接池

超时预算是最容易被忽略的:一条链路 A→B→C,若 A 的超时是 3s、B 是 3s、C 是 3s,则 A 等 B 时 B 可能已经等 C 花了 3s,A 的 3s 根本不够。正确做法是逐层递减:

A 超时 1000ms
  └─ B 超时 800ms
       └─ C 超时 600ms

熔断与限流的实现细节可参考 熔断器与限流 。

5.2 重试的正确姿势

重试是双刃剑——错误的重点会放大故障。三条铁律:

# 1) 指数退避 + 抖动,避免重试风暴
delay = min(base * (2 ** attempt), max_delay)
delay = delay * random.uniform(0.5, 1.5)   # 抖动

# 2) 只重试幂等的、可恢复的错误
RETRYABLE = {Timeout, ConnectionError, HTTP_503}

# 3) 重试要在熔断器之内,而非绕过它
with circuit_breaker:
    for attempt in range(max_retries):
        try:
            return call()
        except RETRYABLE:
            sleep(delay)

幂等性是重试的前提:非幂等的写操作重试会重复扣款、重复下单。重试必须有幂等键或去重机制。

5.3 降级与兜底

韧性工程的终极目标是"故障时仍提供核心价值"。降级要预先设计并演练:

故障降级策略
推荐服务挂返回热门榜单(缓存兜底)
支付超时允许"稍后支付",先锁库存
评论服务挂隐藏评论区,主流程不受影响
第三方登录挂切换手机验证码登录

关键原则:降级路径必须是"核心流程的一部分",而不是临时写的补丁。


6. 度量与组织配套

6.1 要持续跟踪的指标

指标含义
错误预算剩余当前还剩多少可靠性额度
变更失败率变更引发故障的比例
MTTR平均恢复时间
演练覆盖率关键故障场景被演练过的比例
降级触发次数降级路径是否真的有效

这些指标与 DevOps 效能度量 中的 DORA 指标互相补充:DORA 看交付效率,SLO 看交付质量,两者结合才完整。

6.2 组织配套

韧性工程落不了地,往往是组织问题:

  • 无责复盘(Blameless Postmortem):故障后追因不追责,否则没人敢上报;
  • SLO 归属:每个服务有明确的 SLO owner,而非"大家的";
  • 预算冻结有共识:冻结发布需要管理层支持,否则形同虚设;
  • 演练常态化:把演练排进迭代计划,而不是"有空再做"。

7. 小结

维度要点
目标不是"不出故障",而是"故障时仍可用"
量化SLI → SLO → 错误预算,SLO 必须严于 SLA
告警多窗口多燃烧率,快而抗噪
行动预算接发布门禁,耗尽即冻结
验证混沌工程 + 游戏日,每次带明确假设
设计超时递减、重试幂等、熔断、隔离舱、降级
组织无责复盘、SLO 归属、演练常态化

一句话记住:错误预算是把"可靠性"翻译成"可计算的成本"的语言。它让团队不再为"要不要发"争吵——预算充足就发,耗尽就修。而故障演练则回答另一个问题:当预算真的被花光时,你的系统还能剩下多少核心能力。这两个问题的答案,才构成了"韧性"的完整定义。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「架构」更多文章

  1. 微前端架构:组合、隔离与独立部署
  2. 数据网格(Data Mesh):领域数据产品与去中心化治理
  3. C4 模型与架构文档化实践