自动化切换的三道关口上一篇把 RPO0 的条件拆明白了半同步只压缩丢数据的窗口窗口归零的那一刻恰恰依赖ACK 来源还活着。于是问题来到运维层——当主库真的没了谁来在几十秒内完成三件事判定它真死了、挑一个账面最全的接班者、把其余节点无损失配到新主。这三道关每一道都有成熟的坑位学名误判导致脑裂、选主选了个数据落后的、切换半路失败留下半套拓扑。这一篇以 MHA 与 Orchestrator 这两个 MySQL 世界最经典的自动化工具为骨架用两个确定性模拟把判定逻辑和补账逻辑跑通再给出参数化落地的清单。机制探测、确认、隔离、恢复、提升自动切换系统的主流程可以浓缩成五步MHAPerl 实现manager 节点 各库上的健康检查表与 OrchestratorGo 实现靠实例上复制元数据自动发现拓扑、内置 Raft 保证管理面自身的高可用在这五步上的差异是工程性的而非路线性的。第一步探测manager 周期执行SELECT 1或 SSH 层存活检查连续 K 次失败才进入故障假设。第二步确认从其他观测点不同网络出口、其他副本视角复核能连上旧主的任何一条路都算没死透——这一步专门克制假阳性。第三步隔离fencing确认死亡前先尽力让旧主永久写不了——SSH 上去 kill mysqld、置read_only、拔 VIP/断交换机口确认不了就放弃自动切换转人工这是防脑裂的底线。第四步账面恢复master_recovery从数据最领先的副本出发把它 relay log 里未回放的事务回放完再从其他副本或 binlog server收集它缺的增量 binlog过滤掉重复区间后应用——这是 MHA 最精髓的一步等于用全集群的副本凑出旧主最后的完整账本。第五步提升与重配新主super_read_onlyOFF其余副本CHANGE MASTER TO 新主 SOURCE_AUTO_POSITION1VIP/DNS/代理层跟着漂。实验一单点探测有多莽脑裂就有多贵模拟一个典型假阳性场景探测机与主库之间网络分区 8 秒20s~28s主库全程健康且以 120 笔/秒继续提交。对比三种判定策略的结果。PROBE_INTERVAL1.0# 探测周期(秒)K_SINGLE1# 朴素策略: 1 次失败即判死K_CONSEC3# 保守策略: 连续 K 次失败T_PART_START,T_PART_END20.0,28.0# 探测机与主库之间的网络分区窗口MASTER_WRITE_RATE120# 笔/秒defprobe_ok(t,observer):# observer 0 在分区窗口内失联; observer 1/2 走不同出口, 全程可达ifobserver0andT_PART_STARTtT_PART_END:returnFalsereturnTruedefdetect(k_consec,observers):fails[0]*observersfortinrange(0,40):foroinrange(observers):fails[o]0ifprobe_ok(float(t),o)elsefails[o]1votessum(1forfinfailsiffk_consec)ifvotes*2observers:# 仲裁: 多数探测点一致才判死returnt,votesreturnNone,0t1,_detect(K_SINGLE,1)print(单点探测 连续%d次规则: %.1fs 判死 - 发起切换, 但主库仍在服务(假阳性)%(K_SINGLE,float(t1)))lost_windowT_PART_END-t1print( 旧主在新主提升后又写了 %.0fs x %d 笔/秒 %d 笔 - 双主脑裂, 数据分叉%(lost_window,MASTER_WRITE_RATE,int(lost_window*MASTER_WRITE_RATE)))t2,_detect(K_CONSEC,1)print(单点探测 连续%d次规则: %.1fs 判死 - 依然是假阳性, 只是晚了几秒%(K_CONSEC,float(t2)))print( 脑裂写入规模: %d 笔%int((T_PART_END-t2)*MASTER_WRITE_RATE))t3,votesdetect(K_CONSEC,3)print(三点探测(不同网络出口) 仲裁: 40s 内无人达成多数 - %s%(未触发切换(正确)ift3isNoneelse误判))print(\n加入 master 自检(能否读自身共享存储/能否触达任一从库):)print( 分区期间主库自检通过 - 明确诊断为探测网络故障而非数据库故障)print( 正确处置: 抑制切换 告警运维检查探测链路, 而不是动数据库)print(常见误判源: 探测SQL超时设置过短、SSH跳板抖动、主库IO打满导致 SELECT 1 慢于探测超时)print( ——这些都会让真故障判定建立在假信号上, 仲裁与自检是唯一廉价保险)运行输出单点探测 连续1次规则: 20.0s 判死 - 发起切换, 但主库仍在服务(假阳性) 旧主在新主提升后又写了 8s x 120 笔/秒 960 笔 - 双主脑裂, 数据分叉 单点探测 连续3次规则: 22.0s 判死 - 依然是假阳性, 只是晚了几秒 脑裂写入规模: 720 笔 三点探测(不同网络出口) 仲裁: 40s 内无人达成多数 - 未触发切换(正确) 加入 master 自检(能否读自身共享存储/能否触达任一从库): 分区期间主库自检通过 - 明确诊断为探测网络故障而非数据库故障 正确处置: 抑制切换 告警运维检查探测链路, 而不是动数据库 常见误判源: 探测SQL超时设置过短、SSH跳板抖动、主库IO打满导致 SELECT 1 慢于探测超时 ——这些都会让真故障判定建立在假信号上, 仲裁与自检是唯一廉价保险这组数字把探测是切换系统最大风险源说透了把探测失败次数从 1 提高到 3脑裂写入从 960 笔降到 720 笔——加大 K 只能推迟误判不能避免误判因为它改变的是等待时长而不是信号来源。唯一能破局的是信号多样性多观测点仲裁把假阳性直接拦死代价是真故障时切换慢一个探测周期。所以生产配置通常是小 K 多观测点 主库自检 强 fencing而不是某个玄学的大 K 数字。这也解释了为什么 Orchestrator 强调管理面自身要跑 Raft探测者的共识如果只是一个单点 manager 进程它本身就是最大的假阳性来源。实验二一次教科书式的选主与增量补洞旧主提交到 tx12 后宕机。三个副本的账面各不相同还有一台 binlog server 完整收到了 12 笔。模拟 MHA 的决策过程打分选主、回放 relay、跨节点收集增量、无损失配全员并给出去掉 binlog server 后的退化路径。old_masterset(range(1,13))# 旧主提交到 tx12 后宕机replicas{# 各副本已回放到 InnoDB的事务集合R1:set(range(1,10)),R2:set(range(1,11)),R3:set(range(1,9)),}relay_only{R1:{10},R2:{11},R3:set()}# 只收到 relay log 未回放binlog_serverset(range(1,13))# 第三接收方: 完整保住了旧主 binlogprint(旧主账面: tx1-%d 已提交; 副本进度: %s%(max(old_master),{k:回放%d笔/relay另%d笔%(len(v),len(relay_only[k]))fork,vinreplicas.items()}))scoredsorted(replicas,keylambdar:-(len(replicas[r]|relay_only[r])))candscored[0]totalreplicas[cand]|relay_only[cand]print(\n候选得分序: %s - 选 %s%(scored,cand))gapold_master-totalprint(新主 %s 先回放自身 relay(补 tx%s), 账面到 tx1-%d, 距旧主还差 %s%(cand,sorted(relay_only[cand]-replicas[cand])or无,max(total),sorted(gap)))ifgap:print( MHA 恢复流程: scp binlog_server:mysql-bin.000003 - 提取 tx%s - 应用到新主%sorted(gap))total|gapprint( 恢复后新主账面: tx1-%d, 与旧主完全一致: %s%(max(total),totalold_master))print(\n提升 %s: SET GLOBAL super_read_onlyOFF; 其余副本 CHANGE MASTER TO %s SOURCE_AUTO_POSITION1%(cand,cand))forrinsorted(replicas):ifrcand:continuepending_relaysorted(relay_only[r]-replicas[r])needsorted(total-(replicas[r]|relay_only[r]))print( %s: 本地账面 %d 笔, relay 待回放 %s, 还需向新主索要 %s%(r,len(replicas[r]),pending_relayor无,needor无))no_bs_totalmax((replicas[r]|relay_only[r]forrinreplicas),keylen)lostold_master-no_bs_totalprint(\n若无第三接收方: 全员最大账面 tx1-%d, 旧主已提交的 tx%s 只能去旧主磁盘上捞%(max(no_bs_total),sorted(lost)))print( 捞回 - 补放后无损; 整机报废 - 有损切换 %d 笔, 必须留书面记录并事后对账, 禁止静默提升%len(lost))运行输出旧主账面: tx1-12 已提交; 副本进度: {R1: 回放9笔/relay另1笔, R2: 回放10笔/relay另1笔, R3: 回放8笔/relay另0笔} 候选得分序: [R2, R1, R3] - 选 R2 新主 R2 先回放自身 relay(补 tx[11]), 账面到 tx1-11, 距旧主还差 [12] MHA 恢复流程: scp binlog_server:mysql-bin.000003 - 提取 tx[12] - 应用到新主 恢复后新主账面: tx1-12, 与旧主完全一致: True 提升 R2: SET GLOBAL super_read_onlyOFF; 其余副本 CHANGE MASTER TO R2 SOURCE_AUTO_POSITION1 R1: 本地账面 9 笔, relay 待回放 [10], 还需向新主索要 [11, 12] R3: 本地账面 8 笔, relay 待回放 无, 还需向新主索要 [9, 10, 11, 12] 若无第三接收方: 全员最大账面 tx1-11, 旧主已提交的 tx[12] 只能去旧主磁盘上捞 捞回 - 补放后无损; 整机报废 - 有损切换 1 笔, 必须留书面记录并事后对账, 禁止静默提升注意选主打分用的是可恢复总账面而不是回放进度R2 已经回放到 10、relay 里躺着 11它就是最优解多花 1 秒回放比选一个账面 9 的节点省 3 秒补洞。而若无第三接收方的退化分支才是大多数团队的真实处境——第 3 篇实验里那 3 秒的半同步降级窗口在这里变成必须直面的一次有损切换。MHA 要求配save_binary_logs的 binlog server、Orchestrator 的钩子体系本质都是把账面收集能力做成基础设施。常见陷阱与落地清单没做 fencing 就提升旧主只是探测失联、实际在写新旧两头各写各的事后合并数据的价格你无法承受。铁律确认旧主不可写或被强制只读之前切换流程不得越过第四步。副本全部配成不保留 relay/binlogrelay_log_purgeON且没开log_slave_updates账面收集无米下锅选主只能看谁回放得多等于放弃增量恢复。切换后不回收旧主身份旧主修好回来以原拓扑接入GTID 集合却领先新主一截——必须先重建再挂载Orchestrator 里叫register failed master in recover-mode。应用层没有重试语义切换成功的 15 秒里连接全是报错业务把报错写成了丢单代理层与客户端超时策略第 5 篇要提前对齐。把自动切换当演练对手MHA/Orchestrator 每年真实故障外触发 0 次的系统最容易在真正那天掉链子季度级拔电源演练是它唯一的单元测试。工具选型上忽视社区活性MHA 官方仓库更新缓慢、Orchestrator 开源版停滞Percona 维护分叉以及 MySQL 原生 Group Replication mysql-shell管理面的此消彼长选型时把三年后还有人修 bug当成硬指标。到这里数据层谁来当主解决了但应用侧还有一问没答切换完成后那些长连接池和读写分离规则怎么在半分钟内集体改口下一篇《数据库高可用与容灾实战5代理层路由选型ProxySQL、中间件与直连的取舍》。参考来源GitHubmysql/mysql-mhaMaster High Availabilityhttps://github.com/mysql/mysql-mhaGitHubopenark/orchestratorhttps://github.com/openark/orchestratorMySQL 8.0 Reference ManualReplication and Uptime (CHANGE REPLICATION SOURCE TO)https://dev.mysql.com/doc/refman/8.0/en/change-replication-source-to.htmlWikipediaSplit-brainhttps://en.wikipedia.org/wiki/Split-brain_(computing)The Raft Consensus Algorithmhttps://raft.github.io/