36.7 演练与验证

一句话

没演练过的容灾方案等于没有方案;演练的价值不是“证明能切”,而是把 RTO 拆成每一段的实测值,让“人等多久”暴露出来。

先摆三个数字

数字 含义 来源
2–3 倍 不演练的多活,切换时长的真实值是文档估算的 2–3 倍 36.4 两地三中心与多活 的经验值
1/5–1/3 做过恢复尝试的备份里,第一次恢复失败的比例 经验值,口径差异大,只用来看量级
45% 第一轮演练能覆盖的潜在缺陷比例(每轮发现率 0.45 时) 推导,跑一遍「可运行的演练记分卡」可复现

这三个数字指向同一件事:容灾方案的可信度不来自设计文档,来自实测。文档里的 RTO 是“所有环节都按预期工作”时的加法;实测 RTO 是“某个环节卡住、某个人不在、某个权限没开”时的加法。两者的差距不是精度问题,是数量级问题。

演练分级:按保真度分,不按成本分

分级的标准是这次演练离“真实切换”有多近,而不是花了多少钱:

级别 做什么 频率 需要的人 风险 能验证什么 典型产出
L1 桌面推演 给定剧本,各角色口头推演决策 季度 指挥、研发、业务、客服 0 谁拍板、谁通报、通知谁 决策树、联系人表
L2 单组件注入 杀一个实例、断一个依赖、主库只读 60 秒 月 / 季度 SRE、值班 低(有冗余兜底) 自愈、告警、runbook 命令是否还能跑 告警阈值调整、脚本修复
L3 单机房切换(真实流量) 把一个机房的流量切走,跑 30 分钟后切回 半年 全部 + 业务方 + 客服 中(容量必须 N+1) 切流耗时、容量够不够、切回来会不会二次故障 实测 RTO、容量结论
L4 全链路地域切换 整个地域停写、异地接管、数据校验、再切回 年 全部 + 管理层 高(真的在赌) 端到端 RTO、数据一致性、外部依赖 演练记分卡、行动项清单

两处关键差别,是 L3/L4 与低级别演练的分水岭:

  • L3 开始用真实流量。 L1/L2 用“演练环境”或“单实例”,动作再熟练也不代表真实流量下的 DNS 生效、连接池重连、缓存冷启动能过。不上真实流量的切换演练,验证的是“脚本能跑”,不是“业务能活”。
  • L3/L4 必须演回切。 切过去只完成一半:回切时缓存是冷的、复制要追、连接池要重建,故障率往往高于切换本身。只演“切过去”,等于只验证了单程。

“备份成功”不等于“能恢复”

备份是可读性验证:文件存在、大小对、能解开。恢复是可写性验证:库能起、能连、能查、能写、应用能接上。两者之间隔着一整条链路,链路上每一环都可能断。

恢复失败的三大原因 典型表现 演练怎么提前发现
缺权限 恢复账号没有建库/建表权限;对象存储的 KMS 密钥没授权给恢复角色;跨账号拉备份被拒 用“只有恢复权限”的账号跑一次,不要用管理员账号
缺依赖 备份里只有表和数据:VPC、白名单、证书、密钥、扩展插件、时区、locale 都不在备份里 恢复到全新环境(不是“清空表再灌”),并记录所有手工补的步骤
版本不匹配 备份来自 8.0.32,目标集群是 8.0.28;逻辑备份的字符集/排序规则与目标不一致;扩展版本对不上 固定“备份版本 → 可恢复版本”矩阵,升级前先做一次跨版本恢复

三条共同点:它们都不在备份文件里,而在备份文件之外。 备份工具只对“文件内容”负责,不对“能不能用”负责。所以恢复演练的验收标准必须是“应用能读写”,不是“导入没报错”。

演练的度量:把 RTO 切成六段

实测 RTO 是一个总数,总数不可改进。要改进必须切开,切到“每一段有明确起点、终点、负责人”:

