35.1 可用性定义与 9 的换算

一句话

可用性只有两个可调的量——多久坏一次(MTBF) 和 坏了多久能好(MTTR);“几个 9” 就是把这两个量换算成年停机时间,凡是换算不成时间的可用性目标,都是口号。

三个口径:不先说清哪个,数字就没有意义

同一个系统,三种算法能差出两个 9:

口径 公式 99.9% 意味着 适合 失真方式
时间口径 可用时长 / 总时长 一年停 8h45m36s 对外承诺、合同 停机 1 秒和停 1 小时都记 1 次,看不出严重程度
请求口径 成功请求 / 有效请求 千分之一的请求失败 技术 SLI、SLO 一个用户被永久打挂也只占千分之一
容量口径 实际服务能力 / 额定能力 全月有 0.1% 的时间只能扛 50% 流量 弹性、降级评估 部分降级算不算“不可用”,没有共识

三个口径的差别不是小数点,是责任边界:

  • 用时间口径承诺、用请求口径考核,是最常见的错位——月初挂了 40 分钟(时间口径已用掉 90% 月预算),但请求成功率因为重试兜底仍是 99.95%,两边永远吵不完。
  • 反过来用请求口径承诺、用时间口径赔钱,客户会被“我们只挂了 40 分钟”这种说法激怒。

🔑 先定口径,再定 9。合同里写“月度可用性 99.9%”,却没写按请求还是按时间、没写健康检查算不算、没写计划内维护算不算——这份合同在出事那天一定会变成争议而不是依据。

口径定完,接下来才是把它变成可执行的预算——那套账(SLO、告警线、冻结政策)在 35.2 SLA / SLO / SLI / 错误预算。

一个公式定生死:MTBF / (MTBF + MTTR)

可用性 A = MTBF / (MTBF + MTTR)
        = 平均无故障时间 / (平均无故障时间 + 平均修复时间)

这个公式把“提升可用性”这件事压成了两个动作:

动作 手段 成本 适合谁
拉长 MTBF 冗余、隔离、减少依赖、消灭单点 高(要加机器、改架构) 故障频繁的系统
缩短 MTTR 自动切换、快速回滚、可观测、演练 低(多数是工程纪律) 绝大多数团队

MTTR 的性价比远高于 MTBF:把年故障次数从 12 次降到 6 次,需要砍掉一半的故障源;把单次修复从 60 分钟压到 10 分钟,只需要把“发现靠用户投诉、决策靠群聊投票、执行靠手工脚本”换成“告警自动触发 + 预案自动执行”。前者是架构改造,后者是流程改造。

灾备侧的停机走的是另一套口径:一次故障丢多少数据、停多久,见 36.1 RTO / RPO。

⚠️ 但 MTTR 有个下限:它不能小于“检测 + 判决”的时间。这就是为什么 35.4 故障检测 里那些“心跳周期”“φ 阈值”看起来是细节,实际上直接决定了可用性的天花板。

9 的换算尺

按 365 天、时间口径算:

可用性 俗称 年停机 月停机 日停机 每月故障 1 次时的 MTTR 上限
99% 2 个 9 87h36m00s 7h18m00s 14m24s 7h18m00s
99.9% 3 个 9 8h45m36s 43m48s 1m26s 43m48s
99.95% — 4h22m48s 21m54s 43s 21m54s
99.99% 4 个 9 52m34s 4m23s 9s 4m23s
99.999% 5 个 9 5m15s 26s 1s 26s

这张表要读出三件事:

  1. 每多一个 9,可用时间除以 10,而被允许的单次故障时长也除以 10。99.99% 意味着“每月只能挂 4 分 23 秒”——一次 5 分钟的发布事故就超标。
  2. “年停机 8 小时”听着宽松,落到月就是 43 分钟。这就是为什么 99.9% 的系统仍然需要 7×24 值班:不是 8 小时很多,是 8 小时被切成了 12 份 43 分钟。
  3. 最后那个 9 最贵。同一个“多一个 9”,在 2 个 9 的阶段靠加一台机器就够了,在 4 个 9 的阶段要改复制协议、要自动化切换、要冻结发布窗口。成本曲线不是线性的,见 36.4 两地三中心与多活 里按拓扑算的边际成本。

从 9 反推 MTTR 预算

这一步是把“目标”翻译成“运维纪律”,也是绝大多数团队漏掉的一步。给定可用性目标和年故障次数,单次修复的时间上限是:

