“系统要 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 对应的预算
| SLO | 30 天预算 | 每百万请求允许失败 | 适用场景 |
|---|---|---|---|
| 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 推荐多窗口多燃烧率告警,兼顾灵敏与抗噪:
| 告警级别 | 长窗口 | 短窗口 | 燃烧率 | 预算消耗 | 动作 |
|---|---|---|---|---|---|
| 紧急 | 1h | 5m | 14.4 | 2% | 立即呼叫 |
| 紧急 | 6h | 30m | 6 | 5% | 立即呼叫 |
| 警告 | 1d | 2h | 3 | 10% | 工单 |
| 警告 | 3d | 6h | 1 | 10% | 工单 |
短窗口是为了快速恢复后立即停止告警,避免误报持续打扰。
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 归属、演练常态化 |
一句话记住:错误预算是把"可靠性"翻译成"可计算的成本"的语言。它让团队不再为"要不要发"争吵——预算充足就发,耗尽就修。而故障演练则回答另一个问题:当预算真的被花光时,你的系统还能剩下多少核心能力。这两个问题的答案,才构成了"韧性"的完整定义。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。