35.2 SLA / SLO / SLI / 错误预算
一句话
SLA 是赔钱的合同,SLO 是内部的预算,SLI 是记账的凭证;三者之间必须留出缓冲,缓冲的宽度就是“内部已经着火、客户还没感觉”的救火时间。
三层各自的负责人和失效方式
| 层 | 谁定 | 数字从哪来 | 违约后果 | 判断它是否健康的信号 |
|---|---|---|---|---|
| SLI | 工程团队 | 监控系统算出来的比例 | 无(它只是数) | 换个分母,指标能涨 3 个点 → 说明分母是挑过的 |
| SLO | 工程 + 业务 | SLI + 目标 + 窗口 | 内部冻结变更、暂停发布 | 一年从未触碰 → 定得太松;每月都烧完 → 定得太紧 |
| SLA | 法务 + 商务 + 工程 | 合同条款,通常是月费百分比 | 赔付、扣款、解约 | 赔付条款比真实损失小一个数量级 → 它是保险,不是承诺 |
三者的关系不是“同一个数的三种叫法”,而是三个不同的责任主体在对同一个系统做三笔不同的交易:
- 工程对 SLO 负责:数字达不到,是技术问题。
- 商务对 SLA 负责:条款定错了,是商业问题。
- SLI 谁都不直接负责,但它错了,上面两个都变成扯皮——SLI 是唯一需要工程严谨性的东西。分母定义错一次,后面所有算术都白算(写法和常见错误见 50.7 SLO 与错误预算)。
“9”本身怎么换算成年停机、月停机与 MTTR 上限,见 35.1 可用性定义与 9 的换算。
SLA 必须写死的七个条款
一份只写“可用性 99.9%”的 SLA,在出事那天一定会变成争议。七个条款缺一不可:
| # | 条款 | 必须写清 | 不写的后果 |
|---|---|---|---|
| 1 | 测量口径 | 按请求还是按时间;分子分母怎么算;健康检查、探针、压测流量算不算 | 双方各按对自己有利的算法解释 |
| 2 | 统计窗口 | 自然月还是滚动 30 天;跨月故障算哪个月 | 月末故障被推到下月,两边记账不一致 |
| 3 | 排除项 | 计划内维护、不可抗力、客户侧网络、超出配额的流量 | 客户把“你自己的发布”也算进停机 |
| 4 | 违约判定 | 累计低于阈值,还是连续 N 分钟;部分区域故障怎么算 | 一次 1 秒抖动是否触发赔付,无法回答 |
| 5 | 赔付方式 | 赔比例、封顶多少、抵扣下月费用还是现金 | 赔出比合同金额更高的钱 |
| 6 | 数据来源 | 以谁的监控为准;第三方探针;原始日志保留多久 | 争议时没有共同认可的账本 |
| 7 | 分级承诺 | 不同套餐/不同区域给不同 9;核心接口单独列 | 一个 99.5% 的免费套餐拖累整体赔付 |
另外三样常被漏掉、但客户真正在意的东西,建议单独写而不是塞进“可用性”:
| 指标 | 典型承诺 | 为什么不能塞进可用性 |
|---|---|---|
| 延迟 | P99 < 300ms | “请求成功但慢 8 秒”按请求口径是 100% 可用 |
| 数据耐久性 | 11 个 9(每年丢 3.2 秒的数据) | 和可用性差 8 个数量级,混在一起会看不出哪个坏了 |
| 恢复时间 | RTO ≤ 30 分钟 | 可用性只统计“挂了多久”,不区分“30 分钟修好”还是“两天后修好”(RTO 的分解与测算见 36.1 RTO / RPO) |
🔑 注意这张表的落差:数据耐久性通常承诺 11 个 9,而可用性只承诺 3 个 9——这不是笔误,它说明在客户心里“不丢数据”比“不宕机”重要一个数量级。拿可用性的 9 去解释数据安全,是这两类条款最常被混用的地方。
缓冲带:SLO 必须比 SLA 高,但高多少要算
SLA 99.90% → 月停机预算 43.2 分钟 ← 客户开始索赔的线
SLO 99.95% → 月停机预算 21.6 分钟 ← 内部开始冻结发布的线
告警 99.97% → 月停机预算 13.0 分钟 ← 值班开始起床的线
三档数字,三个动作,缓冲宽度是 21.6 分钟和 8.6 分钟。这条缓冲带的用途:
| 缓冲宽度 | 意味着 | 适合 |
|---|---|---|
| < 5 分钟 | 报警即违约,团队永远在救火 | 不该出现 |
| 10–30 分钟 | 有一到两次故障的处理余量 | 多数在线业务 |
| > 2 小时 | 缓冲太厚,SLO 形同虚设 | 只在 99.9% 以下的低目标里合理 |
缓冲不是拍出来的,是故障处理时间的函数:缓冲至少要覆盖“一次典型故障从告警到恢复”的时间(P90 恢复时长)。告警线本身怎么定级、怎么降噪,见 50.5 告警 (分级 / 症状 vs 原因 / runbook / 降噪)。如果 P90 恢复要 25 分钟,缓冲只有 10 分钟,那 SLO 一定守不住。
依赖链上的 SLO 分解
整体目标定完,下一步是把它拆给依赖。串联链路上的可用性相乘,所以反解就是开 n 次方:
A_依赖 = A_整体^(1/n)
| 串联依赖数 | 每个依赖至少需要 | 每个依赖的月停机预算 |
|---|---|---|
| 1 | 99.9000% | 43.2 分钟 |
| 2 | 99.9500% | 21.6 分钟 |
| 3 | 99.9667% | 14.4 分钟 |
| 5 | 99.9800% | 8.6 分钟 |
| 10 | 99.9900% | 4.3 分钟 |
| 20 | 99.9950% | 2.2 分钟 |
这张表解释了一个常见的组织冲突:业务方要求“整体 99.9%”,而每个依赖团队都觉得自己“99.9% 已经很好了”。10 个依赖各 99.9%,整体只有 99.9%^10 ≈ 99.0%,差了一个 9。要么依赖做到 99.99%,要么把串联链路砍短。
⚠️ 开 n 次方是“平均主义”的分解,只在依赖同等重要时成立。真实系统里不同依赖的调用占比差 10 倍,按占比分配更合理。
错误预算在依赖之间怎么分
按调用占比分,而不是按团队人数分:
| 依赖 | 调用占比 | 分到的月预算 | 折算可用性 |
|---|---|---|---|
| 搜索 | 60% | 25.92 分钟 | 99.9400% |
| 推荐 | 25% | 10.80 分钟 | 99.9750% |
| 风控 | 10% | 4.32 分钟 | 99.9900% |
| 其他 | 5% | 2.16 分钟 | 99.9950% |
这套算法的好处是自动惩罚“高占比 + 低可靠”的组合:搜索占 60% 的流量,就只能分到 60% 的预算,它每多挂 1 分钟,整体就多损失 0.6 分钟。
三个补充规则,否则分摊会失真:
- 按“影响”而不是“调用量”分:支付接口调用量只有 2%,但挂了就是资损,预算要单独给,不能按 2% 分。
- 共享依赖的预算要重复计算:一个 DB 被 5 个服务共用,它的故障会同时吃掉 5 份预算,不能只记一次。
- 预算可以借,不能送:A 团队本月剩余预算“借”给 B 团队做压测,下月要还;直接“送”等于把可靠性变成人情。
错误预算的三种政策
预算本身不是目的,它要能触发动作。常见的三档政策(燃烧率的计算与多窗口告警配置在 50.7 SLO 与错误预算):
| 预算剩余 | 政策 | 具体动作 |
|---|---|---|
| > 50% | 正常 | 按计划发布,可以主动做风险实验(混沌、压测) |
| 10%–50% | 收紧 | 发布需要 SRE 会签;禁止非必要变更;优先修可靠性 |
| < 10% | 冻结 | 只允许修复类变更;启动可靠性专项;复盘每一起消耗 |
政策必须提前写进流程,而不是烧完再开会决定。事后决定的政策,一定是“这次先发完再说”。
SLA 赔付的经济学:它是保险,不是承诺
算一笔账。月费 100 万、某月违约概率 5%、违约赔月费的 10%:
年期望赔付 = 12 × 5% × 10% × 100 万 = 6 万
真实损失 = 年 4 次故障 × 1 小时 × 每小时 20 万 = 80 万
赔付覆盖率 = 6 / 80 = 7.5%
赔付只覆盖真实损失的 7.5%。 这不是条款刻薄,是商业逻辑:SLA 赔付是“违约折扣”,不是“损失赔偿”。指望用 SLA 条款转移可用性风险,方向就错了——真正的风险对冲是 SLO 管理和冗余投入。
再看“多买一个 9”值不值:
| 提升 | 年成本增加 | 年停机减少 | 折合每减少 1 小时停机的成本 |
|---|---|---|---|
| 99.9% → 99.99% | +60 万 | 7.8 小时 | 7.7 万/小时 |
| 99.99% → 99.999% | +240 万 | 0.8 小时 | 308.6 万/小时 |
最后半个 9 的单价是第一段的 40 倍。 决策判据只有一条:
提升收益 = 每小时停机损失 × 年减少的停机小时
提升成本 = 增量基础设施 + 增量人力 + 变更速度损失
买 9 的工程形态(主备 / 双活 / 多活)以及它们各自的切换代价,见 35.3 冗余 (主备 / 双活 / 多活)。
每小时停机损失 20 万的业务,7.7 万/小时买 99.99% 是划算的;308.6 万/小时买 99.999% 不划算,除非停机损失本身涨到 300 万/小时。这个乘法算不出来,就别定 5 个 9。
可运行的 SLO 预算分配器
把缓冲带、依赖分解、预算分摊、赔付期望、买 9 的单价一次算清:
package main
import (
"fmt"
"math"
)
// monthDown 返回 30 天窗口内的停机分钟数。
func monthDown(a float64) float64 { return (1 - a) * 30 * 24 * 60 }
// decompose 把整体目标平摊到 n 个串联依赖:A_dep = A_total^(1/n)。
func decompose(total float64, n int) float64 { return math.Pow(total, 1/float64(n)) }
func main() {
fmt.Println("== 1. 缓冲带:SLA / SLO / 告警线 ==")
sla, slo, alert := 0.999, 0.9995, 0.9997
fmt.Printf("SLA %.4f 月停机 %6.1f 分钟\n", sla, monthDown(sla))
fmt.Printf("SLO %.4f 月停机 %6.1f 分钟 → SLA 与 SLO 缓冲 %.1f 分钟\n", slo, monthDown(slo), monthDown(sla)-monthDown(slo))
fmt.Printf("告警 %.4f 月停机 %6.1f 分钟 → SLO 与告警缓冲 %.1f 分钟\n", alert, monthDown(alert), monthDown(slo)-monthDown(alert))
fmt.Println("\n== 2. 串联依赖的 SLO 分解(整体 99.9%)==")
for _, n := range []int{1, 2, 3, 5, 10, 20} {
d := decompose(0.999, n)
fmt.Printf("%2d 个串联依赖 → 每个至少 %.4f%%(月停机 %.1f 分钟)\n", n, d*100, monthDown(d))
}
fmt.Println("\n== 3. 按调用占比分摊错误预算(总预算 43.2 分钟/月)==")
total := monthDown(0.999)
deps := []struct {
name string
share float64
}{{"搜索", 0.60}, {"推荐", 0.25}, {"风控", 0.10}, {"其他", 0.05}}
for _, d := range deps {
budget := total * d.share // 该依赖可用的停机分钟
fmt.Printf("%-4s 占比 %4.0f%% → 预算 %5.2f 分钟/月(可用性 %.4f%%)\n",
d.name, d.share*100, budget, (1-budget/(30*24*60))*100)
}
fmt.Println("\n== 4. SLA 赔付期望 vs 真实损失 ==")
fee, pBreach, ratio := 100.0, 0.05, 0.10 // 月费万元、违约概率、赔付比例
expect := 12 * pBreach * ratio * fee
lossPerHour, times := 20.0, 4.0
fmt.Printf("月费 %.0f 万、违约概率 %.0f%%、赔付 %.0f%% → 年期望赔付 %.1f 万\n", fee, pBreach*100, ratio*100, expect)
fmt.Printf("年 %.0f 次 × 1 小时 × 每小时 %.0f 万 = 真实损失 %.0f 万\n", times, lossPerHour, times*lossPerHour)
fmt.Printf("赔付覆盖率 %.1f%%\n", expect/(times*lossPerHour)*100)
fmt.Println("\n== 5. 多买一个 9 的单价 ==")
for _, c := range []struct{ from, to, cost float64 }{
{0.999, 0.9999, 60}, {0.9999, 0.99999, 240},
} {
gain := (monthDown(c.from) - monthDown(c.to)) * 12 / 60 // 每年少停机小时
fmt.Printf("%.5f → %.5f:年成本 +%.0f 万,年停机减少 %.1f 小时,每小时 %.1f 万\n",
c.from, c.to, c.cost, gain, c.cost/gain)
}
}
输出(GO111MODULE=off go run main.go,本机实测):
== 1. 缓冲带:SLA / SLO / 告警线 ==
SLA 0.9990 月停机 43.2 分钟
SLO 0.9995 月停机 21.6 分钟 → SLA 与 SLO 缓冲 21.6 分钟
告警 0.9997 月停机 13.0 分钟 → SLO 与告警缓冲 8.6 分钟
== 2. 串联依赖的 SLO 分解(整体 99.9%)==
1 个串联依赖 → 每个至少 99.9000%(月停机 43.2 分钟)
2 个串联依赖 → 每个至少 99.9500%(月停机 21.6 分钟)
3 个串联依赖 → 每个至少 99.9667%(月停机 14.4 分钟)
5 个串联依赖 → 每个至少 99.9800%(月停机 8.6 分钟)
10 个串联依赖 → 每个至少 99.9900%(月停机 4.3 分钟)
20 个串联依赖 → 每个至少 99.9950%(月停机 2.2 分钟)
== 3. 按调用占比分摊错误预算(总预算 43.2 分钟/月)==
搜索 占比 60% → 预算 25.92 分钟/月(可用性 99.9400%)
推荐 占比 25% → 预算 10.80 分钟/月(可用性 99.9750%)
风控 占比 10% → 预算 4.32 分钟/月(可用性 99.9900%)
其他 占比 5% → 预算 2.16 分钟/月(可用性 99.9950%)
== 4. SLA 赔付期望 vs 真实损失 ==
月费 100 万、违约概率 5%、赔付 10% → 年期望赔付 6.0 万
年 4 次 × 1 小时 × 每小时 20 万 = 真实损失 80 万
赔付覆盖率 7.5%
== 5. 多买一个 9 的单价 ==
0.99900 → 0.99990:年成本 +60 万,年停机减少 7.8 小时,每小时 7.7 万
0.99990 → 0.99999:年成本 +240 万,年停机减少 0.8 小时,每小时 308.6 万
四个读数,四条结论:
- 缓冲带是“分钟数”,不是“百分比”:SLA 99.9% 和 SLO 99.95% 之间是 21.6 分钟,这个数直接对应“一次故障的处理时间”。定缓冲时问“我们的 P90 恢复要多久”,而不是“高 0.05 个点够不够”。
- 依赖分解的破坏力在 n≥5 时显现:5 个依赖各要 99.98%(月停 8.6 分钟),10 个就要 99.99%(月停 4 分 19 秒)。微服务拆到 10 个以上,如果不做降级,可用性目标是算不过来的。
- 赔付覆盖率只有 7.5%:SLA 是折扣条款,不是保险。真正的止损在 SLO 和冗余,不在合同。
- 最后半个 9 的单价贵 40 倍:99.99% → 99.999% 每年只少停 0.8 小时,却要多花 240 万。这个数一算出来,“要不要冲 5 个 9”就不再是信仰问题。
反模式
| 反模式 | 后果 |
|---|---|
| SLA 只写“99.9%”,不写口径与排除项 | 出事后按各自有利口径解释,赔付变扯皮 |
| SLO 等于 SLA | 没有救火缓冲,客户感知即违约,团队长期高压 |
| SLO 定 100% | 预算为 0,任何抖动都违约,两周内团队学会忽略它 |
| 依赖分解用“平均主义”不看调用占比 | 低占比依赖分到过多预算,高占比依赖永远不够 |
| 一个共享 DB 的故障只记一次账 | 5 个上游同时超预算,但账本上看不出来是谁的责任 |
| 错误预算没有对应政策 | 烧完了只开会不冻结,预算退化成报表 |
| 用 SLA 赔付额倒推可靠性投入 | 赔付只覆盖真实损失的个位数百分比,会系统性投入不足 |
| SLI 分母含健康检查/压测流量 | 分母虚高,SLO 永远达标,故障被指标掩盖 |
| 把延迟、数据耐久混进“可用性” | 一个数字承担三种承诺,哪个坏了都说不清 |
| 目标由单团队定 | 上游 SLO 决定下游可达性,下游只能背锅 |
自检
Q1 商务签了 SLA 99.9%,工程把 SLO 也定成 99.9%。三个月后客户投诉“你们老是出问题”,但监控显示每月都达标。问题出在哪?
▸ 答案:SLO 等于 SLA,没有缓冲。SLA 是“赔钱的线”,SLO 应该是“内部开始行动”的线,两者相等意味着客户感知到问题的那一刻你才开始救火,此时预算已经烧完,任何动作都只是止损而不是预防。正确做法是把 SLO 抬到 99.95%(月停机 21.6 分钟),并在 99.97% 设告警线,让“告警—处理—恢复”全部发生在客户感知之前。另外要检查 SLI 是否只统计了服务端 5xx——如果是,客户感知的“慢”和“部分失败”根本没进指标。
Q2 链路有 10 个串联依赖,业务要求整体 99.9%。每个依赖团队都说“我们做到 99.9% 已经尽力了”。整体能做到多少?有三条出路,各自的代价是什么?
▸ 答案:10 个 99.9% 串联是 0.999^10 ≈ 99.0045%,年停机约 87 小时,只有 2 个 9。三条出路:① 每个依赖提到 99.99%(月停机 4.3 分钟)——代价是每个团队都要做多副本 + 自动切换,成本乘以依赖数;② 把串联链路砍短(合并服务、本地缓存、异步化),依赖从 10 降到 5,每个只需 99.98%;③ 对非核心依赖做降级(挂了返回默认值),让它从“串联”变成“并联”,可用性不再相乘。现实中三条要一起用:核心链路砍到 3–5 跳,非核心全部可降级,只有剩下的关键依赖才值得做到 99.99%。
Q3 老板问:“我们赔客户的钱一年才 6 万,为什么还要花 60 万把可用性从 99.9% 提到 99.99%?”你怎么回答?
▸ 答案:因为赔付和损失是两笔账。赔付 6 万是合同约定的折扣,真实损失是“故障期间没人下单”(例中 80 万/年),而且还有赔付覆盖不到的部分:客户流失、品牌、SLA 谈判筹码变弱、团队救火占用的人力。判据是 每小时停机损失 × 年减少停机小时 > 增量成本:本例 20 万/小时 × 7.8 小时 = 156 万 > 60 万,所以划算。如果每小时损失只有 3 万,收益 23.4 万 < 60 万,那就该拒绝,转而把钱花在缩短 MTTR 上(更便宜)。
Q4 你们把错误预算按调用占比分给了 4 个依赖,月底发现“搜索”用掉了 80% 的预算,但复盘时搜索团队说“我们的 SLI 是达标的”。最可能的原因是什么?
▸ 答案:SLI 的分母或统计边界不一致。常见三种:① 搜索团队的 SLI 只统计“搜索服务自身返回 200”,而调用方看到的超时(搜索慢到 3 秒)在调用方侧记为失败;② 搜索的 SLI 排除了被上游熔断掉的请求,实际这些请求在用户侧就是失败;③ 共享依赖(同一个 DB/缓存)的故障被记到了搜索头上,而搜索自身确实没坏。修法是统一 SLI 的观测点(在调用方侧统计,或至少两边对账),并把“共享依赖故障”单独归类,否则预算分摊会变成互相指责。
关联
- 前置:35.1 可用性定义与 9 的换算(9 怎么换算成时间)
- 并列:35.3 冗余 (主备 / 双活 / 多活)(用钱买 9 的工程形态)
- 下游:36.1 RTO / RPO(停机预算的灾备侧账本)
- 下游:50.7 SLO 与错误预算(燃烧率与告警规则)、50.5 告警 (分级 / 症状 vs 原因 / runbook / 降噪)