阶段 起点 终点 常见占比 主要成本在
检测 故障发生 有人认领告警 2%–5% 告警质量(见 50.5 告警)、心跳灵敏度(见 35.4 故障检测)
决策 认领 有人批准切换 20%–35% 授权、审批层级、怕担责
资源 批准 备侧算力/存储就绪 10%–20% 扩容速度、镜像预热、配额
恢复 资源就绪 数据可用(挂载/回放/校验位点) 10%–15% 数据量、binlog 回放、冷启动
验证 数据可用 核心链路自检通过 5%–15% 校验脚本是否事先写好
切流 验证通过 100% 流量生效 15%–25% DNS TTL、灰度节奏、客户端重连

“资源”和“恢复”这两段的长度,直接由备侧形态决定:冷备要从对象存储把数据拉回来,温备只差起服务,热备只差一次 VIP 漂移。三种形态的差别见 36.3 冷备 / 温备 / 热备。

🔑 最贵的一段几乎总是决策,而它不出现在任何技术指标里。监控能看到“数据库挂了”,看不到“没人敢拍板”。所以演练记分卡必须有一列专门记“人工步骤数”和“这一段的等待时间”。

一张可抄的记分卡模板:

阶段 目标(s) 实测(s) 占比 人工步骤数 失败步骤数 判定
检测 60 0
决策 540 2(审批)
资源 300 1
恢复 240 0
验证 180 1
切流 480 0
合计 1800 100% 对照 RTO

四个核心指标,缺一个就没法比较两轮演练的优劣:实测 RTO(总和)、人工步骤数(人越多越不稳定)、失败步骤数(runbook 跑不通的命令条数)、切换后校验项通过率(数据对不对)。

GameDay 的组织:六步,和一条纪律

步 动作 反例
1 定假设 写清“我们相信 X 情况下能在 Y 分钟内恢复” “演练一下切换”,没有可证伪的假设
2 定爆炸半径 明确这次最多影响多少流量/多少用户 一上来切 100%
3 定停止条件 错误率、P99、资损阈值的具体数字 + 谁有权喊停 只写“情况不对就停”
4 执行 按剧本注入,只做演练相关变更 顺手发个版,无法归因
5 记录时间戳 每个阶段边界记一个时间 事后回忆“大概十来分钟”
6 复盘 行动项带 owner、截止、验证方式,方法见 56.6 无指责复盘 报告只写“演练圆满成功”

一条纪律:必须有人专门掐表。 这个人不参与任何操作,只做三件事:

  1. 独立记录,不采信操作者自报的时间(操作者会说“马上好”,那是主观感受)。
  2. 统一时基,所有时间戳来自同一个时钟源,否则跨团队的时间线对不上,复盘时先吵半小时谁是先动的。
  3. 阶段边界由外部可观测事件定义:切流阶段的结束是“监控上 100% 流量打到新机房”,不是“我执行完了切流脚本”。这两者之间可能差一个 DNS TTL。

掐表的人缺位,演练就退化成“大家觉得还挺顺”——而“觉得顺”不能写进 RTO。

安全边界

边界 做法 不做的后果
爆炸半径 先 1% 流量 / 单单元 / 单只读副本 一次满切,演练变事故
熔断开关 错误率或 P99 越线自动中止,不等人判断 人在开会,指标已经崩了
回滚预案 独立于演练脚本,预先验证过,不需要临时申请权限 回滚也依赖刚刚被破坏的东西
干系人通知 业务、客服、上下游、值班表逐项确认 用户先于公司知道
时间窗 避开大促、结账、重大发布 风险叠加,无法归因

角色分工与护栏清单见 56.2 故障演练。灾备演练额外加一条:必须提前确认“切回来”的窗口,因为 L3/L4 会占用备侧全量容量,占用越久,真实故障撞上来的概率越高。

演练的收敛规律:什么时候该收

