36.1 RTO / RPO

一句话

RPO 决定“丢多少数据”、RTO 决定“停多久”,两者都由复制方式与切换流程决定,不由“有没有备份”决定。

四个时刻,两条线段

最后一次确认的数据点        故障发生 T0            服务恢复            数据追平
        │                      │                    │                  │
────────●──────────────────────●────────────────────●──────────────────●────▶ t
        │◀───── RPO ──────────▶│◀────── RTO ───────▶│◀── 追平窗口 ────▶│
         这段写入会丢            用户看到不可用        服务已可用,但读到的
                                                      可能还是旧数据

四个时刻定义清楚,两个指标才不会互相挪用:

时刻 定义 谁决定它
最后一个确认的数据点 备份完成的那一秒,或备端确认收到的那一秒 备份周期 / 复制方式(18.2 同步、半同步、异步)
T0 故障发生 第一个错误请求被处理的时刻,不是告警响的时刻 故障检测能力
服务恢复 能正确处理业务请求(不是“进程起来了”) 切换流程 + 验证
数据追平 备端复制滞后归零 带宽与回放速度
  • RPO 从 T0 往回量,量到最后一个确认的数据点。它是“历史账”:故障已经发生,能丢多少在上一秒就定死了。
  • RTO 从 T0 往后量,量到服务恢复。它是“未来账”:还能靠流程和预置压缩。
  • 追平窗口是两条线之间的缝:服务已恢复但数据还没追上,这段里读到的可能是旧值,写进去可能被回放覆盖,主场在 36.6 切换时的数据一致性。
  • ⚠️ 最常见的口径错误:把 T0 记成“告警响的时刻”。这样算出来的 RTO 天生少了检测那一段,承诺给别人时短了 3–15 分钟。

RPO:四个来源,最差的那个说了算

RPO 不是一个配置项,它是四个来源里最差的那一个。任何一个环节都能让你丢数据,而业务只记得最惨的那次。

# 来源 计算式 示例数字 收口手段
① 备份周期 RPO = 两次备份的间隔 每日一次全备 → 24h 加 binlog 连续归档做 PITR
② 异步复制滞后 积压 ÷ 带宽 4 GiB ÷ 200 MB/s = 20.5s 加带宽、削峰、关键表改半同步
③ 半同步降级瞬间 降级时长 × 写入 TPS 60s × 3000 TPS = 18 万条 降级必须告警 + 自动恢复同步
④ 人为误删 发现延迟 + 备份保留期 最坏 = 保留期 不可变备份、延迟副本、PITR

四个来源的换算都在计算器里跑出来了,逐条读:

  • ① 24 小时:日全备意味着最坏情况丢掉“上次备份之后的全部写入”。这个数字与存储多贵无关,只与备份间隔有关,备份本身怎么做见 36.2 备份策略。
  • ② 20.5 秒:4 GiB × 1024 ÷ 200 MB/s = 20.5s。带宽砍到 50 MB/s 变成 81.9 秒;积压涨到 16 GiB 变成 327.7 秒 ≈ 5.5 分钟。注意滞后是“积压 ÷ 带宽”,而积压又是“写入速率 × 落后时间”——写放大 3 倍,滞后就 3 倍。大促前必须按峰值重算,不能沿用平峰数字。
  • ③ 18 万条:半同步在备库变慢时会自动降级成异步(否则写不进去)。降级的这一分钟里,每一条提交都没有备端确认——此刻备库宕机,这 18 万条全部消失。真正的坑不是降级,是降级了没人知道,一小时后再看,RPO 已经从 0 变成了小时级。
  • ④ 保留期:没有 PITR 时,误删能被恢复的前提是“备份里还有”。保留 7 天、第 8 天才发现,数据就是没了。开了 binlog PITR,误删场景的 RPO 降到 ≈0,但代价转到 RTO 上:重放 50 GB 日志、按 40 MB/s 算是 21 分钟。

