没演练过的容灾方案等于没有
到这里,系列已经攒齐了零件:半同步、自动切换、代理改口、备份与 PITR。但零件齐不等于机器能转——机房级故障是唯一一个"所有组件同时被考验、人也被考验"的场景,而人会掉链子:预案文档半年没更新、值班同学没见过真切换、监控里最关键的那个指标恰好没接告警。混沌工程的立场很朴素:与其等故障来验证架构,不如自己策划一场可控的故障去验证。它的纪律性体现在两个词上:实验(有假设、有稳态指标、有对照组)和可回滚(每一枚注入的针都有对应的解毒剂,且解毒动作本身也被演练过)。本篇用两个模拟说明"安全闸"和"回滚"各自长什么样。
机制:把"搞破坏"做成科学方法
一个合格的故障实验有六件套:稳态定义(用业务指标而非资源指标描述"正常":写成功率、P99 时延、从库延迟上界)、假设(“杀掉任一从库不影响写路径"这类可证伪命题)、注入手段(网络隔离/丢包/延迟、断电拔线、CPU/IO 压满、DNS 劫持,工具上 tc netem、Chaos Mesh、Blade 都能承担)、爆炸半径(同时受影响的节点数必须小于多数派安全线——对 5 投票节点,一次最多动 1~2 台)、中止条件(稳态指标越线即自动停止注入并回滚,而不是靠人盯屏幕)、恢复验证(注入撤销后系统回到稳态的实测时长)。机房级演练是这套框架的"大 boss 关”:注入对象从进程/网络升级为整个数据中心,中止条件必须升级为一组硬阈值(写错误率、订单创建失败数),恢复验证则要包含"回切"——因为真故障之后你迟早要把主写切回原机房,回切本身就是一次计划内切换,得建反向复制、选低峰窗口、冻结写入做对账。
演练节奏上,业界常见的是"季度机房级 + 月度实例级 + 每周混沌自动化"。每一次都必须产出数字:MTTD(多久发现)、切换完成时刻、RTO 收敛曲线、RPO 实测值——这些数字是下一篇架构总装的需求输入。
实验一:没有安全闸的注入是二次事故
10 台节点、每 20 秒依次注入 150 秒网络隔离;集群写可用需要至少 5 个投票节点在线;观察"有熔断线自动刹车"与"没有"的结局差别。
N=10# 参与注入的节点数GAP=20# 每 20s 对下一台注入网络隔离DUR=150# 每次隔离持续 150sMON=10# 熔断确认窗: 异常读数需持续 10s 才落闸ABORT_AT=0.01# 错误率熔断线deferror_rate(degraded):# 集群写可用需要 >=5 个投票节点在线: 降级数逼近/击穿多数派时错误率阶跃table={0:0.001,1:0.003,2:0.008,3:0.15,4:0.3,5:0.9}returntable[min(degraded,5)]defrun(guard):t=0injected=0peak=0.0aborted_at=Nonewhilet<=400andinjected<N:ift>=injected*GAP:injected+=1continued=sum(1forjinrange(injected)ift-j*GAP<DUR)er=error_rate(d)peak=max(peak,er)ifguardander>ABORT_AT:aborted_at=t+MON# 读数越线后还要走完确认窗breakt+=5returninjected,peak,aborted_at n1,p1,_=run(guard=False)n2,p2,at=run(guard=True)print("无熔断: 全部 %d 台被注入, 峰值错误率 %.0f%% —— 第 3 台起多数派已失守却无人踩闸"%(n1,p1*100))print("有熔断: t=%ds 读数越线, 确认窗 %ds 后落闸, 实际影响 %d 台, 峰值错误率 %.0f%%"%(at-MON,MON,n2,p2*100))print("落闸时点 t=%ds; 爆炸半径=3 台 < 多数派安全线(还能挂 %d 台) —— 这就是'实验设计'四个字的含量"%(at,n2,5-n2))print("安全设计: GAP(20s) < 单节点恢复时间 << DUR(150s) 保证读数可归因; 熔断线取业务错误率的量级下限, 而非测得出异常的安慰值")运行输出:
无熔断: 全部 10 台被注入, 峰值错误率 90% —— 第 3 台起多数派已失守却无人踩闸 有熔断: t=40s 读数越线, 确认窗 10s 后落闸, 实际影响 3 台, 峰值错误率 15% 落闸时点 t=50s; 爆炸半径=3 台 < 多数派安全线(还能挂 2 台) —— 这就是'实验设计'四个字的含量 安全设计: GAP(20s) < 单节点恢复时间 << DUR(150s) 保证读数可归因; 熔断线取业务错误率的量级下限, 而非测得出异常的安慰值这组数字里最该抄进预案的是两条线的位置关系:熔断线(错误率 1%)必须设在"多数派失守"之前,确认窗(10 秒)远短于故障传播时间;而自动刹车能把峰值从 90% 截到 15%,靠的不是反应快,是阈值设计得早。真实演练还要防"熔断后无人收尸"——落闸必须连带触发注入撤销与告警拉群,演练报告里"中止路径用时"是和"恢复用时"平级的指标。
实验二:机房切换的 RTO,大头在客户端嘴上
假设机房级断电,数据库侧按第 4 篇的自动化走完:判定 45 秒 + 补账 40 秒 + 提升与代理改口 8 秒 = 93 秒。但业务真正恢复要等所有客户端改口。DNS 缓存按 TTL 收敛、连接池里的旧长连接按 maxLifetime 或报错重连收敛——两路合流成一条指数曲线。
importmath T_DETECT=45# 故障判定(多探测点仲裁, 第 4 篇)T_RECOVER=40# 选主 + 补账(回放 relay + binlog server 增量)T_PROMOTE=8# 提升新主 + 代理写组改口SWITCH_AT=T_DETECT+T_RECOVER+T_PROMOTE RTO_TARGET=300.0# 业务承诺: 5 分钟内 95% 写恢复defrto(tau_dns,tau_pool,level=0.95):# 切换后错指旧机房的流量按两路收敛合成: 1/tau = 1/tau_dns + 1/tau_pooltau=1.0/(1.0/tau_dns+1.0/tau_pool)need=-math.log(1-level)*taureturnSWITCH_AT+need before=rto(120,300)# DNS TTL=300s, 连接池 maxLifetime=1800safter=rto(30,60)# 专用短 TTL 域名 + maxLifetime 缩短 + 报错立即重连print("切换完成时刻(数据库+代理): t=%ds"%SWITCH_AT)print("现状参数 95%% 写恢复 RTO = %.0fs -> %s"%(before,"超出 300s 承诺"ifbefore>RTO_TARGETelse"达标"))print("优化参数 95%% 写恢复 RTO = %.0fs -> %s"%(after,"达标"ifafter<=RTO_TARGETelse"仍超时"))print("差额 %.0fs 全部出在'客户端改口慢': 数据库侧 93s 已是硬时间, 收敛尾巴由 DNS TTL 与连接池寿命决定"%(before-SWITCH_AT))forpctin(0.99,0.999):t99=rto(120,300,pct)t99b=rto(30,60,pct)label="99%"ifpct==0.99else"99.9%"print(" %s 收敛: 现状 %.0fs / 优化后 %.0fs"%(label,t99,t99b))print("\n回滚预案(演练必须真演练这三步):")print(" 1) 实验级: 中止注入指令 -> 网络恢复, 系统自愈时间 = 会话超时 + 复制重连, 实测记入报告")print(" 2) 系统级: 新机房已是唯一事实源, '回切'不是撤销而是二次计划切换(反向复制建立 -> 低峰冻结写 -> 回切)")print(" 3) 数据级: 切换窗口内经业务网关兜底的降级写(排队补偿单)必须对账清零, 否则 RPO 尾巴挂在演练报告上")运行输出:
切换完成时刻(数据库+代理): t=93s 现状参数 95% 写恢复 RTO = 350s -> 超出 300s 承诺 优化参数 95% 写恢复 RTO = 153s -> 达标 差额 257s 全部出在'客户端改口慢': 数据库侧 93s 已是硬时间, 收敛尾巴由 DNS TTL 与连接池寿命决定 99% 收敛: 现状 488s / 优化后 185s 99.9% 收敛: 现状 685s / 优化后 231s 回滚预案(演练必须真演练这三步): 1) 实验级: 中止注入指令 -> 网络恢复, 系统自愈时间 = 会话超时 + 复制重连, 实测记入报告 2) 系统级: 新机房已是唯一事实源, '回切'不是撤销而是二次计划切换(反向复制建立 -> 低峰冻结写 -> 回切) 3) 数据级: 切换窗口内经业务网关兜底的降级写(排队补偿单)必须对账清零, 否则 RPO 尾巴挂在演练报告上350 秒对 300 秒的承诺,输在哪一目了然:数据库侧的 93 秒是硬成本,尾巴的 257 秒全是客户端惯性。三个优化方向对应三行配置:数据库专用域名把 TTL 压到 30~60 秒并保证解析器端尊重(长 TTL 缓存是 DNS 容灾的经典漏洞,RFC 1035 的缓存语义决定了"改记录"天然滞后);连接池 maxLifetime 从 30 分钟降到 5 分钟,让连接"自然老死"在可接受的改口窗口内;驱动层配置连接错误立即重解析(JDBC 的重连语义、代理层 keepalive 探测)。演练时这四个参数都要在报告里留名——RTO 不是一个组件的属性,是整条调用链的参数乘积。
常见陷阱与演练清单
- 演练只在"能看监控的时间段"做:真实故障不挑时间,至少一次演练安排在无人值守窗口(周末夜间 + 值班新人),验证告警链是否真的叫醒人。
- 注入手段不验证撤销通道:先确认"撤销指令 100% 能执行"再开始注入——撤销失败时你手里要有第二把闸(直接恢复网络/电源的人工 runbook)。
- 把"切换成功"当终点:没有回切演练的容灾是单向门;原机房修复后的回切窗口、反向复制的延迟与数据核对,都要进同一份演练方案。
- 稳态指标用 CPU/连接数:这些是"体检指标"不是"心跳指标",熔断线要挂在业务成功率上。
- 演练报告只写"通过":必须写数字——MTTD、切换用时、RTO 收敛曲线、RPO 实测、中止路径触发次数,否则下一次演练没有改进靶子。
演练把"纸面架构"锤成了"实测数字"。最后一篇回到工程原点:把这些数字、参数与组件收拢成一张可评审的架构答卷——下一篇,也是本篇终章《数据库高可用与容灾实战(10):架构实战:为一个 500GB 生产库设计高可用方案》。
参考来源
- Principles of Chaos:https://principlesofchaos.org/
- Wikipedia:Chaos engineering:https://en.wikipedia.org/wiki/Chaos_engineering
- GitHub:Netflix/chaosmonkey:https://github.com/Netflix/chaosmonkey
- GitHub:chaos-mesh/chaos-mesh:https://github.com/chaos-mesh/chaos-mesh
- RFC 1035:Domain names - implementation and specification:https://datatracker.ietf.org/doc/html/rfc1035
本系列已结集为免费专栏:数据库高可用与容灾实战:从主从复制到机房级演练;进阶推荐付费专栏:Python 自动化接单实战:从脚本到第一单(限时 ¥9.9,首篇免费试读)