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 分钟。

三个补充规则,否则分摊会失真:

  1. 按“影响”而不是“调用量”分:支付接口调用量只有 2%,但挂了就是资损,预算要单独给,不能按 2% 分。
  2. 共享依赖的预算要重复计算:一个 DB 被 5 个服务共用,它的故障会同时吃掉 5 份预算,不能只记一次。
  3. 预算可以借,不能送: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 万

四个读数,四条结论:

  1. 缓冲带是“分钟数”,不是“百分比”:SLA 99.9% 和 SLO 99.95% 之间是 21.6 分钟,这个数直接对应“一次故障的处理时间”。定缓冲时问“我们的 P90 恢复要多久”,而不是“高 0.05 个点够不够”。
  2. 依赖分解的破坏力在 n≥5 时显现:5 个依赖各要 99.98%(月停 8.6 分钟),10 个就要 99.99%(月停 4 分 19 秒)。微服务拆到 10 个以上,如果不做降级,可用性目标是算不过来的。
  3. 赔付覆盖率只有 7.5%:SLA 是折扣条款,不是保险。真正的止损在 SLO 和冗余,不在合同。
  4. 最后半个 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 的观测点(在调用方侧统计,或至少两边对账),并把“共享依赖故障”单独归类,否则预算分摊会变成互相指责。

关联