单次 MTTR 上限 = 年停机预算 / 年故障次数 = (1 − A) × 8760h / N
目标 年故障 1 次 年故障 4 次 年故障 12 次
99.9% 8h45m36s 2h11m24s 43m48s
99.99% 52m34s 13m08s 4m23s

这张表是给运维和 on-call 看的,不是给架构师看的:99.99% + 每月一次故障 = 每次只有 4 分 23 秒,扣掉 1 分钟检测、1 分钟判决,真正留给你“恢复”的时间不到 2 分钟。这个目标下,人工 SSH 上去看日志是不可能的,必须是自动切换 + 自动回滚。

🔑 可用性目标必须和“年故障次数”一起定。只写“99.99%”,等于隐含假设了“一年坏几次”,而这个假设通常比目标本身更离谱。

串联相乘:为什么拆微服务会让可用性下降

链路上任何一环挂掉都导致整体不可用,就是串联,可用性相乘:

A_total = A₁ × A₂ × … × Aₙ
依赖个数(每个 99.99%) 整体可用性 年停机
2 99.980001% 1h45m07s
5 99.950010% 4h22m45s
10 99.900045% 8h45m22s
20 99.800190% 17h30m12s
50 99.501223% 43h41m34s

10 个 99.99% 的依赖串起来,整体就只剩 99.9%。 这不是运维不行,是算术:每次拆分服务都在乘一个小于 1 的数。

这解释了一个反直觉现象:一个单体(假设 99.99%)拆成 10 个微服务,每个都做到 99.99%,整体可用性却从 99.99% 掉到 99.9%——拆分本身让可用性下降了一个 9,除非同时做了降级(依赖挂了我还能用默认值)或冗余(依赖也做多副本,把单个依赖的可用性推到 99.999%)。

⚠️ 同步调用是串联,异步消息是“部分解耦”:下游挂了,生产方仍然成功(可用性不串联),代价是积压与延迟。这就是 43.5 积压与背压 里“异步化能换可用性,但要还延迟的债”的由来。

并联:冗余的收益是乘性的,但只在故障独立时成立

n 个副本并联,全部挂掉才不可用:

A_total = 1 − (1 − A)ⁿ
副本数(每个 99%) 整体可用性 年停机
1 99% 87h36m00s
2 99.99% 52m34s
3 99.9999% 32s
4 99.999999% < 1s

两个 99% 并联就是 99.99%,看起来是“免费的两个 9”。但这条曲线有一个致命前提:故障必须相互独立。工程上把“独立”真正做出来,就是主备 / 双活 / 多活要解决的问题,见 35.3 冗余 (主备 / 双活 / 多活)。

现实中副本往往共享同一个故障源:同一个 K8s 集群、同一个数据库、同一个可用区、同一份配置、同一次发布。用相关系数 ρ 表示“两个副本一起挂”的比例(ρ=0 完全独立,ρ=1 完全同生共死):

相关度 ρ 双副本整体可用性 年停机
0.00 99.999900% 32s
0.50 99.949950% 4h23m04s
0.90 99.909990% 7h53m06s
0.99 99.900999% 8h40m21s
1.00 99.900000% 8h45m36s

ρ 从 0 涨到 0.5,两个 9 的收益就没了 96%(年停机从 32 秒变成 4 小时 23 分)。而真实的“两个副本”默认 ρ 就不低:同机房、同编排、同发布流水线。

🔑 冗余买到的不是“多一个副本”,是“多一个独立的故障域”。判断标准很硬:能不能在不影响另一个副本的前提下,把这块硬件/这个可用区/这次发布干掉。不能,就是假冗余。

平均值的陷阱:99.9% 有三种形状

同样报 99.9%,用户的体感可能天差地别:

形状 表现 达标吗 用户感受
全站抖动 每天有 1 分 26 秒全站 500 达标 偶尔卡一下,能忍
部分用户永久坏 1000 个用户里有 1 个永远打不开 达标 那个人会卸载,且到处说产品坏了
部分功能永久坏 支付能用,退款永远失败 达标 财务每天手工处理,看着像能用

这三种在请求口径下都是 99.9%,但只有第一种是“可用性”,后两种是“打了折的可用性”。