🔑 RPO 与 RTO 不是两个独立旋钮。 把误删的 RPO 压到 0,就要付 21 分钟的 RTO;把 RTO 压到分钟级,就要求 RPO 已经接近 0(没有日志可重放)。调一个,另一个必然跟着动。

RTO:七段相加,数据只占三成

一次真实恢复要经过七段,任何一段都能把 RTO 拖长。一套“同城温备 + 有值班表 + 演练过两次”系统的实测值:

阶段 实测 占比 压缩手段 压完
检测(故障 → 确认) 3 min 5.2% 心跳 + φ 值自适应超时,固定 30s 超时换成 2–3 个探测周期 1 min
决策 / 审批(确认 → 批准切) 12 min 20.7% 预案 + 值班授权表,把“请示”变成“值班长按预案拍板” 3 min
资源就绪(机器/网络/依赖) 5 min 8.6% 备环境常驻,容量按故障后 100% 预留 1 min
数据恢复(重放日志/导数据) 18 min 31.0% 增量链缩短、并行重放、跳过无用表 9 min
服务启动与预热 6 min 10.3% 预热脚本 + 就绪探针,别让冷 JVM 直接接满流量 3 min
验证(功能/数据一致/核心链路) 10 min 17.2% 自动化冒烟集,人工点页面换成脚本 3 min
流量切入(DNS/网关/灰度) 4 min 6.9% DNS TTL 压到 60s,网关 1%→10%→100% 放量 2 min
合计 58 min 100% 22 min

🔑 RTO 里只有 31% 在恢复数据,50% 在等人、验证、切流。 所以“换更快的存储”对 RTO 的杠杆极小:存储快一倍,58 分钟只变 49 分钟(省 15.5%);把决策和验证各压一半,58 分钟才变 47 分钟。

冷、温、热三种形态的差别正是这张表里“资源就绪”和“数据恢复”两段的差距,形态本身怎么分见 36.3 冷备 / 温备 / 热备。

换算:单次 RTO 上限 = 年停机预算 ÷ 年故障次数

年停机预算 = (1 − 可用性) × 8760h,单次 RTO 上限 = 年停机预算 ÷ 年故障次数。按年故障 4 次算:

可用性目标 年停机预算 单次 RTO 上限 58 分钟的实测值
99.9% 8.76h 2.19h = 131.4 min 达标(余 73 min)
99.95% 4.38h 1.095h = 65.7 min 达标(余 8 min,很险)
99.99% 0.876h = 52.6 min 0.219h = 13.1 min 超支 4.4 倍

这张表最容易被读错的方向是“年故障次数”。同样是 99.99%,年故障 20 次的系统单次只能停 2.6 分钟——比一次数据库主从切换还短。所以提高可用性有两条腿:压 RTO,或者减少故障次数(前者是灾备,后者是稳定性工程,错误预算的口径见 50.7 SLO 与错误预算,9 的定义见 35.1 可用性定义与 9 的换算)。

这条式子还给出一个硬约束,比“努力优化”有用得多:

🔑 13.1 分钟的预算装不下 18 分钟的数据重放。 即使把检测、决策、预热、验证、切流全部压到 0,也凑不出 99.99%。反推:要做到 13 分钟,数据恢复必须 ≤ 5 分钟,也就是根本没有日志可放——只能靠同步复制 + 热备,此时 RPO 顺带变成 0。两个指标被同一个架构决定,不能分开许诺。

另外:RTO 要看 P90 / P99,不要看均值。一次 6 小时的故障能把 10 次 30 分钟的故障均值抹平到 1 小时,而客户记住的永远是那 6 小时。

五级容灾形态对照

