
来了先说结论主库挂掉这件事我从没见过哪家公司的MySQL环境敢说“永远不会发生”。硬盘坏道、机房断电、内核panic、误操作DROP TABLE甚至一次不恰当的ALTER TABLE把库锁死每一种都可能让主库瞬间失去响应。这个场景下从库不是摆设而是你唯一能用来续命的东西。但“怎么办”这三个字里面藏着大量需要前置思考的细节从库有没有把日志追平复制线程是否还在正常跑调整配置后业务侧怎么切旧主库回归时怎么避免双写冲突把这些问题都理清楚你才有底气说一句“我能处理”。这篇文章不写高深原理只讲一个运维老鸟在主库出问题时真正会做的动作、会敲的命令、会回避的坑。我会按实际运维的时间线展开先判断故障等级再确认从库状态然后执行切换最后处理旧主库回归和日常预防措施。文中的命令都在MySQL 5.7/8.0上验证过GTID和传统位点复制两种模式我都会兼顾。1. 主从架构到底在防什么先搞清楚故障类型再动手很多人对主从复制的理解就是“主库写从库读”但真到主库出问题时你会发现这个理解远远不够用。主从复制本质上是“基于binlog的日志回放”主库的每次写操作都会记录到二进制日志binlog中从库通过IO线程把binlog拉到本地变成中继日志relay log再由SQL线程读取中继日志并应用到自己的数据文件上。这个过程没有实时性保障也不存在强一致承诺所以“主库挂了从库直接顶上”这句话得加很多前提条件才能成立。1.1 主库故障的常见类型与影响范围我是把主库故障分成三类来判断的因为不同类型对应的处理策略完全不同。第一类叫“彻底不可用”比如硬件损坏、操作系统崩溃、数据库进程被kill -9之后无法启动、数据文件损坏导致InnoDB恢复失败。这种故障下主库在短时间内很难救活从库就是你唯一的依赖必须考虑切换。第二类叫“部分不可用”比如磁盘满了导致只读操作正常但写操作卡住或者某条大事务把innodb_buffer_pool耗尽、锁等待堆积导致连接超时。这类故障往往不需要切换把主库的问题解决后从库还能继续追日志切换反而会造成不必要的中断。第三类最阴险叫“逻辑损坏”典型代表是误执行了DELETE FROM全表、DROP TABLE或者一次错误的UPDATE把整列值改错。这类故障下从库会忠实地把错误操作也同步过来你看到的从库数据一样是坏的。此时直接切换不仅救不了业务还等于把故障从“一个”变成“两个”。正确的处理是立即停掉从库复制用binlog或备份做时间点恢复。所以在动手之前第一件事不是看从库怎么办而是回答一个“主库到底是哪种坏了”。你可以在心里或者纸上快速过一遍有没有办法重启能不能连上错误日志里有没有明显异常如果连诊断的时间都没有那就跳到下一节先用最保守的方式保护从库。1.2 从库的三种状态追平、延迟、损坏接到主库出问题的消息后你要马上弄清楚从库处于哪种状态。这里有个很常见的误解很多人认为从库和主库“数据应该差不多”或者“稍微差几秒没问题”但实际上一旦主库写入压力大或者从库机器配置差延迟可以高达几十分钟甚至几个小时。判断从库状态最简单的方式是登录从库执行SHOW SLAVE STATUS\G重点关注两个字段Seconds_Behind_Master从库落后主库的秒数和Master_Log_FileIO线程读取到的master最新binlog文件名。如果Seconds_Behind_Master为0说明正好追平如果值很大说明从库还在赶工如果显示NULL多半是SQL线程没在运行。但必须提醒你Seconds_Behind_Master是一个“估算”值它依赖从库自身执行时钟与binlog中记录的原始主库写入时间的差值。一旦SQL线程卡住这个值可能一直是0但实际上数据早就不一致了。所以更可靠的判断方式是看Exec_Master_Log_Pos和Relay_Master_Log_File它们标记了从库实际执行到主库binlog的哪个位置。额外说一个技巧用SHOW MASTER STATUS在旧主库上拿到当前binlog位点如果从库的Exec_Master_Log_Pos已经追平或非常接近说明数据是完整的。如果你用的是MySQL 5.7以上的并行复制补充一个查看并行worker的命令SELECT * FROM performance_schema.replication_applier_status_by_worker\G可以看到每个应用线程各自执行到哪个位点、是否报错。在切换前的评估中我通常会把这一列数据作为“数据完整性”的证据之一。2. 故障发生后前5分钟先看状态别急着切换某个周一凌晨我被电话叫醒的体验是这篇文章的灵感来源。当时值班同事说“主库连接报错应用已经全挂了要不要把从库切上去”我第一反应是先别动给我查三样东西。这三样东西是主库到底死没死、从库复制线程状态、从库的中继日志是否还有未应用的堆积。在没有确认这三件事之前贸然切换轻则丢数据重则把本来正常的从库也搞出问题。2.1 查看主库存活状态与错误日志登录主库服务器先看进程ps -ef | grep mysqld如果进程还在尝试用mysqladmin pingmysqladmin -uroot -p ping如果进程已经没了别马上启动先打开错误日志看最后几十行tail -n 100 /var/log/mysql/error.log这时往往会看到两种典型情况一种是正常的shutdown日志说明进程是被系统或人为kill掉数据文件大概率是完整的另一种是InnoDB恢复报错比如“corrupted page”或者“redo log crashes”这时候就算强行启动也可能在启动过程中又挂掉。正确姿势是先备份数据目录至少备份ibdata1和整个datadir的目录结构快照有条件的用文件系统快照再去考虑修复或切换。有一种情况我要特别提一下主库进程还活着但完全无法写入多半是磁盘满了或者临时表空间满了。此时从库可能并不会停止同步因为读binlog不需要写主库数据文件。这时候优先处理主库的磁盘或配置问题通常不需要切换。2.2 停止从库复制保住最后的“干净数据”如果你判断主库需要长时间停机下一件事就是锁住从库避免后续误操作继续污染数据。很多文章的推荐顺序是“先执行切换”但我的习惯是“先停复制”。在从库上执行STOP SLAVE IO_THREAD;只停IO线程不停SQL线程是因为这样可以让SQL线程继续把已经拉到本地的中继日志应用完尽可能追平数据同时不再读取主库新的binlog。等SHOW SLAVE STATUS\G中的Slave_SQL_Running显示为Yes并且观察到Retrieved_Gtid_Set和Executed_Gtid_SetGTID模式不再变化之后再执行STOP SLAVE;此时从库已经是一个“数据冻结点”。我再解释一下为什么这个顺序如此重要如果一上来就STOP SLAVE那么从库可能还积压着大量中继日志没有应用后面的切换步骤当然没问题但这个从库拿到手的其实是“旧数据”不是“尽最大努力追平的数据”。多等几分钟让SQL线程把日志应用完能极大降低切换后的数据丢失风险。另外如果你用的是GTID复制建议在停止复制前执行一次SELECT GLOBAL.gtid_executed;把当前已执行的事务清单保存下来。这是之后重建复制关系时绝对需要的依据。3. 从库晋升主库的标准动作从STOP SLAVE到RESET SLAVE ALL好了此时从库已经追平或尽可能追平了主库数据复制也停下来了。接下来就是真正的“切换主库”动作让从库变成可写的主库。这里有一个容易出错的细节很多人以为只要把只读参数关掉就行了。实际上MySQL从库的只读状态只是read_only参数或者从机库特有的super_read_only而复制关系遗留下来的元数据如果不清理干净后面和旧主库重新构建复制时会冲突。标准动作我拆成四步。3.1 停止只读并清理复制元数据先确认从库的只读状态SELECT read_only, super_read_only;如果返回1执行SET GLOBAL read_only OFF; SET GLOBAL super_read_only OFF;如果是8.0版本可能会提示部分参数是只读的需要写在配置文件中重启但绝大多数情况下动态设置就可以生效。接下来清掉复制元数据RESET SLAVE ALL;注意这里我用的是ALL而非不带参数的RESET SLAVE。两者的区别在于RESET SLAVE只清掉master info和relay log info而RESET SLAVE ALL会移除所有复制相关的元数据文件包括IO线程和SQL线程的信息相当于把一台从机完全“去复制化”。这样它才更像一台独立的新主库。3.2 让从库变成可写并记录关键位点清理完复制元数据后这台机器本质上已经是一台独立MySQL实例了。在正式暴露给业务前我还会做两件事。第一把binlog开启并设置log_slave_updates。如果你原来的从库只是为了只读查询很可能没开启log_slave_updates从库把从主库应用的事务写入自己的binlog。但从库晋升主库后它自己也要承担“被别人复制”的角色没有log_slave_updates后面的新从库将无法基于它建立复制。用SQL确认SELECT log_bin, log_slave_updates;如果log_bin为OFF那就麻烦了说明这台从库之前根本没开binlog你得在配置文件中加[mysqld] log-binmysql-bin log_slave_updatesON binlog_formatROW然后重启MySQL。虽然这意味着短暂中断但此时业务还在旧主库挂掉的状态反而是一个不错的窗口。第二记录关键位点信息方便后续验证和恢复。传统位点模式下执行SHOW MASTER STATUS;记录File和PositionGTID模式则记录GLOBAL.gtid_executed。不管你是人肉操作还是交给脚本这两个字段都是你证明“我是从哪儿接班的”最好证据。3.3 关于自动切换工具的个人看法上面这套流程也可以交给工具做比如MHA、orchestrator、MySQL Router配合InnoDB Cluster甚至MGRMySQL Group Replication自带选举能力。我实际用过MHA和orchestrator各有各的优势但我不建议你在完全理解手动流程之前就直接上自动切换工具。原因很简单工具帮你省掉了“敲命令”的动作却省不掉“判断输入条件”的脑子。你至少得熟悉STOP SLAVE、RESET SLAVE ALL和位点确认这套逻辑才能在工具误判时反应过来。如果公司已经有MGR主库故障后剩余的primary节点会自动选举出新的主节点应用不需要干预这也是一个很好的方案。但MGR对网络延迟、节点配置要求比较高不适合随便套用到既有主从架构上。4. 业务无感切换的关键环节VIP漂移与应用重连数据库层面切换完只代表这台MySQL实例开始接受写入了但业务还连着旧主库的地址呢不会自动转过头来。这里需要引入VIP漂移或者DNS切换才能让应用请求找到新主库。4.1 切换VIP或修改DNS记录我的经验是用VIPVirtual IP的方式让应用始终访问同一个IP故障时只需把VIP从旧主库网卡解绑绑定到新主库网卡上。比如旧主库IP是10.0.0.10从库IP是10.0.0.11VIP为10.0.0.20# 在旧主库上解绑VIP ifconfig eth0:0 10.0.0.20 down # 或者 ip addr del 10.0.0.20/24 dev eth0 # 在新主库原从库上绑定VIP ifconfig eth0:0 10.0.0.20 netmask 255.255.255.0 up # 或者 ip addr add 10.0.0.20/24 dev eth0注意一定要先解绑再绑定避免出现IP冲突导致网络不可用。如果用的是云厂商的SLB或内网负载均衡就改后端服务器组把旧主库摘除、把新主库加进去。DNS模式的缺点是缓存只要应用侧或本地解析有缓存切换生效时间就不可控。所以我通常只在APP连接串本身支持多地址时才敢用DNS。如果你只有DNS一种方式记得先把TTL设短比如60秒。4.2 应用连接池的恢复与预热VIP切换完成后应用不可能马上恢复——绝大多数连接池都不会无限重试而是会在连接失败后退避一段时间。等连接池里的旧连接全部失效并重建时业务才真正恢复。这里有个容易被忽略的点连接池参数。常见的Java应用用的是HikariCP默认connection-timeout是30秒MySQL连接超时默认8小时。当主库挂了池子里的连接都是坏的应用每次尝试拿连接都要等30秒才超时。把connection-timeout调小到3~5秒并且在配置里加上connection-test-query: SELECT 1让连接池在切换后更快识别坏连接并重建。同时新主库刚上线缓冲池是冷的大量表数据和索引页需要从磁盘加载短时间内可能出现“切换后反而变慢”的情况。有条件的话在切换前对关键表执行SELECT COUNT(*) FROM major_table;这种全表扫描可以强制把数据页读入buffer pool起到一定的预热作用。4.3 切换后的数据一致性验证切换完不等于万事大吉一定要先做数据验证。我会从新主库挑几张核心业务表对比数量级和关键指标SELECT COUNT(*), SUM(amount) FROM orders; SELECT MAX(created_at) FROM orders;如果旧主库还有办法打开比如只读模式起来也可以连接上去做行数比对。但更靠谱的方式是比对binlog位点前的主库快照数据这需要提前做过校验不是现场能补的。所以我的建议是定期做全量数据校验如pt-table-checksum这样主库出问题时你能知道从库的数据偏差范围。5. 旧主库回归重建复制链路的常见坑主库故障后的修复往往需要时间但等旧主库重启成功你不可能让它继续当独立节点否则会出现“双主写同一业务”的脑裂风险。一般人会想当然地“把旧主库挂成新主库的从库”但实际操作里至少有三个坑。5.1 旧主库缺少部分事务时的处理旧主库在故障期间可能丢失了部分事务也可能因为IO线程停滞后没有接收到新主库上发生的所有写入。如果直接CHANGE MASTER TO指向新主库从新主库那个binlog位点开始拉取旧主库的本地数据就会和新主库不一致。这个场景下正确的做法是先备份旧主库上能抢救的数据然后在新主库上用mysqldump或xtrabackup做一次全量备份恢复到旧主库再从旧主库CHANGE MASTER TO新主库。不要省这一步全量备份数据一致性比省一个小时重要得多。如果是GTID模式旧主库的gtid_purged如果处理不干净复制会直接报错。我通常会这样操作在旧主库上执行SET GLOBAL gtid_purged xxxxxxx-xxxx-xxxx:1-N;这里的GTID集合必须完全覆盖新主库已有的执行集合否则SLAVE连接时会因为“server has purged binlogs“之类的错误失败。具体数值从新主库上SELECT GLOBAL.gtid_executed获取。5.2 避免在旧主库上重启SQL线程导致的重复事务在重建复制时最怕的动作是旧主库本地还有未应用的relay log但你没清理干净START SLAVE后SQL线程重新执行了一部分事务与后来新主库发来的事务产生主键冲突。所以清理旧主库的复制状态必须彻底RESET SLAVE ALL;然后再CHANGE MASTER TO MASTER_HOST10.0.0.11, MASTER_USERrepl_user, MASTER_PASSWORDpassword, MASTER_PORT3306, MASTER_AUTO_POSITION1; START SLAVE;如果用的是传统位点复制必须把MASTER_LOG_FILE和MASTER_LOG_POS填写为从新主库SHOW MASTER STATUS拿到的值写错一个数字都会导致丢数据或重复数据。5.3 变更业务连接别让VIP切回去太早旧主库恢复后很多人会习惯性把VIP切回旧主库觉得“旧主库配置更高、更带劲”。这是一个让我踩过两次坑的坏习惯。除非旧主库的硬件性能碾压新主库且你能确认旧主库完全追平了所有数据否则不建议立刻切回去。稳妥的做法是让旧主库以“从库角色”稳定运行一段时间观察24小时延迟和错误日志再决定是否把VIP切回。6. 我曾经踩过的三个雷写给运维同行的提醒最后这部分是最私人的经验总结和教科书无关都是留在故障复盘PPT里的教训。第一个雷是“过度相信Seconds_Behind_Master”。有一回主库负载很高从库的Seconds_Behind_Master一直显示0我以为数据是齐的结果切换后发现从库少了近5分钟的事务。后来排查才知道SQL线程执行完一批事务后回退到等待状态计算落后时间时用了错误的对照点导致显示为0。现在我的习惯是切换前一定要对比binlog位点至少执行三次SHOW MASTER STATUS和SHOW SLAVE STATUS确认位点不再变化。第二个雷是“切换前忘了关super_read_only”。MySQL的从库只读机制有个微妙之处如果主从复制关系还开着super_read_only会阻止包括超级用户在内的所有写入而普通用户只受read_only限制。我曾在脚本里只关了read_only结果应用连接是用超级账号建的写连接被super_read_only挡住业务恢复直接延迟了十几分钟。现在写切换脚本时我固定先查super_read_only。第三个雷是“旧主库回归时没检查binlog是否已包含新主库的GTID”。有一次旧主库崩溃恢复很顺利我直接开复制结果报错GTID_NEXT can not be set to uuid:1原因就是旧主库的gtid_executed里没有新主库对应的GTID。最后只好清空旧数据、重新全量恢复浪费了大半天。这个教训告诉我GTID模式下不要老想着“增量修复”没有完整备份打底一切增量都是空中楼阁。现在再回想那个凌晨的故障处理我的最终选择其实是走了一套“STOP SLAVE确认位点→RESET SLAVE ALL→VIP切换→重建旧主库复制”的流程。整个过程不算复杂但每一步都建立在事前演练和定期校验的基础上。希望大家在看完这篇文章之后也能排查一下自己MySQL环境里有没有类似的问题从库追平过吗VIP切换脚本试过吗连接池参数合理吗如果这些都没验证过我建议你先做一次安全演练——找一个业务低峰期把主库停掉看看整个链路能不能经受住考验。虽然演练时紧张但总比真实故障时再手忙脚乱强得多。