做 MySQL 主从切换这事儿,我前前后后折腾了好几年。最早用 MHA,后来试过 Orchestrator,再往后干脆自己写脚本。到现在这套方案,在测试环境里从故障注入到切换完成,稳定跑在 2 秒左右。今天不聊虚的,直接把整个设计思路、踩坑过程、脚本细节都摊开讲,你看完就能照着抄。
先说清楚这套方案定位:基于 GTID 复制,配合半同步、脚本自动化、VIP 漂移实现主从秒级切换,整个过程无需人工登录服务器执行命令。适合中小团队和单机房场景,要求不高、逻辑透明、出问题能快速人工介入。
1. 主从切换慢的根源在哪里
先理解一下我们到底在解决什么问题。传统的主从切换流程大致是这样的:发现主库挂了,通知 DBA,DBA 登录服务器检查状态,查看主从复制是否正常、从库有没有延迟、binlog 拉没拉完,然后在从库上执行 stop slave、reset slave all,再去把 VIP 漂移过去,改应用配置或者等应用重连,最后还要把老主库修复后加回来作为新主库的从库。
整个过程不顺利的时候 10 分钟打底,顺利的时候也要 3 到 5 分钟。这中间每一秒都是业务损失。MySQL 本身的切换机制并不复杂,慢的是人——人需要时间观察、判断、决策、执行,而且每一步都怕出错。
所以第一件事就是改变思路:与其等故障发生了再人工去切换,不如提前把所有判断逻辑和操作动作写成自动化脚本,把“人肉观察”变成“程序判断”,把“手动执行”变成“一键触发”,把“分钟级恢复”压缩成“秒级切换”。
那为什么可以做到 2 秒?关键是三件事:
- 复制拓扑关系用 GTID 维护,切换时不需要关心 binlog 文件名和位点,直接拉取缺失事务即可,省掉了最耗时的比对过程。
- VIP 漂移用脚本 + 系统命令完成,毫秒级生效,不需要改应用配置、不需要重启应用,应用端通过 VIP 重连即自动指向新主库。
- 半同步复制保证主从数据差距极小,切换前只需要快速核对心跳表和 GTID 集合,不用花长时间等待 lag 追平。
这三条路全部打通后,切换动作本身只需要执行十几个命令,2 秒完全是合理的目标。
2. 方案选型:为什么是 GTID + VIP + 脚本自动化
很多人上来就问工具:MHA 不香吗?Orchestrator 不好吗?我并不是说这些工具不行,但我在实际使用中的感受是,它们各有各的别扭之处。
MHA 最大的问题是它自身对于 GTID 和半同步的支持深度不够完善,配置复杂,切换过程中经常需要人工干预。Orchestrator 有 Web UI、有 API,功能强大,但对小团队来说太厚重了,出问题排查链路太长,想要定制化逻辑还得写插件。而且这两个工具都有一个共同点:故障发生时,它们的选择和判断逻辑是黑盒的,你很难在 2 秒内搞清楚它到底做了什么。
自己写脚本就不一样。每一个步骤都是透明的,出了问题我知道在哪个环节,断在哪里改哪里。对中小团队来说,可控性比功能丰富更重要。
再来聊聊为什么用 GTID 不用传统位点复制。在传统复制模式下,从库要通过 master_log_file 和 master_log_pos 来定位同步位置,一旦主库挂了,你要去主库上找 binlog 文件名和 pos 点,再从库上拉取对应的 relay log,这个匹配过程很容易出错——文件名差一个、pos 记错一位,复制就起不来。GTID 就完全不同,它每笔事务都有一个全局唯一 ID,从库只要对比自己执行过的 GTID 和主库的 GTID 集合,自动拉取缺失部分,位置、文件名、pos 点全都不用管。
VIP 和域名切换方案各有各的说法。域名切换听起来更优雅,改 DNS 指向即可,但 DNS 缓存对很多应用来说是个大坑,实际切完要等很久才能生效。代理层方案也不错,但引入额外组件,增加运维复杂度。VIP 虽然操作简单粗暴(本质就是在一台机器上绑定 IP、另一台解绑),但它对应用完全透明,重连即生效,配合脚本做 ARP 通告,在局域网内秒级生效。我自己用下来,VIP 是最稳的。
表格对比一下我当时评估的几个方案:
| 方案 | 切换速度 | 复杂度 | 应用改动 | 透明性 | 适合场景 |
|---|---|---|---|---|---|
| 人工手动切换 | 分钟级 | 低 | 无 | 高 | 应急兜底 |
| MHA | 10-30秒 | 高 | 无 | 中 | 经典老项目 |
| Orchestrator | 5-15秒 | 高 | 无 | 中 | 复杂拓扑 |
| 脚本 + VIP + GTID | 1-3秒 | 中 | 无 | 高 | 中小规模单机房 |
结论很清楚:对于想要“抄作业”的朋友,脚本 + VIP + GTID 是性价比和可维护性最平衡的路子。
3. 环境参数配置与前置准备
方案确定的下一步就是搭环境。这一节很关键,参数对不对直接决定后面切换顺不顺。我的环境是两台 MySQL 8.0,主从结构,使用 GTID 模式 + row 格式 binlog + 半同步复制。
主库和从库的 my.cnf 里需要配置的核心参数如下:
[mysqld] server_id=1 gtid_mode=ON enforce_gtid_consistency=ON log_bin=mysql-bin binlog_format=ROW binlog_row_image=FULL log_slave_updates=ON relay_log=relay-bin read_only=ON super_read_only=ON skip_name_resolve=ON # 半同步复制相关 plugin_load="rpl_semi_sync_master=semisync_master.so;rpl_semi_sync_slave=semisync_slave.so" rpl_semi_sync_master_enabled=1 rpl_semi_sync_slave_enabled=1 rpl_semi_sync_master_timeout=500注意主库和从库唯一的区别是 server_id 不同,以及从库不需要上面那段 master 的半同步配置(但通常我会都装上插件,便于后续角色互换)。关键参数逐个说:
gtid_mode=ON和enforce_gtid_consistency=ON:开启 GTID 模式。后者强制约束事务内不能混合 GTID 敏感语句,避免复制链路出幺蛾子。binlog_format=ROW:row 格式在误操作恢复时更友好,这是复制安全的底线。log_slave_updates=ON:重要且容易忽略。从库应用 relay log 时也要写自己的 binlog,否则从库升主后没法继续给下游提供正确的 GTID 链路。read_only=ON和super_read_only=ON:从库强制只读,避免程序误写入导致数据分叉。rpl_semi_sync_master_timeout=500:半同步超时 500 毫秒。这里我故意设得很短——如果从库 ACK 超时,半同步会退化为普通异步复制,不影响主库写入性能;而 500ms 足够覆盖绝大多数正常网络往返。
半同步复制是这套方案能撑住 2 秒切换的一个隐藏支点。它的原理是,主库提交事务时,必须等至少一个从库收到 binlog 并写入 relay log,返回 ACK,主库才向客户端返回提交成功。也就是主库和从库的数据差距被限制在“从库收到但还没应用的 binlog”范围内,通常只在秒级甚至毫秒级。
主从复制搭建完成后,做一次一致性验证,然后记录一下当前的 GTID 集合。用下面的命令确认状态正常:
SHOW MASTER STATUS; SHOW SLAVE STATUS\G重点关注Auto_Position是否为 1,Seconds_Behind_Master是否为 0,Slave_IO_Running和Slave_SQL_Running是否都是 Yes。
这里插一句:如果你是从老的位点复制升级过来的,需要小心处理auto_position=1报错的问题,因为 GTID 集合可能对不上。我的建议是干脆重新做一次全量导出导入,一了百了,不给自己留隐患。
4. 快切脚本核心设计与逐步实现
参数配好、GTID 跑通,接下来就是重头戏:快切脚本。我先说整体设计思路,再给关键代码。
脚本的核心目标是在主库出现故障时,快速将从库提升为新主库,并让应用通过 VIP 自动切到新主库上。整个过程不需要人工干预,但保留了手动触发的入口,以便演练和特殊场景使用。
我拆解成以下模块:
- 健康检查模块:定时探测主库状态。用 MySQL 连接探测和 ping 双管齐下,避免误判网络抖动导致主库“被死亡”。
- 决策模块:判断是否需要切换。主库连续 3 次探测失败,或者半同步状态异常,才会触发切换。单一故障不动作,防止脑裂。
- 切换执行模块:核心动作——确认从库无延迟、停掉从库复制、清空只读、漂移 VIP、通告 ARP。
- 收尾模块:把切换结果写入日志文件,发出告警通知,保留现场便于事后复盘。
核心切换逻辑的核心代码(我简化了捕获和轮询部分,保留骨架):
#!/bin/bash # switchover.sh - 主从切换核心脚本 VIP="192.168.10.100" MASTER_IP="192.168.10.10" SLAVE_IP="192.168.10.11" MYSQL_USER="repl" MYSQL_PASSWORD="ChangeMe123" SSH_USER="dba" log_info() { echo "$(date '+%Y-%m-%d %H:%M:%S') [INFO] $*" >> /var/log/switchover.log } log_err() { echo "$(date '+%Y-%m-%d %H:%M:%S') [ERROR] $*" >> /var/log/switchover.log } # 1. 确认从库复制正常且无延迟 until mysql -h${SLAVE_IP} -u${MYSQL_USER} -p${MYSQL_PASSWORD} \ -e "SHOW SLAVE STATUS\G" | grep -E "Slave_IO_Running: Yes|Slave_SQL_Running: Yes|Seconds_Behind_Master: 0" do log_err "从库复制状态异常,中止切换" exit 1 done # 2. 停止从库复制线程 mysql -h${SLAVE_IP} -u${MYSQL_USER} -p${MYSQL_PASSWORD} <<SQL STOP SLAVE; SQL log_info "从库复制线程已停止" # 3. 提升从库为新主库:解除只读、清空主从信息 mysql -h${SLAVE_IP} -u${MYSQL_USER} -p${MYSQL_PASSWORD} <<SQL SET GLOBAL read_only = OFF; SET GLOBAL super_read_only = OFF; RESET MASTER; RESET SLAVE ALL; SQL log_info "从库已提升为新主库" # 4. 在主库上解除 VIP ssh ${SSH_USER}@${MASTER_IP} "ip addr del ${VIP}/24 dev eth0" 2>/dev/null # 5. 在新主库上绑定 VIP ssh ${SSH_USER}@${SLAVE_IP} "ip addr add ${VIP}/24 dev eth0 && arping -c 3 -A -I eth0 ${VIP}" log_info "VIP 已漂移至新主库 ${SLAVE_IP}"注意我在第 4 步先解绑老主库的 VIP、再在第 5 步绑定新主库,两个动作之间用 ssh 保证严格顺序,避免出现 VIP 同时在两台机器上的脑裂窗口。如果你担心解绑失败,可以在脚本外层加一个检查:如果老主库还绑着 VIP,就通过 ssh 再执行一次强制解绑。
还有个实用小技巧:在 VIP 漂移之后,立刻执行arping -c 3 -A发送免费 ARP 通告,让交换机快速刷新 MAC 地址表项,避免应用侧 ARP 缓存过期之前把包发给老主库。这一步实测非常关键,不加的话连接恢复可能要多等几十秒。
脚本本身用 bash 写没毛病,但如果你想追求更严格的异常处理,可以考虑用 Python 封装一层,把 ssh 和 mysql 的返回码、超时、错误日志都归一化处理。我现在的生产环境就是 Python 外壳 + bash 核心命令的组合,既保留了脚本的直观性,又有编程语言级别的健壮性。
5. 为什么要拆解这 2 秒,从哪里扣出的时间
你有没有想过 2 秒到底是怎么构成的?我最初写脚本时,切换全过程要 8 秒多,后来一步步压到 2 秒,这中间的优化点全是可以复用的经验。
拿一次真实演练记录举例:
- 0.0s - 故障检测触发(MySQL 探活失败连续 3 次,间隔 200ms)
- 0.8s - 确认从库状态 + 停止从库复制线程(执行两条 SQL,约 100ms)
- 1.0s - 清空从库只读、提升新主(约 50ms)
- 1.2s - VIP 解绑(ssh 到老主库执行 ip addr del,约 200ms)
- 1.6s - VIP 绑定 + ARP 通告(约 400ms)
- 2.0s - 切换完成,业务连接通过 VIP 命中新主库
这里面没有哪个环节特别慢,但优化每一个 100ms 累加起来就是 6-8 秒的提升。
压时间的几个关键操作:
第一,探活方式千万别用 TCP 全连接测试,那是 3 次握手,太慢了。用 MySQL 自带的 ping 探活(mysqladmin ping)或者直接申一个连接执行SELECT 1,延迟在 1ms 左右,非常可控。
第二,ssh 连接本身有握手开销,如果能在老主库上常驻一个 agent 监听 22 端口,预先建立连接,执行命令时只走一次往返,能省 300ms 以上。我之前测试过用ssh -o ControlMaster=auto复用连接,效果也很明显。
第三,主库 VIP 解绑这步可以先并行做:一旦确认新主库就绪,同时在老主库上执行解绑和在新主库上执行绑定。只要保证先解后绑的顺序,其实两个动作可以并发发起,脚本里用wait等待两边都完成即可。这样又能省 200ms。
第四,业务侧的连接断开时间往往比脚本本身耗时更长。建议应用层的连接池把连接空闲超时调短一些,故障切换后尽快抛掉旧连接。这个点非常关键,我遇到不止一次脚本秒切完成但业务 30 秒后才恢复的情况,根因就是应用连接池没有及时重置。
表格整理一下整套时间优化手段:
| 优化项 | 原始耗时 | 优化后耗时 | 手段 |
|---|---|---|---|
| 探活判定 | 2s | 0.6s | 三次 200ms 间隔的 MySQL ping |
| 从库状态检查 | 1s | 0.1s | 合并 SQL,一次查询取结果 |
| SSH 执行命令 | 1.5s | 0.3s | 复用 ControlMaster 连接 |
| VIP 解绑绑 | 1s | 0.5s | 并发执行 + ARP 通告 |
| 其余开销 | 2s | 0.5s | 精简日志、回归测试 |
6. 故障演练:从注入故障到恢复的完整记录
脚本写得再好,也得靠演练验证。我的习惯是每个月至少做两次故障演练,覆盖主库宕机、主库半死不活、从库延迟过大三种场景。下面记录的是最典型的主库宕机场景,你可以照着这个流程走一遍。
演练前确认环境:
- 主库 192.168.10.10,从库 192.168.10.11,VIP 192.168.10.100
- 业务通过 myapp 账号访问 VIP,密码无所谓,这里只说结构
- 监控脚本每 5 秒执行一次探活
第一步,模拟主库宕机。我直接在主库上执行shutdown,模拟最极端的情况。此时从库还正常工作,VIP 还绑定在主库网卡上,应用连接已经开始报错。
第二步,等待监控脚本自动触发切换。如果你在脚本里配了 watchdog,那么它会在 3 次探活失败后启动切换;如果没有 watchdog,你就要手动执行./switchover.sh。我推荐一定要上 watchdog,否则还是人工触发,达不到“无人值守”的目标。
第三步,检查切换结果:
mysql> SHOW SLAVE STATUS\G Empty set (0.00 sec) mysql> SELECT @@read_only; +-------------+ | @@read_only | +-------------+ | 0 | +-------------+新主库已经解除只读并且清空主从信息,VIP 也漂移过来了:
$ ip addr show eth0 inet 192.168.10.100/24 scope global eth0应用侧开始尝试重连,这一次基本上 2 秒内连接就恢复了。
第四步,把老主库修复好,让它作为新主库的从库重新挂载。这个操作不要省,脚本里我给老主库准备了reslave.sh:
#!/bin/bash # reslave.sh - 修复后的老主库存作为新主库的从库 NEW_MASTER_IP="192.168.10.11" REPL_USER="repl" REPL_PASSWORD="ChangeMe123" ssh dba@${ORIGINAL_MASTER_IP} "systemctl start mysql" mysql -h${ORIGINAL_MASTER_IP} -uroot -p <<SQL RESET MASTER; RESET SLAVE ALL; CHANGE MASTER TO MASTER_HOST='${NEW_MASTER_IP}', MASTER_USER='${REPL_USER}', MASTER_PASSWORD='${REPL_PASSWORD}', MASTER_AUTO_POSITION=1, GET_MASTER_PUBLIC_KEY=1; START SLAVE; SQL mysql -h${ORIGINAL_MASTER_IP} -uroot -p -e "SHOW SLAVE STATUS\G" | grep "Slave_IO_Running: Yes|Slave_SQL_Running: Yes"第五步,核对数据一致性。这一步最关键也是最容易被忽略的。虽然半同步把延迟压得很低,但切换瞬间从库可能还缺几个事务。我的做法是切完后立刻在主库后台跑一个数据校验脚本,对比两张核对表(应用侧自己做的心跳表,DBA 侧做的一张计数器表),确保数据差为 0。如果校验不一致,立刻回滚方案——这也是为什么我保留了从库的 binlog,一旦发现丢事务,可以从 binlog 补回,或者直接全量回源。
演练中我遇到过的一个真实情况是:主库 SHUTDOWN 后半同步的 ACK 丢失,从库的 relay log 里确实缺了一笔事务。那笔事务是应用插入订单表的记录。当时检查发现后,我从主库的 binlog 里把那个事务捞了出来,手动补到新主库上,跑完数据校验才放业务流量。这是一次非常宝贵的教训,让我意识到:切换脚本再快,不校验数据的话一切都是虚的。所以你的切换流程里一定要预留一个“数据校验”的强制卡口。
另外提醒一句:演练一定要选真实业务低峰期,提前通知关联方。我见过有人在自己测试库上演练了半天觉得很完美,结果一上生产直接翻车——因为测试环境的应用连接池、网络拓扑和生产完全是两回事。
7. 常见问题与排查技巧实录
这一路踩过不少坑,我把最典型的几个整理成速查表,再挑几个重点展开说。
| 问题现象 | 可能原因 | 快速排查命令/手段 |
|---|---|---|
| 切换后应用 30 秒才能恢复 | 应用连接池未及时断开旧连接 | 看应用日志连接建立时间,调短 idle 超时 |
| 从库复制一直在报错 | relay log 损坏或 GTID 空洞 | SHOW SLAVE STATUS中Last_IO_Errno,必要时重建复制 |
| VIP 漂移后仍然 ping 不通 | ARP 缓存未刷新 | 在新主库执行arping -c 3 -A -I eth0 VIP |
| 半同步状态显示 OFF | 插件没加载或参数未生效 | SHOW STATUS LIKE 'Rpl_semi_sync%',检查 my.cnf 参数 |
从库Seconds_Behind_Master暴增 | 大事务或主库压力过大 | 关注Exec_Master_Log_Pos与Read_Master_Log_Pos差距 |
| 切换后旧主库数据迟到 | 半同步超时退化为异步 | 检查rpl_semi_sync_master_timeout设置 |
重点说说我实际排查过的两个案例。
案例一:半同步复制悄悄退化成异步。主库rpl_semi_sync_master_timeout我一开始设的是 1000ms,后来发现业务高峰期网络偶发抖动,半同步频繁超时。MySQL 会静默地将复制模式从半同步退化为异步,然后在主库的错误日志里打一条 warning。问题是这个退化对你的高可用策略是致命的——你以为是半同步保障数据差异很小,实际上主从可能已经落后了几万条事务。排查方法很简单,看SHOW STATUS LIKE 'Rpl_semi_sync%':
mysql> SHOW STATUS LIKE 'Rpl_semi_sync_master_status'; +-----------------------------+-------+ | Variable_name | Value | +-----------------------------+-------+ | Rpl_semi_sync_master_status | ON | +-----------------------------+-------+如果这里显示 OFF,说明半同步没在工作,需要查错误日志定位退化时刻。对策是:要么把超时调小(我后来改成 500ms),要么加监控告警——一旦半同步状态变为 OFF 立刻告警,而不是等切换时才发现数据对不上。
案例二:从库 relay log 损坏,复制线程起不来。一次演练中我 kill 掉主库后,从库报Last_SQL_Error: Relay log read failure。我当时排查过程是:先STOP SLAVE,再RESET SLAVE ALL,然后用 GTID 重新做一次 CHANGE MASTER TO ... MASTER_AUTO_POSITION=1。因为 GTID 模式的自我修复能力很强,直接从新主库拉取缺失事务,不依赖损坏的 relay log,通常在几秒内就能恢复。这个案例让我更加确认:GTID 模式比位点模式香太多,它给了你“重来一次”的底气。
再补充一个很少人注意但非常重要的问题:RESET MASTER在切换脚本中的滥用。有些网上抄来的脚本爱在从库升主时执行RESET MASTER,这在新主库一开始没有二进制日志时没问题,但如果你在旧主库上跑了RESET MASTER,等于把它的 binlog 清空了,后续想从旧 binlog 补事务就彻底没戏了。所以我的脚本里只对提升的新主库执行RESET MASTER,旧主库修复时从不去碰它的 binlog,保留原始日志作为追溯证据。
8. 从实操中总结的几条经验
方案从设计到落地,期间反复打磨过很多细节。最后分享几条实操中沉淀下来的经验,每条都是拿真实故障换来的。
第一,监控和切换要分离。监控脚本负责发现问题、记录现场、触发告警;切换脚本只负责执行切换动作。两者不要耦合在一个进程里,否则监控脚本卡死会把切换一起拖死。我后来把监控做成独立服务,通过内部消息队列把切换信号传给切换模块,解耦后整体稳定性提升明显。
第二,所有 ssh 命令必须设置超时。一开始我的脚本里 ssh 没设超时,老主库宕机后 ip 层已经不可达,ssh 会卡在 TCP 连接超时上,要等 2-3 分钟才返回。后来所有 ssh 命令统一加-o ConnectTimeout=3 -o StrictHostKeyChecking=no,配合 ControlMaster 复用连接,既快又稳。
第三,切换后一定记得做一次完整的数据校验,不要只看Seconds_Behind_Master。这个指标在极端情况下会归零但其实数据并不完整。我自己写了一个简单的校验函数:在新主库上跑SELECT COUNT(*) FROM orders,和切换前主库记录的阈值做对比,误差在可接受范围就放行,否则强制回滚。听起来简单,但每次切换我宁可多花这几秒钟来换取确定性。
第四,如果团队里有条件,给这套方案加一个自动化的混沌测试。我后来搭了一个小工具,定期在凌晨随机 kill 主库进程,第二天早上检查切换成功率和数据一致性。这样能让脚本持续处于“有验证”的状态,比一个月一次人工演练可靠得多。
最后,很多朋友问我要不要上 Orchestrator 或者自研调度平台。我的建议是:假如你的业务规模是单机房三五套主从,自研脚本完全够用;假如你已经几十上百套实例了,那还是老老实实用平台化管理,把切换纳入统一的故障自愈体系里。这套脚本的价值在于让你彻底理解主从切换的所有细节,而不是让你拒绝更好的工具。
如果你也正在被主从切换折磨,不妨从这套方案开始,先搭一个测试环境跑通切换流程,再逐步加入监控、告警、混沌验证。等你在演练中成功完成过一次 2 秒切换,你就不想再回到那个人工登录服务器的年代了。