形态 典型 RTO 典型 RPO 成本倍数 年停机(4 次) 适用业务
备份恢复(只有备份,没有备环境) 12–24h 24h 1.0× 48–96h 内部报表、日志归档、可停一天的后台
冷备 8.7h 24h 1.0× 34.8h 有 RTO 要求但预算极紧的边缘系统
温备 1.4h 分钟~小时 1.4× 5.6h 一般业务系统、SaaS 主站
热备 6min 秒级 2.2× 0.4h 交易、支付、下单
多活(单元化) <1min 秒级/0 3.1–5.2× <0.1h 全国性核心链路、地域级故障无感

成本倍数是相对“备份恢复”的月账单,来自同口径的存储 + 计算 + 带宽合计。冷备与备份恢复同为 1.0×,差的是“备环境预置没预置”——那 6 小时的装环境时间买不来,只能靠预置,具体账见 36.4 两地三中心与多活 的拓扑成本表。

翻译成钱:什么时候值得升级

年期望损失 = 每秒损失 × RTO 秒数 × 年故障次数。拿“冷备 8.71h → 热备 0.1h”这一档算,每次省 30996s,年故障 4 次省 123984s:

每秒损失 年期望损失下降 值得花的增量预算上限
10 元/s 124 万元 124 万元/年
100 元/s 1240 万元 1240 万元/年
1000 元/s 1.24 亿元 1.24 亿元/年

决策表:

你的情况 选 关键动作
每秒损失 < 10 元、年故障 < 2 次 备份恢复 / 冷备 备份跨区 + 每季度真恢复一次
每秒损失 10–100 元 温备 环境预置 + 值班表 + 自动化冒烟
每秒损失 > 100 元、承诺 4 个 9 热备 自动检测 + 自动切流 + 容量按 100% 预留
承诺 5 个 9 但年故障 > 6 次 先减少故障次数 灾备再强也只管“恢复得快”,不管“不出事”
已上多活、但跨单元全靠同步调用 回退到“单元内闭环” 先量跨单元调用比例,再决定要不要多活

我的判断是:年营收 1 亿以内、每秒损失不到 100 元的业务,做单元化多活是负收益——同样的预算放在“减少变更故障”和“自动化演练”上,年停机降得更多。

可运行的 RTO / RPO 预算计算器

package main

import (
	"fmt"
	"sort"
)

// 一次恢复的七个阶段(单位:分钟)。数值取自“同城温备 + 有值班表 + 演练过两次”的典型值,
// 换一种形态主要改“资源就绪/数据恢复”,其余阶段受组织能力影响更大。
type stage struct {
	name  string
	min   float64
	lever string // 压缩手段
}