每轮演练能发现的问题会递减,因为容易发现的先被发现。用一个简单的模型描述:潜在缺陷 N 个,每轮抓走其中的比例 p,剩下的下一轮继续抓。

  • 第 n 轮发现数 = N × p × (1−p)^(n−1)
  • 累计覆盖 = 1 − (1−p)^n

N=60、p=0.45 时跑出来的曲线(「可运行的演练记分卡」的真实运行结果):第 1 轮 27 个、覆盖 45%;第 2 轮 14.9 个、累计 69.8%;第 5 轮 2.5 个、累计 95.0%;第 8 轮 0.4 个、累计 99.2%。

这条曲线的用途是决定还要不要投入:当某轮增量掉到 2 个百分点以下(这里是第 6 轮,2.3pp 已接近临界),说明“这套剧本”已被榨干——继续加轮次只会发现同类问题的变体。这时正确的动作不是加轮次,而是换场景:换故障类型(机房断电换成网络分区)、换爆炸半径(单机房换成地域)、换时间点(白天换成凌晨)。换个剧本,曲线会重新从 45% 开始掉。

反过来,若连续三轮增量都低、却每次都有“数据不一致”的发现,说明问题不在覆盖面,在切换逻辑本身——该回去改代码。

与混沌工程的分工

三件事,一句话各自归位:

  • 56.1 混沌工程:验证“日常故障下系统是否保持稳态”,可自动化、天天跑,判定靠指标假设。
  • 56.2 故障演练:验证“人、流程、工具能否在时限内恢复”,阶段性、有组织成本,判定靠恢复时间。
  • 灾备切换演练:验证“整体切换的正确性”——切得过去、切过去数据对、切得回来。它比前两者多两件事:真实流量的接管和回切。

混沌问“扛不扛得住”,演练问“救不救得回”,灾备演练问“切得过去、切得回来吗”。三者不能互相替代:混沌实验做得再好,也发现不了“没人有权拍板切换”。

可运行的演练记分卡

输入六段实测耗时与目标 RTO,输出总 RTO、各段占比、瓶颈段、是否达标,以及“删掉审批能省多少”;后半段跑缺陷收敛曲线:

package main

import (
	"fmt"
	"math"
)

type phase struct {
	name   string
	actual int // 实测耗时(秒)
	manual int // 其中人工动作占的秒数(审批、手工执行、人工核对)
}

func pad(s string, w int) string {
	n := 0
	for _, r := range s {
		if r > 0x2E80 {
			n += 2
		} else {
			n++
		}
	}
	for n < w {
		s += " "
		n++
	}
	return s
}