所以对用户可感知的核心链路,还要加两条约束:

  1. 按用户群分档:大客户/付费用户单独算 SLO,不能和长尾用户混在一个平均值里。
  2. 按功能分档:下单、支付、登录这类链路单独定目标,不要用“全站平均 99.9%”糊过去。

五个 9 各需要什么

目标 年停机 必须有的能力 做不到的常见原因
99% 87h 监控 + 手工恢复 没有告警,靠用户投诉
99.9% 8h45m 多副本 + 快速回滚 + on-call 单点数据库、发布不可回滚
99.95% 4h22m 同城多 AZ + 自动摘除 + 演练 故障检测靠人工、DNS 缓存
99.99% 52m 自动切换 + 多活读 + 变更管控 决策要开会、切流靠人
99.999% 5m15s 多地域多活 + 秒级切换 + 变更冻结 成本不可承受,且多数业务不需要

99.99% 是一个分水岭:跨过它,系统的瓶颈从“硬件可靠性”变成“人的反应速度”。而人的反应速度下限是分钟级——所以 99.99% 以上的目标,本质上是在要求“把人从故障处理链路里拿掉”(见 35.9 自动切换与故障恢复)。

目标定完,日常怎么记账——错误预算、燃烧率、多窗口告警——见 50.7 SLO 与错误预算。

可运行的可用性计算器

把上面的公式做成一个可以套自己系统的程序:串联、并联、混合拓扑、反推 MTTR、以及相关性对并联收益的侵蚀。

package main

import (
	"fmt"
	"time"
)

// series 计算串联可用性:任何一环挂,整体挂。
func series(as ...float64) float64 {
	p := 1.0
	for _, a := range as {
		p *= a
	}
	return p
}

// parallel 计算并联可用性:全部挂才挂,前提是故障相互独立。
func parallel(as ...float64) float64 {
	q := 1.0
	for _, a := range as {
		q *= 1 - a
	}
	return 1 - q
}

func yearDown(a float64) time.Duration {
	return time.Duration((1 - a) * 365 * 24 * float64(time.Hour))
}

func dur(d time.Duration) string {
	d = d.Round(time.Second)
	return fmt.Sprintf("%dh%02dm%02ds", int(d.Hours()), int(d.Minutes())%60, int(d.Seconds())%60)
}

func makeN(n int, a float64) []float64 {
	out := make([]float64, n)
	for i := range out {
		out[i] = a
	}
	return out
}

func main() {
	fmt.Println("== 1. 单个 9 的换算 ==")
	for _, a := range []float64{0.99, 0.999, 0.9995, 0.9999, 0.99999} {
		d := yearDown(a)
		// 每月 1 次故障时,单次修复的时间上限 = 年预算 / 12
		fmt.Printf("%-8.5f 年停机 %14s 月停机 %12s 日停机 %10s MTTR上限 %12s\n",
			a, dur(d), dur(d/12), dur(d/365), dur(d/12))
	}

	fmt.Println("\n== 2. 串联:依赖越多越不可用 ==")
	for _, n := range []int{2, 5, 10, 20, 50} {
		a := series(makeN(n, 0.9999)...)
		fmt.Printf("%2d 个 99.99%% 串联 → %.6f%% 年停机 %s\n", n, a*100, dur(yearDown(a)))
	}

	fmt.Println("\n== 3. 并联:冗余的收益 ==")
	for _, n := range []int{1, 2, 3, 4} {
		a := parallel(makeN(n, 0.99)...)
		fmt.Printf("%d 个 99%% 并联 → %.10f%% 年停机 %s\n", n, a*100, dur(yearDown(a)))
	}

	fmt.Println("\n== 4. 混合:串联里的单点是天花板 ==")
	lb := 0.9999                            // 负载均衡只有 1 台
	app := parallel(0.999, 0.999, 0.999)    // 应用 3 副本,每个 99.9%
	db := parallel(0.999, 0.999)            // 数据库 2 副本,每个 99.9%
	total := series(lb, app, db)
	fmt.Printf("LB(1台 99.99%%) + App(3 副本) + DB(2 副本)\n")
	fmt.Printf("  App 并联 %.9f%%  DB 并联 %.9f%%\n", app*100, db*100)
	fmt.Printf("  整体 %.6f%% 年停机 %s\n", total*100, dur(yearDown(total)))
	total2 := series(parallel(lb, lb), app, db)
	fmt.Printf("  LB 改 2 台并联后:%.6f%% 年停机 %s\n", total2*100, dur(yearDown(total2)))

	fmt.Println("\n== 5. 从 9 反推 MTTR 预算 ==")
	for _, a := range []float64{0.999, 0.9999} {
		for _, f := range []int{1, 4, 12} {
			fmt.Printf("SLO %.4f 且年故障 %2d 次 → 单次 MTTR 上限 %s\n", a, f, dur(yearDown(a)/time.Duration(f)))
		}
	}

	fmt.Println("\n== 6. 独立性假设一破,并联就失效 ==")
	for _, rho := range []float64{0, 0.5, 0.9, 0.99, 1.0} {
		// 相关故障模型:rho 是"两个副本一起挂"占单副本故障的比例
		bothFail := (1-rho)*0.001*0.001 + rho*0.001
		a := 1 - bothFail
		fmt.Printf("相关度 %.2f → 双副本整体 %.6f%% 年停机 %s\n", rho, a*100, dur(yearDown(a)))
	}
}