func main() {
	stages := []stage{
		{"检测(故障发生 → 有人/系统确认)", 3, "心跳 + φ 值自适应超时,把固定 30s 超时换成 2-3 个探测周期"},
		{"决策 / 审批(确认 → 批准切)", 12, "预案 + 值班授权表,把“请示”变成“值班长按预案拍板”"},
		{"资源就绪(机器/网络/依赖)", 5, "预置备环境常驻,容量按故障后 100% 预留"},
		{"数据恢复(重放日志/导数据)", 18, "增量链缩短、并行重放、跳过无用表"},
		{"服务启动与预热(JIT/连接池/缓存)", 6, "预热脚本 + 就绪探针,别让冷 JVM 直接接满流量"},
		{"验证(功能/数据一致/核心链路)", 10, "自动化冒烟集,把人工点页面换成脚本 3 分钟跑完"},
		{"流量切入(DNS/网关/灰度放量)", 4, "压低 DNS TTL 到 60s,网关按 1%→10%→100% 放量"},
	}

	var total float64
	for _, s := range stages {
		total += s.min
	}

	// 一、年停机预算与单次 RTO 上限
	const hoursPerYear = 365 * 24.0
	const incidents = 4.0 // 每年故障次数(不含计划内维护)

	targets := []float64{0.999, 0.9995, 0.9999}
	fmt.Println("【一】年停机预算与单次 RTO 上限(年故障 4 次)")
	fmt.Printf("%-8s %11s %11s %12s %9s\n", "可用性", "年停机预算", "单次上限", "单次上限(分)", "实测 58m")
	for _, a := range targets {
		budgetH := (1 - a) * hoursPerYear
		capH := budgetH / incidents
		verdict := "达标"
		if total > capH*60 {
			verdict = "超支"
		}
		fmt.Printf("%-8s %10.2fh %10.1fm %12.1f %9s\n",
			fmt.Sprintf("%.3f%%", a*100), budgetH, budgetH*60, capH*60, verdict)
	}

	// 二、阶段分解:谁在吃 RTO
	fmt.Printf("\n【二】阶段分解:实测 RTO = %.0f 分钟\n", total)
	fmt.Printf("%-34s %8s %7s %9s  %s\n", "阶段", "耗时(分)", "占比", "累计(分)", "压缩手段")
	sorted := append([]stage(nil), stages...)
	sort.Slice(sorted, func(i, j int) bool { return sorted[i].min > sorted[j].min })
	var cum float64
	for _, s := range stages {
		cum += s.min
		fmt.Printf("%-34s %6.0f %6.1f%% %7.0f     %s\n", s.name, s.min, s.min/total*100, cum, s.lever)
	}

	// 三、数据恢复到底占多少
	dataPart := stages[3].min
	peoplePart := stages[0].min + stages[1].min + stages[5].min + stages[6].min
	fmt.Printf("\n真正在搬数据/重放日志:%.0f 分钟,占 %.1f%%\n", dataPart, dataPart/total*100)
	fmt.Printf("等人 + 验证 + 切流:%.0f 分钟,占 %.1f%%\n", peoplePart, peoplePart/total*100)

	// 四、把最大的三段各压一半,能换来什么
	fmt.Println("\n【三】按阶段压缩后的 RTO(每段只压到一半)")
	for _, s := range sorted[:3] {
		saved := s.min / 2
		fmt.Printf("  压 %-30s 省 %4.1f 分 → RTO %.0f 分(年停机 %.1fh,可用性 %.4f%%)\n",
			s.name, saved, total-saved, (total-saved)*incidents/60,
			(1-(total-saved)*incidents/60/hoursPerYear)*100)
	}

	// 五、RPO 的四个来源
	fmt.Println("\n【四】RPO 的四个来源(数值均可推导)")
	// ① 备份周期
	fmt.Printf("① 每日一次全备:RPO = 24.00h(上一次备份到现在)\n")
	// ② 异步复制滞后 = 积压 / 带宽
	fmt.Printf("%-6s %10s %10s %10s\n", "", "积压 1GiB", "积压 4GiB", "积压 16GiB")
	for _, bw := range []float64{200, 50} {
		fmt.Printf("带宽 %3.0fMB/s ", bw)
		for _, g := range []float64{1, 4, 16} {
			fmt.Printf("%9.1fs ", g*1024/bw)
		}
		fmt.Println()
	}
	// ③ 半同步降级瞬间:降级期间提交的事务都在“异步窗口”里
	tps, degradeSec := 3000.0, 60.0
	fmt.Printf("③ 半同步降级 %.0fs、写入 %.0f TPS → 有 %.0f 万条已提交事务处于“无备库确认”状态,备库此刻宕机全丢\n",
		degradeSec, tps, tps*degradeSec/1e4)
	// ④ 人为误删:RPO = 备份保留期;有 PITR 则 RPO≈0,代价是重放时长
	fmt.Printf("④ 无 PITR、每日全备保留 7 天:误删后被发现的时刻决定丢多少,最坏 RPO 24h;\n")
	binlogGB, replayMBps := 50.0, 40.0
	fmt.Printf("   有 binlog PITR(保留 7 天):RPO≈0,但恢复了 %.0fGB 日志 → RTO 多出 %.0f 分钟\n",
		binlogGB, binlogGB*1024/replayMBps/60)
}