func main() {
	const rto = 1800 // 目标 RTO:30 分钟

	ps := []phase{
		{"检测(告警到认领)", 45, 0},
		{"决策(认领到批准切换)", 620, 480},
		{"资源拉起(备机房扩容)", 300, 60},
		{"数据恢复(挂载 + 回放 binlog)", 240, 0},
		{"验证(5 项校验)", 180, 90},
		{"切流(DNS TTL + 灰度到 100%)", 420, 0},
	}

	total, manualTotal := 0, 0
	bottleneck, bnSec := "", 0
	for _, p := range ps {
		total += p.actual
		manualTotal += p.manual
		if p.actual > bnSec {
			bottleneck, bnSec = p.name, p.actual
		}
	}

	fmt.Println("== 演练记分卡:同城机房整体切换 ==")
	fmt.Printf("%s %8s %8s %8s\n", pad("阶段", 30), "实测(s)", "占比", "人工(s)")
	for _, p := range ps {
		share := float64(p.actual) / float64(total) * 100
		fmt.Printf("%s %8d %7.1f%% %8d\n", pad(p.name, 30), p.actual, share, p.manual)
	}
	fmt.Printf("%s %8d %7.1f%% %8d\n", pad("合计", 30), total, 100.0, manualTotal)

	fmt.Printf("\nRTO 目标 %ds,实测 %ds,差 %+ds → %s\n",
		rto, total, total-rto, map[bool]string{true: "达标", false: "未达标"}[total <= rto])
	fmt.Printf("瓶颈阶段:%s(%ds,占 %.1f%%)\n",
		bottleneck, bnSec, float64(bnSec)/float64(total)*100)

	// 场景 A:只砍两级审批(决策阶段那 480s)
	approval := 480
	// 场景 B:把所有人工动作都换成脚本/预授权
	fmt.Println("\n== 如果压缩人工环节 ==")
	for _, c := range []struct {
		name string
		cut  int
	}{
		{"只删两级审批", approval},
		{"删掉全部人工步骤", manualTotal},
	} {
		after := total - c.cut
		fmt.Printf("%s:%ds → %ds(省 %ds,占原时长 %.1f%%),%s\n",
			pad(c.name, 20), total, after, c.cut,
			float64(c.cut)/float64(total)*100,
			map[bool]string{true: "达标", false: "仍不达标"}[after <= rto])
	}

	// 缺陷收敛:N 个潜在缺陷,每轮抓走 p,剩下的下一轮继续抓
	const N, p = 60.0, 0.45
	fmt.Println("\n== 多轮演练的缺陷收敛(N=60,每轮发现率 p=0.45)==")
	fmt.Printf("%s %8s %10s %10s\n", pad("轮次", 8), "发现数", "累计覆盖", "本轮增量")
	prev := 0.0
	for n := 1; n <= 8; n++ {
		found := N * p * math.Pow(1-p, float64(n-1))
		cov := 1 - math.Pow(1-p, float64(n))
		fmt.Printf("%s %8.1f %9.1f%% %9.1fpp\n",
			pad(fmt.Sprintf("第 %d 轮", n), 8), found, cov*100, (cov-prev)*100)
		prev = cov
	}
}

本机运行结果(GO111MODULE=off go run main.go,Go 1.27.1 darwin/arm64):

== 演练记分卡:同城机房整体切换 ==
阶段                              实测(s)       占比    人工(s)
检测(告警到认领)                   45     2.5%        0
决策(认领到批准切换)              620    34.3%      480
资源拉起(备机房扩容)              300    16.6%       60
数据恢复(挂载 + 回放 binlog)      240    13.3%        0
验证(5 项校验)                    180    10.0%       90
切流(DNS TTL + 灰度到 100%)       420    23.3%        0
合计                               1805   100.0%      630

RTO 目标 1800s,实测 1805s,差 +5s → 未达标
瓶颈阶段:决策(认领到批准切换)(620s,占 34.3%)

== 如果压缩人工环节 ==
只删两级审批        :1805s → 1325s(省 480s,占原时长 26.6%),达标
删掉全部人工步骤    :1805s → 1175s(省 630s,占原时长 34.9%),达标

== 多轮演练的缺陷收敛(N=60,每轮发现率 p=0.45)==
轮次          发现数       累计覆盖       本轮增量
第 1 轮      27.0      45.0%      45.0pp
第 2 轮      14.9      69.8%      24.8pp
第 3 轮       8.2      83.4%      13.6pp
第 4 轮       4.5      90.8%       7.5pp
第 5 轮       2.5      95.0%       4.1pp
第 6 轮       1.4      97.2%       2.3pp
第 7 轮       0.7      98.5%       1.2pp
第 8 轮       0.4      99.2%       0.7pp