输出(GO111MODULE=off go run main.go,本机实测):

== 1. 单个 9 的换算 ==
0.99000  年停机      87h36m00s 月停机     7h18m00s 日停机   0h14m24s MTTR上限     7h18m00s
0.99900  年停机       8h45m36s 月停机     0h43m48s 日停机   0h01m26s MTTR上限     0h43m48s
0.99950  年停机       4h22m48s 月停机     0h21m54s 日停机   0h00m43s MTTR上限     0h21m54s
0.99990  年停机       0h52m34s 月停机     0h04m23s 日停机   0h00m09s MTTR上限     0h04m23s
0.99999  年停机       0h05m15s 月停机     0h00m26s 日停机   0h00m01s MTTR上限     0h00m26s

== 2. 串联:依赖越多越不可用 ==
 2 个 99.99% 串联 → 99.980001% 年停机 1h45m07s
 5 个 99.99% 串联 → 99.950010% 年停机 4h22m45s
10 个 99.99% 串联 → 99.900045% 年停机 8h45m22s
20 个 99.99% 串联 → 99.800190% 年停机 17h30m12s
50 个 99.99% 串联 → 99.501223% 年停机 43h41m34s

== 3. 并联:冗余的收益 ==
1 个 99% 并联 → 99.0000000000% 年停机 87h36m00s
2 个 99% 并联 → 99.9900000000% 年停机 0h52m34s
3 个 99% 并联 → 99.9999000000% 年停机 0h00m32s
4 个 99% 并联 → 99.9999990000% 年停机 0h00m00s

== 4. 混合:串联里的单点是天花板 ==
LB(1台 99.99%) + App(3 副本) + DB(2 副本)
  App 并联 99.999999900%  DB 并联 99.999900000%
  整体 99.989900% 年停机 0h53m05s
  LB 改 2 台并联后:99.999899% 年停机 0h00m32s

== 5. 从 9 反推 MTTR 预算 ==
SLO 0.9990 且年故障  1 次 → 单次 MTTR 上限 8h45m36s
SLO 0.9990 且年故障  4 次 → 单次 MTTR 上限 2h11m24s
SLO 0.9990 且年故障 12 次 → 单次 MTTR 上限 0h43m48s
SLO 0.9999 且年故障  1 次 → 单次 MTTR 上限 0h52m34s
SLO 0.9999 且年故障  4 次 → 单次 MTTR 上限 0h13m08s
SLO 0.9999 且年故障 12 次 → 单次 MTTR 上限 0h04m23s

== 6. 独立性假设一破,并联就失效 ==
相关度 0.00 → 双副本整体 99.999900% 年停机 0h00m32s
相关度 0.50 → 双副本整体 99.949950% 年停机 4h23m04s
相关度 0.90 → 双副本整体 99.909990% 年停机 7h53m06s
相关度 0.99 → 双副本整体 99.900999% 年停机 8h40m21s
相关度 1.00 → 双副本整体 99.900000% 年停机 8h45m36s