在本机 go run 的真实输出(GO111MODULE=off go run main.go,Go 1.27.1 darwin/arm64):

【一】年停机预算与单次 RTO 上限(年故障 4 次)
可用性            年停机预算        单次上限      单次上限(分)    实测 58m
99.900%        8.76h      525.6m        131.4        达标
99.950%        4.38h      262.8m         65.7        达标
99.990%        0.88h       52.6m         13.1        超支

【二】阶段分解:实测 RTO = 58 分钟
阶段                                    耗时(分)      占比     累计(分)  压缩手段
检测(故障发生 → 有人/系统确认)                      3    5.2%       3     心跳 + φ 值自适应超时,把固定 30s 超时换成 2-3 个探测周期
决策 / 审批(确认 → 批准切)                      12   20.7%      15     预案 + 值班授权表,把“请示”变成“值班长按预案拍板”
资源就绪(机器/网络/依赖)                          5    8.6%      20     预置备环境常驻,容量按故障后 100% 预留
数据恢复(重放日志/导数据)                         18   31.0%      38     增量链缩短、并行重放、跳过无用表
服务启动与预热(JIT/连接池/缓存)                     6   10.3%      44     预热脚本 + 就绪探针,别让冷 JVM 直接接满流量
验证(功能/数据一致/核心链路)                       10   17.2%      54     自动化冒烟集,把人工点页面换成脚本 3 分钟跑完
流量切入(DNS/网关/灰度放量)                       4    6.9%      58     压低 DNS TTL 到 60s,网关按 1%→10%→100% 放量

真正在搬数据/重放日志:18 分钟,占 31.0%
等人 + 验证 + 切流:29 分钟,占 50.0%

【三】按阶段压缩后的 RTO(每段只压到一半)
  压 数据恢复(重放日志/导数据)                 省  9.0 分 → RTO 49 分(年停机 3.3h,可用性 99.9627%)
  压 决策 / 审批(确认 → 批准切)              省  6.0 分 → RTO 52 分(年停机 3.5h,可用性 99.9604%)
  压 验证(功能/数据一致/核心链路)               省  5.0 分 → RTO 53 分(年停机 3.5h,可用性 99.9597%)

【四】RPO 的四个来源(数值均可推导)
① 每日一次全备:RPO = 24.00h(上一次备份到现在)
          积压 1GiB    积压 4GiB   积压 16GiB
带宽 200MB/s       5.1s      20.5s      81.9s 
带宽  50MB/s      20.5s      81.9s     327.7s 
③ 半同步降级 60s、写入 3000 TPS → 有 18 万条已提交事务处于“无备库确认”状态,备库此刻宕机全丢
④ 无 PITR、每日全备保留 7 天:误删后被发现的时刻决定丢多少,最坏 RPO 24h;
   有 binlog PITR(保留 7 天):RPO≈0,但恢复了 50GB 日志 → RTO 多出 21 分钟

逐条读这份输出:

  1. 第一段第三行是判决书:99.99% 的单次上限 13.1 分钟,实测 58 分钟超支 4.4 倍。前面两行达标是假象——65.7 分钟只余 8 分钟,一次多一倍的日志重放就破了。
  2. 第二段 31% 与 50%:数据恢复 18 分钟吃掉三成,等人+验证+切流 29 分钟吃掉一半。这两行决定了预算该花在存储还是花在流程上。
  3. 第三段:三段各压一半,最优的那一段(数据恢复)也只省 9 分钟,把 RTO 从 58 分钟拉到 49 分钟;三件事全做完才到 38 分钟(省 9+6+5=20)。靠优化单段到不了 13 分钟,这是结构问题,不是努力问题。
  4. 第四段带宽矩阵:4 GiB / 200 MB/s = 20.5s,带宽降到 50 MB/s 就是 81.9s;同样 50 MB/s 下积压涨到 16 GiB 是 327.7s。RPO 是写入速率的函数,不是配置项。