四条读数:

  1. 实测 1805 秒,比目标 1800 秒多 5 秒,判“未达标”。 差 5 秒看着像噪声,却是结构性的:这套流程没有余量,备机房扩容慢 60 秒就超 1 分钟。差 5 秒和差 300 秒是同类问题——没有 buffer 的 RTO 等于没有 RTO。
  2. 决策段 620 秒占 34.3%,是瓶颈。 六段里它的技术含量最低(不需要扩容、不需要回放数据),却吃掉三分之一时间。这就是“人等多久”——它不在监控里,只在记分卡里。
  3. 只删两级审批,总时长 1805 → 1325 秒,一步从“未达标”变“达标”,省 26.6%。 换句话说,“多买一个机房”要花一倍预算,而“把审批从两级降到一级”是零成本。大多数团队的 RTO 优化空间在流程里,不在硬件里。
  4. 缺陷收敛第 6 轮增量跌到 2.3 个百分点,第 7 轮 1.2。 这时候继续加轮次的边际收益已经很小,该换剧本了。曲线不是用来“证明做得够多”,是用来判断“什么时候该换个方向投”。

反模式

反模式 后果
只演练非核心链路 切换时最贵的核心链路从未被验证,演练报告“全绿”但主流程切不过去
只桌面推演,从不上真实流量 验证了“大家知道该做什么”,没验证“系统做得到”;DNS、连接池、冷启动全是空白
不掐表,靠事后回忆 RTO 是估的,无法比较两轮改进,也无法定位瓶颈段
不演练回切 单程验证,回切时缓存冷启动 + 复制追上,二次故障比第一次更糟
演练不影响任何人 说明流量没切、依赖没断、没有真实用户受影响——这样的演练不可信
演练报告只写“成功” 没有失败步骤、没有人等时长、没有行动项,下一轮还是同样的断点
用管理员账号做恢复演练 掩盖了权限缺口,真实恢复时才发现恢复角色没有建表权限
演练完不复演同一场景 无法证明改进有效,行动项烂在 backlog 里

关联

自检

Q1 一次演练总时长 1500 秒,RTO 是 1800 秒,能不能判“通过”?

▸ 答案:不能直接判。总时长没超只说明“这次运气好”,还要看两件事:各段有没有超过自己目标的 1.5 倍(单段断点会被更坏的故障放大),以及人工步骤数有没有下降。更要紧的是看余量:1500 对 1800 只留 300 秒,如果瓶颈段是决策(人),下一次换个时间点、换个人值班,波动很容易超过 300 秒。有 buffer 的达标才算达标。而这几段时间靠谁记?靠一个不参与操作的掐表人,用统一时基,阶段边界按监控上的外部事件算,不是按“我执行完了”。

Q2 备份文件能正常解开、导入也没报错,为什么还不能说“恢复成功”?

▸ 答案:解开和导入验证的是“文件可读”,恢复要验证的是“系统可用”。中间还差:库能不能起、账号权限够不够、应用连上后能不能读写、依赖(VPC/证书/密钥/扩展/时区)是否齐、版本是否兼容。验收标准必须是“应用跑通一次核心链路”,不是“导入 0 error”。用管理员账号做恢复演练会把权限缺口整段掩盖掉。

Q3 演练做了六轮,每轮还能发现 2–3 个问题,要不要继续做第七轮?

▸ 答案:看的不是“还能发现几个”,是“增量占整体的比例”。按每轮 45% 的发现率,第六轮的累计覆盖已经到 97%,增量只剩 2.3 个百分点——再演同一套剧本,发现的会是同类问题的变体,边际收益很低。这时该换的是场景(故障类型、爆炸半径、时间点),不是加轮次。但如果这 2–3 个问题里反复出现“数据不一致”,那问题在切换逻辑而不在覆盖面,该回去改代码。

Q4 为什么灾备演练必须包含“回切”,而普通故障演练可以不包含?

▸ 答案:普通故障演练的注入是临时的,删掉注入就恢复了,不存在“回切”这个动作。灾备切换是把流量和数据整体搬到另一侧,搬过去之后系统停在一个新状态上,必须再搬回来才算回到日常——而回切是第二次结构相同的切换,且条件更差:缓存是冷的、复制要追平、连接池要重建、备侧已被占用一段时间。只演单程的方案,等于把最容易出事的那一半留给了真实故障。