
一提到 Orchestrator很多人脑子里第一反应是自动故障转移主库探测失败它选出一个从库提升应用流量切过去完事。但我做了几年 MySQL 高可用之后越来越觉得真正考验一个编排系统的不是切换那一刻有多快而是故障老主库恢复之后怎么办。老主库一醒过来你是放手让它自动归队还是先拦住它补数据这个过程 Orchestrator 里有一套专门的自动归队能力也就是标题里说的“故障老主库恢复后的自动归队过程”。我自己第一次完整观察这个过程是在一次演练里把老主库直接 KILL 掉再恢复当时我以为切完之后还得手动CHANGE MASTER TO结果发现 Orchestrator 早在日志里完成了归队动作。那次之后我才专门去把相关配置、判断逻辑和失败场景整个翻了一遍。1. 一次故障演练让我重新理解“自动归队”1.1 故障转移从来不是单点动作现实中的主库故障切换更像一次接力棒交接老主库 m1 不可达后Orchestrator 的心跳探测线程连续失败在RecoverMasterClusterFilters匹配的集群里进入 recovery接着从从库列表里排除掉延迟过大、不满足候选条件的节点挑一个被选中的从库提升成新主其余从库先被摘掉再统一重新指向新主最后才轮到我们关心的老主库。前面几步如果不做拓扑就会变成一个“多主”的网状结构。而老主库如果恢复后没人管它依然会按自己旧的复制位置继续接受写入这就直接变成脑裂。所以 Orchestrator 把老主库恢复之后的处理设计成一条完整的流水线发现、评估、归队、验证。它不是在“看到实例活着”的瞬间就盲目挂上去而是按照恢复流程的上下文一步步来。这个“上下文”包括这次恢复产生的 recovery id、被提升的新主是谁、原来集群里的从库列表是什么。没有这个上下文Orchestrator 是不会贸然对老主库做复制变更的因为它根本不知道应该把它挂到谁下面。1.2 归队不是“重新挂一下复制”手动做复制维护时一句CHANGE MASTER TO就能把实例塞回拓扑。但自动归队完全不同。Orchestrator 在真正执行归队之前会确认三件事这个实例当前是否处于维护窗口内、是否被禁止操作新主的 GTID 集合能不能覆盖老主库缺失的事务老主库有没有新主不认识的多余事务归队动作会不会触发跨机房策略、拒绝名单等安全限制。三个条件全部通过它才会执行复制相关指令。我见过不少运维同学用“搬个小板凳等它自己挂回去”的心态看待自动归队结果老主库恢复后一两个小时都没有动静最后发现是之前做变更时顺手给老主库开了维护模式忘了关。自动归队省掉的是重复劳动不是安全判断。它更像“手术后病人出院要复查、评估、限制性复工”而不是“签个字直接回到一线岗位”。2. 老主库恢复后Orchestrator 如何决定要不要收留它2.1 先从发现和注册说起Orchestrator 是一个无 agent 依赖的拓扑发现模型每隔几秒扫描一次所有实例。这个周期由InstancePollSeconds控制线上我通常会设成 3 到 5 秒。老主库在故障期间处于 unreachable 状态当进程恢复、网络恢复之后下一次探测周期里它重新被探活状态从 dead 变成 alive。这时 Orchestrator 的元数据里还留着该实例的source_host信息正常情况下它要么指向自己要么是空。如果source_host还是老主库自己那说明在 Orchestrator 的视角里它仍是“主库”而新一轮 recovery 的上下文已经被提升出来的新主绑定。接下来 Orchestrator 会结合恢复记录里的集群名、新主地址判断这个旧主是否应该被重新纳入拓扑。如果老主库是在我们手动维护之后重新注册回来的recovery 上下文缺失Orchestrator 会把它当作一个游离实例不会自动分配主库。很多“为什么没自动归队”的问题其实是上下文已经丢了。2.2 GTID 集合对比自动归队的第一道安全闸门在高可用环境里GTID 是自动归队能够安全执行的最大前提。GTID 模式下每个事务都有全局唯一编号事务来源一目了然。老主库恢复后Orchestrator 会在两台实例上分别拿到 GTID 执行集合然后做对比。我们可以用最原始的 SQL 自己模拟这个过程-- 新主上执行 SELECT global.gtid_executed; -- 老主库上执行 SELECT global.gtid_executed;假设新主的集合是2c1e0d26-xxxx:1-300老主库是2c1e0d26-xxxx:1-200那说明老主库只是缺了 100 个事务而且这些事务在新主的 binlog 里仍然存在自动归队就可以继续。Orchestrator 会通过MASTER_AUTO_POSITION1让 MySQL 自己找位置把老主库追平。反过来说如果老主库的集合是2c1e0d26-xxxx:1-300, aaaa-xxxx:1-10而新主完全没有aaaa-xxxx开头的 GTID说明老主库在故障期间接受了额外的写入这些事务从未进入新主。这种“分叉”状态下Orchestrator 会直接拒绝归队。即便强制挂载MySQL 也会在复制握手时报错绝不会产生一个“假装一致”的从库。这里有一个容易被忽略的运维细节新主 binlog 的保留时间一定要足够长。默认的binlog_expire_logs_seconds在不少默认配置里只有 3600 秒甚至更短。老主库可能因为硬件故障、网络隔离、人工处理拖了几个小时才恢复。到时候新主 binlog 已经清理掉老主库缺失的事务即便 GTID 集合“逻辑上可达”复制也拉不到数据了。2.3 数据分叉时为什么 Orchestrator 宁可拒绝也不强行归队强行归队最典型的后果是老主库里那些多出来的事务会在后续复制里和新主的数据产生主键冲突、唯一键冲突SQL 线程直接停在Last_SQL_Errno1062或者1032。更怕的是那些没有唯一约束的表两边各自写入不同数据复制不会报错但最终结果就是两套数据悄悄合并成不可信的脏数据。所以遇到分叉正确做法是先把差异评估清楚确认老主库多出来的事务对应哪些业务写入能否安全补偿或回放如果需要从新主同步覆盖再用pt-table-checksum和pt-table-sync这类工具做数据修复修复通过后再让 Orchestrator 重新归队。这个过程不应该自动化原因很简单自动脚本无法判断一个多出来的事务到底是该保留还是该丢弃判断它需要业务上下文。这里还要多说一句如果集群没有开启 GTIDOrchestrator 虽然也能通过 binlog file/position 和 Pseudo-GTID 机制做一定程度的自动匹配但安全性依赖额外配置不稳定。我个人的建议非常明确用 Orchestrator 做高可用集群一律开启 GTID。没有 GTID 的自动归队最多只能算“尽力而为”出问题的时间点通常都在你半夜最不想处理的时候。3. 触发自动归队的关键配置与前置条件3.1 总开关RecoverToPromotedMaster自动归队这个能力对应 Orchestrator 配置里一个很直白的开关RecoverToPromotedMaster。它默认是 true含义是“恢复完成后把故障老主库恢复到被提升出来的新主下面让它变成这个新主的从库”。如果把它改成 false老主库恢复后只会被标记成一个游离实例不会自动挂回新主必须人工介入。核心集群我一般会这样配置{ RecoverMasterClusterFilters: [db.*], RecoverToPromotedMaster: true, MasterFailoverDetachSlaves: true, RecoveryPeriodBlockSeconds: 3600, InstancePollSeconds: 5 }这几个字段的作用分别是RecoverMasterClusterFilters用正则匹配哪些集群允许执行主库恢复避免误伤不重要的测试集群。RecoverToPromotedMaster自动归队的总开关true 代表故障转移完成后尝试将旧主恢复到新主下面。MasterFailoverDetachSlaves切换过程中先把从库从旧主上摘除统一重挂减少脏状态残留。RecoveryPeriodBlockSeconds一次恢复完成后的冷却时间。比如设置 3600 秒就表示同一集群在一小时内不允许再触发下一次恢复防止主库抖动导致连环切换。千万不要小看最后的冷却时间。没有冷却时间一个故障恢复后主库状态还没稳定另一台机器又出问题Orchestrator 可能连续做两三次切换整个集群被切得七零八落。这个参数是我在生产环境里吃过亏之后才真正重视起来的。3.2 维护窗口和拒绝名单防止误归队的护栏自动归队不是什么时候都该自动。如果 DBA 正在老主库上做数据检查Orchestrator 却一封CHANGE MASTER TO把它挂上去检查就白做了。这时要给它开维护窗口让 Orchestrator 暂停对该实例的所有自动化操作orchestrator-client -c begin-maintenance -i 10.0.0.10:3306 -r checking data conflict -t 30m-r是维护原因-t是维护时长。维护期间该实例不会被 Orchestrator 执行恢复、归队或重挂动作检查完之后再手动结束orchestrator-client -c end-maintenance -i 10.0.0.10:3306另外一类护栏是RecoveryIgnoreHostnameFilters通过主机名过滤让特定实例不参与恢复。这个配置适合用于机器即将下线、或者硬件已经告警的场景。使用时要小心它不只是阻止老主库归队也会影响该实例被选为新主的可能性。3.3 复制账号、只读状态和网络恢复等前置条件配置开关只是告诉 Orchestrator “可以做”真正能不能做还要看底层 MySQL 的状态。第一Orchestrator 用来连接实例的账号必须拥有执行复制变更的权限。线上我通常给一个专用账号权限至少包含SUPER、REPLICATION_SLAVE、REPLICATION_APPLIER。如果是 GTID 模式还需要能读取mysql.gtid_executed表。权限不够归队动作会在日志里留下一个 SQL error但表面看起来实例又没有变化排查起来非常容易走弯路。第二老主库恢复后必须先保证它处于只读状态。Orchestrator 在故障恢复时会尽力把老主库设置成read_only1但如果当时机器彻底失联这个动作可能没执行成功。老主库一旦恢复后还继续接受写入GTID 集合必然分叉自动归队永远不可能成功。所以每次故障恢复后我做的第一件事就是在老主库上确认SELECT read_only;返回 1如果没有先手动设成只读再说。第三网络和名字解析要正常。Orchestrator 是通过 hostname:port 来识别实例的如果老主库恢复后换了主机名或者 DNS 解析顺序变了它可能会把同一个实例识别成两台机器自动归队同样会失败。这类问题在云环境里特别常见因为主机重启后内网 IP 可能变化。最好把 Orchestrator 的发现配置做成 IP 或一个长期稳定的内部域名而不是随手写一个短主机名。4. 自动归队失败从现象到根因的排查链路4.1 现象一旧主已经能看到却不执行归队最常遇到的是这个Orchestrator 页面里老主库已经变绿了但拓扑里它还是孤零零一个节点既没有归属也没有执行任何复制动作。这时候我先查它是不是被维护窗口锁住了orchestrator-client -c maintenance -i 10.0.0.10:3306如果输出里有维护记录先判断维护是否该结束结束之后再让它自然收敛。如果维护记录为空就去查 Orchestrator 日志看恢复完成后有没有报“skipping rejoin”之类的信息。常见原因是什么RecoverToPromotedMaster被改成了 false或者集群名没有匹配上RecoverMasterClusterFilters。这两种情况都能导致旧主“明明活着就是不归队”。另外我还会执行orchestrator-client -c which-master -i 10.0.0.10:3306如果返回空说明 Orcherstrator 根本没认定这台实例的主库。那就要继续往集群拓扑方向查是不是恢复上下文已经丢失只能人工引导了。4.2 现象二归队执行了但 Seconds_Behind_Master 持续增长如果自动归队已经执行SHOW SLAVE STATUS里Slave_IO_Running和Slave_SQL_Running都是 Yes但延迟一直拉大这通常是新主 binlog 量太大或者老主库硬件性能不足。可以先看几个关键值SHOW SLAVE STATUS\G重点看三列Retrieved_Gtid_Set、Executed_Gtid_Set、Seconds_Behind_Master。如果Retrieved_Gtid_Set已经和新主一致说明 IO 线程已经把 binlog 拉到本地瓶颈在 SQL 线程如果Retrieved_Gtid_Set还在增长而延迟没有停止那大概率是批量大事务在单线程回放。这种情况下我不会急着调大并行复制参数而是先看是不是老主库上还挂着其他历史从库导致它一边当从库一边又当主库。故障恢复后的拓扑里经常出现“旧主下面还挂着一堆旧从库”的局面这会放大复制压力。正确做法是先把旧从库摘干净让老主库只做新主的一个纯从库等它追平后再考虑是否恢复原来的层级。4.3 现象三报错提示在主库上找不到旧主包含的 GTID这个报错是最“硬”的分叉信号经常表现为Last_IO_Errno1236Got fatal error 1236 from master when reading data from binary log: The slaves GTID set includes transactions that are not in the masters GTID set这行报错的意思非常明确老主库有新主不认识的事务。看到它之后我第一反应不是去改复制配置而是先把两台实例的 GTID 集合导出来做对比SELECT global.gtid_executed;两边集合放在一起看通常能直接看出多出来的 GTID 属于哪个 uuid。老主库有新 uuid 或新主没有的 uuid说明它在故障窗口里独立产生过事务。这类问题没有银弹只能回到文章第 2.3 节说的流程先评估差异再修复数据修复完再做归队。千万别想着RESET MASTER来“清掉分叉”这会直接把老主库的数据一致性交给运气。4.4 我的兜底手法手动归队的标准动作自动归队反复失败时我会退回手动流程。手动不一定代表用 Orchestrator 的修改接口而是先在 MySQL 层把复制关系理顺再让 Orchestrator 重新发现。前提是在新主和旧主之间已经确认没有不可覆盖的分叉数据。然后执行下面的 SQLSET GLOBAL read_only ON; STOP SLAVE; RESET SLAVE ALL; CHANGE MASTER TO MASTER_HOST10.0.0.11, MASTER_PORT3306, MASTER_USERrepl_user, MASTER_PASSWORDyourpass, MASTER_AUTO_POSITION1; START SLAVE; SHOW SLAVE STATUS\GRESET SLAVE ALL会清掉旧的复制信息这是必须的因为老主库的复制关系还指向自己不清干净会有残留。执行完这组命令后我一般会先在 MySQL 侧确认Slave_IO_Running、Slave_SQL_Running都正常再打开 Orchestrator 页面等下一个探测周期。如果 Orcherstrator 发现旧主已经是一个正常从库它会自动把元数据里的状态修正不需要额外人工“注册”。这里有个警告上面这套命令只在 GTID 模式下安全。如果集群没有 GTIDMASTER_AUTO_POSITION用不了你只能手动指定MASTER_LOG_FILE和MASTER_LOG_POS位置找错就是数据事故。这也是我一直坚持生产环境开 GTID 的原因。5. 自动归队之后验证、收尾和后续风险控制5.1 归队成功的状态长什么样自动归队完成后问题并没有结束至少要确认三处状态。首先看 Orchestrator 页面或命令行输出老主库应该显示为 slaveSource 指向新主不再是一个游离节点。可以用orchestrator-client -c which-slave -i 10.0.0.11:3306其次回到 MySQL 里看复制线程状态。理想的输出是SHOW SLAVE STATUS\GSlave_IO_Running: YesSlave_SQL_Running: YesSeconds_Behind_Master: 0Executed_Gtid_Set已经和新主一致。一次成功的自动归队在 Orchestrator 的审计日志里会留下明确的恢复记录包括 recovery id、实例地址、时间戳。这个记录最好保存好后续复盘要用。5.2 后续几天我会盯什么指标不要看它归队成功就完全放手。老主库在故障期间的数据往往不是完整连续的归队后会有一个“追历史”的过程延迟曲线、从库负载都是重点观察对象。我会接下来三天每天看一遍复制延迟和SHOW SLAVE STATUS的报错字段。还会跑一遍数据一致性校验比如pt-table-checksum --userchecksum --passwordxxx --host10.0.0.11 --databasesmyapp如果 checksum 发现差异再配合pt-table-sync定向修复。这一步不是每次都必须但只要发生过故障切换我都会跑一次因为它能发现那些“复制正常但数据已经不一致”的隐蔽问题。另一个容易被忽视的点是应用流量的写入入口。如果老主库还挂在负载均衡或 VIP 后面即使它已经变成从库业务连接仍然可能把写流量打到它上面。这也是我要专门检查的老主库必须从所有写流量入口里摘掉只保留复制链路。否则归队成功只是表象下一分钟就可能因为新的写入制造新的分叉。5.3 自动归队与自动回切是两件不同的事最后必须把概念说清楚。很多人认为自动归队就等于自动回切以为老主库回来以后Orchestrator 会在某个时间点再把主角色还给它。实际不是这样。归队只是让老主库以从库身份回到拓扑继续在新主下面复制数据回切则是把主从角色再换回来让老主库重新成为主库这个操作风险和故障转移一样高甚至更高。Orchestrator 默认不会自动回切这是它做过的最正确的安全选择之一。生产环境里回切应该由 DBA 在业务低峰期经过评估后手动触发而不是让一个编排系统自动决定。我个人的原则是自动归队可以放心开但前面要有三个前提——GTID 已开启、只读状态有校验、新主 binlog 保留时间足够长。归队后也别急着做任何优化先看两天数据一致性再决定要不要把老主库重新纳入候选池。自动归队省掉的只是重复劳动该做的检查一项都不能少。