反模式

反模式 后果
RPO 按“每日备份 24h”回答别人,线上却是异步复制 一个数字糊住两个场景:故障只丢 20 秒、误删丢 24h,量级差 4320 倍;真风险(降级即丢数)反而没人管
RTO 只算数据恢复,不算验证与切流 承诺 30 分钟,真实 58 分钟——差的那 28 分钟全是等人和验证
没演练过就把 RTO 写进合同 首次真实切换的耗时是估算值的 2–3 倍,违约条款直接触发
用平均 RTO 代替 P99 一次 6 小时故障把均值抹平,客户记住的是最长那次
只测“切过去”,不测“切回来” 回切时缓存冷启动 + 数据追上,二次故障比第一次更久
把 RPO=0 当口号,复制拓扑却是单备半同步 备库一挂立刻降级成异步,降级窗口内的提交全在赌命
备份成功 = 备份可用 没做过恢复演练的备份,恢复失败率按经验在一成以上
大促沿用平峰的 RPO 数字 写放大 3 倍,滞后同步放大 3 倍,账在故障那天才算清

关联

自检

Q1 你对客户承诺 RPO ≤ 24h(依据是每日全备),但线上主从是异步复制、滞后约 20 秒。客户问“真出事到底丢多少”,正确答案是什么?为什么“24h”这个承诺本身是错的?

▸ 答案:真实 RPO 是 20 秒,不是 24 小时。异步复制的最后一个确认数据点在 T0 前 20 秒,备份周期只影响“④ 人为误删”这一类场景。承诺 24h 是按备份周期许诺、按复制方式实现——两个数字量级差 4000 倍。正确做法是分开承诺:故障类 RPO = 复制滞后(20s,且要写清是峰值下的值),误删类 RPO = 备份保留期,并各自给收口手段。

Q2 七段耗时合计 58 分钟,其中数据恢复 18 分钟。把存储吞吐提高一倍(18 → 9 分钟),RTO 变成多少?想压到 30 分钟,必须先动哪两段?

▸ 答案:58 − 9 = 49 分钟,只省 15%,因为另外 40 分钟与存储无关。要压到 30 分钟得砍掉 28 分钟,只能从“决策 12 分钟”和“验证 10 分钟”下手(各压到 3 分钟可省 16 分钟),再叠加“资源就绪 5 → 1”和“数据恢复 18 → 9”,才够 30 分钟。结论:RTO 的主要矛盾是人和流程,不是 IO。

Q3 团队目标是 99.99%,年故障按 4 次估;团队同时说“我们的 RTO 是 30 分钟,够满足 4 个 9”。矛盾在哪?如果年故障变成 20 次呢?

▸ 答案:99.99% 的年停机预算是 52.6 分钟,除以 4 次得到单次上限 13.1 分钟,30 分钟超支 2.3 倍。更硬的约束是:13.1 分钟装不下 18 分钟的日志重放,所以 4 个 9 实质上要求同步复制 + 热备(RPO 同时为 0),而不是“把 30 分钟优化到 13 分钟”。年故障 20 次时单次上限只有 2.6 分钟——比一次主从切换还短,此时唯一出路是减少故障次数,灾备已无空间。

Q4 为什么“RPO = 0”不等于“不会丢数据”?举出至少两条路径。

▸ 答案:① 半同步降级:备库变慢时自动退化成异步,降级窗口内的提交没有备端确认,60s × 3000 TPS 就是 18 万条,此刻备库宕机全丢;② 人为误删 / 逻辑错误:同步复制会把 DELETE 同步到备库,RPO=0 反而“保证”了错误被完整复制,只能靠 PITR(此时 RPO 由保留期决定)或不可变备份;③ 脑裂:旧主没被 fencing 掉,两边都接受写,RPO=0 变成“两份都自认为正确”。RPO=0 只对“单机硬件故障”这一类场景成立。