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 分钟
逐条读这份输出:
- 第一段第三行是判决书:99.99% 的单次上限 13.1 分钟,实测 58 分钟超支 4.4 倍。前面两行达标是假象——65.7 分钟只余 8 分钟,一次多一倍的日志重放就破了。
- 第二段 31% 与 50%:数据恢复 18 分钟吃掉三成,等人+验证+切流 29 分钟吃掉一半。这两行决定了预算该花在存储还是花在流程上。
- 第三段:三段各压一半,最优的那一段(数据恢复)也只省 9 分钟,把 RTO 从 58 分钟拉到 49 分钟;三件事全做完才到 38 分钟(省 9+6+5=20)。靠优化单段到不了 13 分钟,这是结构问题,不是努力问题。
- 第四段带宽矩阵:
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 倍,账在故障那天才算清 |
关联
- 前置:35.1 可用性定义与 9 的换算、18.2 同步、半同步、异步(RPO 的物理来源)
- 并列:36.2 备份策略(备份周期决定 RPO 的①)、36.3 冷备 / 温备 / 热备(形态决定 RTO 的量级)
- 下游:36.4 两地三中心与多活(拓扑怎么买 9)、36.6 切换时的数据一致性(追平窗口)
- 并列:50.7 SLO 与错误预算(年停机预算的会计口径)
自检
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 只对“单机硬件故障”这一类场景成立。