三个读数,三条结论:

  1. 第 4 组的整体可用性是 99.9899%,只有 “3 个 9 多一点”——App 并联做到 7 个 9、DB 并联做到 5 个 9,全被 1 台 99.99% 的 LB 拖到 4 个 9 以下。串联系统的天花板由最弱的单点决定,冗余必须覆盖到最后一个单点。把 LB 也做成 2 台并联,年停机从 53 分 05 秒降到 32 秒。
  2. 第 5 组说明目标与频率必须一起定:同样是 99.99%,一年坏 1 次有 52 分钟可用,一年坏 12 次只有 4 分 23 秒。定目标时不说故障频率,等于没说。
  3. 第 6 组是本节最该记住的数字:ρ 从 0 到 0.5,双副本的年停机从 32 秒变成 4 小时 23 分。冗余的收益不是被“打折”,是被“腰斩”——同机房双副本的真实 ρ 通常就在 0.5 以上。

反模式

反模式 后果
只写“99.9%”不写口径(时间/请求) 出事后按对自己有利的口径解释,合同与考核互相打脸
可用性目标不区分核心与非核心链路 推荐位挂了和支付挂了记同一个账,资源投错地方
用“年停机 8 小时”安慰自己 摊到每月只有 43 分钟,一次发布事故就用完
把平均成功率当可用性 1% 用户永久不可用也能被平均成 99.9%
只做冗余不做单点排查 3 副本 App + 1 台 LB = 99.99%,钱花在没用的地方
认为“多副本”就等于高可用 共享同一个 K8s/DB/发布流水线,ρ≈1,等于没冗余
用计划内维护窗口凑可用性 客户按 7×24 计费,维护窗口照样是停机
定 100% 或 99.999% 但没人算成本 目标不可达 → 团队学会忽略目标 → 连 99.9% 也守不住
把 MTTR 当作“技术问题” 检测、判决、审批占了 MTTR 的 50% 以上,只优化恢复脚本收效甚微

自检

Q1 你们的服务是 3 个 99.9% 的微服务串联,对外的 SLO 写的是 99.9%。这个目标现实吗?如果要把整体做到 99.9%,每个服务至少要做到多少?

▸ 答案:不现实。串联相乘,3 个 99.9% 的整体是 0.999³ = 99.7003%,年停机 26h15m,只有 “2 个 9 多”。要让整体 ≥ 99.9%,每个服务的可用性至少是 0.999^(1/3) ≈ 99.9667%(年停机约 2h55m)。注意这是“三个服务完全独立”的下限;如果它们共享同一个数据库或同一个集群,实际值会更差,需要按相关性模型重算。

Q2 你们有 2 个副本,单副本可用性 99%,按公式并联后应该是 99.99%。实际线上只有 99.9%。最可能的原因是什么?怎么验证?

▸ 答案:故障不独立,ρ 不为 0。两个副本共享了某个故障源(同机房、同 K8s 集群、同一个数据库主库、同一次配置推送、同一条发布流水线)。验证方法:拉一年的故障记录,看“两个副本是否在同一个时间窗内都不可用”——如果每次故障都是双副本同时挂,ρ≈1,并联公式完全失效(99% 而不是 99.99%)。修法不是加第三个副本,而是消除共享故障源,或用 36.4 两地三中心与多活 里的故障域划分把副本真正隔开。

Q3 老板要求“明年可用性从 99.9% 提到 99.99%”,但预算只够把 MTBF 提升 20%。你能做到吗?给出你的算账过程。

▸ 答案:取决于当前 MTTR。99.9% 年停机 8h45m36s,99.99% 是 52m34s,需要把总停机压到 1/10。如果故障频率不变(年 N 次),就必须把单次 MTTR 压到 1/10;MTBF 提升 20% 只能把停机压到 1/1.2,远远不够。可行路径是三条一起上:① MTBF 提升 20%(停机 ×0.83);② MTTR 从比如 60 分钟压到 10 分钟(×0.17),靠自动切换与演练;③ 减少依赖串联数,把 10 个依赖的链路砍到 5 个。三者相乘约 0.83 × 0.17 × (0.9999^5/0.9999^10),才可能落到 1/10。只做第①条,做不到。

Q4 一个团队报“全年可用性 99.95%,达标”。但用户投诉集中在“退款功能一直不好用”。这两件事矛盾吗?

▸ 答案:不矛盾,这正好暴露了口径问题。退款链路可能只占全部请求的 2%,即使它 100% 失败,全站请求口径的可用性也只掉 2 个点;如果退款请求占比 0.5%,全站指标几乎不动。所以“全站平均可用性”必须配“按核心链路分档的 SLO”:退款属于资金链路,应当单独定 SLO 并按业务笔数(而不是请求数)加权。只看总平均,会把“局部完全不可用”藏进小数点后两位。

关联