同城机房MySQL迁移实战:MHA切换与GTID数据一致性复盘
1. 一次同城机房迁移的完整复盘:为什么这件事比想象中更棘手
同城数据中心切换,听起来比跨城容灾简单不少——物理距离近,专线延迟低,理论上把主库一停、从库一升就完事了。但真正操作过的人都知道,同城切换的难点从来不在网络层面,而在于数据一致性校验、切换窗口控制、以及回滚方案的完备性。我这次接手的项目,是把一套跑了三年多的MySQL核心业务库从A机房整体迁移到同城B机房,业务侧要求RTO控制在5分钟以内,RPO为0。这套库当时是1主3从的架构,主库承载读写,三个从库分别承担报表查询、数据同步下游、以及一个冷备角色,版本是MySQL 5.7.44,开启了GTID模式,用MHA做故障切换管理。
先说结论:整个切换从正式停写到最后业务恢复写入,实际耗时3分42秒,数据零丢失,回滚预案没有用上。但这个结果背后,是前后将近两周的准备和三次演练。这篇文章我会把整个过程中的架构分析、工具选型、参数计算、实操步骤、踩过的坑全部摊开讲,尤其是那些文档里不会写、只有真正切过库的人才知道的细节。如果你手上也有类似的迁移任务,或者正在搭建同城双活架构,这篇内容可以直接拿去当参考模板。
适合阅读的人群包括:负责MySQL运维的DBA、需要参与数据库迁移的后端工程师、以及正在评估MHA是否还值得用的架构师。我会尽量用大白话把每个决策背后的逻辑讲清楚,不堆术语,不绕弯子。
2. 迁移前的架构梳理与方案选型
2.1 现有架构的真实状态盘点
动手之前第一件事不是装软件,而是把现有架构摸清楚。很多迁移出问题,根源就在于对现有环境的理解停留在“文档写的”层面,而不是“实际跑的”层面。我当时做了一次完整的架构盘点,发现了好几个文档里没写的东西。
主库配置是8C16G,数据量约420GB,binlog格式为ROW,GTID模式开启,sync_binlog=1,innodb_flush_log_at_trx_commit=1,双一配置是齐的。三个从库中,有两个是级联复制(从库A再往下挂了一个从库),另一个是直连主库。MHA管理节点独立部署在一台2C4G的虚机上,配置了VIP漂移。这里有个关键发现:级联复制的存在会直接影响切换方案,因为如果只提升直连从库,级联从库需要重新指向新主库,这个链路调整必须在切换窗口内完成。
另外还发现主库上有一个定时任务,每天凌晨2点会跑一批数据归档的存储过程,这个任务在切换期间必须暂停,否则可能在新主库上重复执行。这种业务侧的隐性依赖,不主动去查根本不会有人告诉你。
2.2 为什么最终选择MHA而不是其他方案
市面上同城切换的方案大致有几类:MHA、Orchestrator、MySQL Group Replication(MGR)、以及云厂商的数据库代理层方案。我们最终选了MHA,原因很实际。
MGR虽然原生支持自动故障切换,但对网络延迟极其敏感,同城机房之间的专线延迟虽然低,但抖动是存在的,MGR在这种环境下容易出现节点被误踢的情况。而且MGR要求所有表必须有主键,我们这套库里有几张历史遗留表没有主键,改造成本太高。Orchestrator功能更强,支持拓扑自动发现和Web界面管理,但引入新组件意味着团队要重新学习,而且当时Orchestrator的版本在5.7环境下的兼容性还需要验证。云厂商方案直接排除,因为我们是自建机房。
MHA的优势在于:团队已经用了三年,运维人员熟悉它的行为模式;支持GTID模式下的切换;VIP漂移对业务侧透明,应用不需要改连接串。劣势也很明显:MHA本身已经很久不更新了,社区活跃度下降,而且它的切换逻辑在某些边界情况下需要人工介入。但综合评估下来,用熟悉的工具做可控的操作,比用新工具冒未知的风险更稳妥。
2.3 GTID模式下的切换逻辑拆解
GTID是这次切换的核心基础,必须把它的行为逻辑讲透。在GTID模式下,每个事务都有一个全局唯一标识,格式是source_id:transaction_id。从库在复制时,会记录已执行的事务GTID集合。切换时,新主库需要确保它的GTID集合是旧主库的超集,或者至少包含所有已提交的事务。
这里有个关键点:MHA在GTID模式下的切换,会先对比新旧主库的GTID差异。如果新主库缺少某些事务,MHA会尝试从旧主库的binlog中补齐。但如果旧主库已经宕机无法访问,这些事务就补不回来了,会导致数据丢失。所以我们在切换前做了一次强制的主从同步校验,确保所有从库的Retrieved_Gtid_Set和Executed_Gtid_Set与主库完全一致。
具体校验命令是这样的:
对比Executed_Gtid_Set是否一致,如果不一致,说明从库还有未执行完的事务,需要等待。这个等待时间必须计入切换窗口。
3. 切换前的准备工作与三次演练实录
3.1 新机房环境搭建的五个关键步骤
新机房的环境搭建不是简单的装个MySQL就完事,有几个细节直接决定后续切换的成败。
第一步是版本对齐。旧环境是5.7.44,新环境必须也是5.7.44,小版本号都不能差。因为不同小版本之间的binlog格式可能有细微差异,GTID的处理逻辑也可能不同。我们当时从官方渠道下载了对应的rpm包,用rpm -ivh安装,安装后用mysql --version确认版本一致。
第二步是参数对齐。把旧主库的my.cnf完整拷贝过来,重点检查server_id、gtid_mode、enforce_gtid_consistency、log_slave_updates、binlog_format这几个参数。server_id必须全局唯一,我们给新环境的每台机器分配了新的ID段。log_slave_updates必须开启,否则级联复制会断。
第三步是目录规划。数据目录、binlog目录、relay log目录分开存放,避免IO争抢。我们用的是独立的SSD盘挂载到/data/mysql,binlog单独放在/data/binlog。
第四步是系统层面调优。关闭THP(透明大页),调整vm.swappiness=1,设置ulimit -n为65535,这些是MySQL运行的基础要求。THP不关会导致MySQL在高负载下出现性能抖动,这个坑我踩过不止一次。
第五步是网络连通性验证。从新机房的每台机器上,用telnet或nc测试到旧主库3306端口的连通性,同时测试反向连通性。专线虽然通,但防火墙策略可能只开了单向,这个必须提前确认。
3.2 用Xtrabackup做全量备份与恢复的实操细节
数据同步的第一步是把旧主库的全量数据搬到新从库。我们用的是Percona Xtrabackup 2.4版本,适配MySQL 5.7。操作流程如下:
这里有几个实操心得。第一,备份期间要记录binlog位置,Xtrabackup会在备份目录下生成xtrabackup_binlog_info文件,里面记录了备份结束时对应的GTID集合,这个信息在搭建从库时要用。第二,--prepare阶段如果数据量大,耗时会很长,我们420GB的数据prepare花了将近40分钟,这个时间要算进准备周期。第三,传输用rsync而不是scp,因为rsync支持断点续传,专线万一抖动不至于重头再来。
3.3 基于GTID搭建从库的完整流程
全量恢复完成后,新从库的数据是备份时间点的状态,需要通过GTID继续追平。操作如下:
MASTER_AUTO_POSITION=1是GTID模式的关键,它让从库自动根据GTID差异去主库拉取缺失的事务,不需要手动指定binlog文件和位置。启动后用SHOW SLAVE STATUS\G检查Slave_IO_Running和Slave_SQL_Running是否都是Yes,以及Seconds_Behind_Master是否在下降。
注意:
gtid_purged的值必须准确,设置错误会导致从库无法启动复制,或者出现GTID冲突。如果不确定,可以从备份目录的xtrabackup_binlog_info文件中直接读取。
3.4 三次演练暴露出的问题与修正
准备工作做完后,我们做了三次演练,每次都在测试环境模拟真实切换。第一次演练暴露的问题是VIP漂移耗时超出预期,MHA切换VIP用了将近90秒,原因是ARP缓存刷新慢。后来调整了交换机的ARP老化时间,第二次演练降到30秒以内。
第二次演练发现级联从库的重新指向没有自动化,需要手动执行CHANGE MASTER TO,这个操作在紧张环境下容易出错。我们后来写了一个脚本,在MHA切换完成后自动触发级联从库的重新指向。
第三次演练基本顺利,但发现应用侧的连接池没有及时感知VIP变化,部分连接还指向旧主库。解决办法是在切换后主动重启应用连接池,或者配置连接池的validationQuery和testOnBorrow,让它自动剔除失效连接。
4. 正式切换当天的操作全记录
4.1 切换窗口的时间线拆解
正式切换选在了一个周二凌晨1点,业务低峰期。整个时间线如下:
| 时间点 | 操作内容 | 耗时 |
|---|---|---|
| 01:00 | 确认所有从库延迟为0,暂停定时任务 | 2分钟 |
| 01:02 | 通知业务方准备停写 | 1分钟 |
| 01:03 | 业务侧停止写入,主库设置read_only=ON | 30秒 |
| 01:04 | 确认主库无活跃写入事务 | 30秒 |
| 01:05 | 执行MHA切换,提升新主库 | 90秒 |
| 01:06 | VIP漂移到新主库 | 30秒 |
| 01:07 | 级联从库重新指向新主库 | 40秒 |
| 01:08 | 校验新主库数据一致性 | 30秒 |
| 01:09 | 新主库设置read_only=OFF,恢复写入 | 10秒 |
| 01:10 | 业务侧恢复连接,验证读写正常 | 2分钟 |
实际总耗时3分42秒,比预期的5分钟窗口有富余。
4.2 MHA切换的核心命令与输出解读
MHA的切换命令是:
--master_state=alive表示旧主库还活着,这是计划内切换。--orig_master_is_new_slave表示切换后把旧主库变成新主库的从库,这样如果出问题可以快速回滚。
切换过程中MHA会输出大量日志,重点看几个阶段:Checking slave status、Waiting for all slaves to catch up、Starting failover、Promoting new master、Failing over VIP。如果卡在某个阶段超过预期时间,要立即介入排查。
提示:MHA切换前会检查所有从库的复制状态,如果有从库延迟过大,切换会被阻塞。所以切换前必须确认所有从库的
Seconds_Behind_Master为0。
4.3 切换后的数据一致性校验方法
切换完成后,不能直接宣布成功,必须做数据校验。我们用了两个层面的校验:
第一层是GTID集合对比。在新主库和旧主库(此时已变成从库)上分别执行SHOW MASTER STATUS和SHOW SLAVE STATUS,对比Executed_Gtid_Set是否一致。如果不一致,说明有事务丢失或重复。
第二层是业务数据抽样对比。选取几张核心业务表,对比切换前后的行数和关键字段的校验和。我们写了一个简单的Python脚本,用checksum table命令做全表校验:
对比新旧主库的校验和结果,完全一致才确认数据无损。
4.4 回滚预案的设计与触发条件
回滚预案是切换方案里最重要的部分,没有之一。我们的回滚设计是这样的:如果切换后5分钟内发现严重问题(比如新主库无法写入、数据校验不一致),立即执行回滚。
回滚操作就是把VIP切回旧主库,把旧主库的read_only关掉,同时把新主库重新指向旧主库。因为切换时用了--orig_master_is_new_slave,旧主库此时是从库状态,数据是完整的,回滚只需要反向操作一次。
触发回滚的条件我们定了三条:新主库写入失败、数据校验不一致、业务侧反馈核心功能不可用。任何一条触发,立即回滚,不犹豫。切换窗口内的时间非常宝贵,犹豫不决比错误决策更致命。
5. 踩过的坑与常见问题排查手册
5.1 GTID冲突导致从库无法启动的解决过程
演练时遇到过一次GTID冲突,从库启动复制时报错:
原因是主库的binlog被purge了,而从库需要的GTID还在被purge的binlog里。解决办法有两个:一是从备份重新搭建从库,二是用gtid_purged手动跳过缺失的事务。我们选了第一种,因为第二种有数据丢失风险。
这个问题的根源是binlog保留时间设置太短。后来我们把expire_logs_days从3天调整为7天,确保有足够的窗口应对突发情况。
5.2 主从延迟突然飙升的排查思路
切换前一周,监控告警显示某个从库延迟突然从0飙升到3000秒。排查过程如下:
首先看SHOW SLAVE STATUS中的Seconds_Behind_Master和Slave_SQL_Running_State,发现SQL线程卡在某个UPDATE语句上。然后用SHOW PROCESSLIST查看该从库上的查询,发现有一个大查询在跑,占用了大量IO。杀掉该查询后,延迟逐渐恢复。
进一步排查发现,这个从库上跑了一个报表任务,没有加LIMIT,全表扫描了一张千万级的大表。解决办法是把报表任务迁移到专门的从库上,并且给报表查询加上资源限制。
经验:从库延迟不一定是复制本身的问题,很多时候是从库上的其他查询抢占了资源。排查时要先看从库自身的负载,再看复制链路。
5.3 VIP漂移失败的三种常见原因
VIP漂移失败是MHA切换中最常见的问题,我们遇到过三种情况:
第一种是ARP缓存未刷新。VIP漂移后,交换机还认为旧主库持有该VIP,导致流量仍然打到旧主库。解决办法是在切换脚本中加入arping命令,主动刷新ARP缓存。
第二种是VIP被其他设备占用。检查发现是另一台测试机误配了相同的VIP。解决办法是建立VIP台账,所有VIP的分配必须登记。
第三种是MHA管理节点与主库网络不通。MHA管理节点需要能SSH到所有MySQL节点,如果网络策略变更导致SSH不通,切换会失败。解决办法是定期检查MHA管理节点的连通性。
5.4 切换后应用连接异常的快速处理
切换后最常见的应用侧问题是连接异常。表现是应用报Communications link failure或Connection refused。原因通常是应用连接池还持有旧主库的连接。
快速处理方法是重启应用连接池,或者等待连接池的maxWaitTime超时后自动重建连接。更优雅的做法是在应用配置中设置testOnBorrow=true和validationQuery=SELECT 1,让连接池在借用连接时自动检测有效性。
如果应用使用了DNS而不是VIP来连接数据库,还需要刷新DNS缓存。我们当时统一用VIP,所以没有这个问题,但如果你用的是DNS,记得提前把TTL调低。
6. 切换完成后的收尾与长期优化
6.1 旧机房的降级与资源回收
切换完成后,旧主库变成了新主库的从库,继续承担冷备角色。旧机房的其他从库,我们保留了一个作为异地备份,其余的逐步下线。资源回收分三步:先停掉MySQL服务,观察一周确认没有业务依赖;然后释放虚机资源;最后清理监控和告警配置。
这里有个细节:旧机房的binlog不要立即删除,保留至少一周,万一新环境有问题,旧机房的binlog可以作为最后的恢复手段。
6.2 监控告警的重新配置
切换后,监控告警的配置需要同步更新。重点更新以下几项:主库地址从旧IP改为新IP,VIP监控目标更新,复制链路监控更新,以及新增新机房的网络延迟监控。我们用的是Prometheus+Granfana,只需要更新prometheus.yml中的targets即可。
另外建议增加一个切换后专项监控,在切换后的一周内,重点关注新主库的CPU、IO、连接数、慢查询数量,以及从库延迟。这些指标能提前发现潜在问题。
6.3 同城双活架构的后续演进方向
这次切换虽然成功了,但本质上还是主备模式,新机房的主库承担全部读写,旧机房只是冷备。后续的演进方向是同城双活,两个机房都能承担读写流量。这需要解决几个问题:数据双向同步、冲突检测与解决、流量调度策略。
数据双向同步可以考虑用GTID+双主架构,但双主架构的冲突处理很复杂,需要业务侧配合做数据分片,比如按用户ID哈希路由到不同机房。流量调度可以用DNS或负载均衡器,根据机房健康状态动态调整权重。
这个演进不是一蹴而就的,需要分阶段推进。第一步先把读流量切到同城双机房,写流量仍然单点;第二步做写流量的分片;第三步实现自动故障切换。每一步都要有完整的演练和回滚方案。
6.4 这次切换留下的经验清单
最后整理一份经验清单,都是实际踩出来的:
- 切换前必须做至少两次全流程演练,第三次演练要模拟真实故障场景。
- 所有从库的延迟必须为0才能开始切换,差一秒都不行。
- VIP漂移的ARP刷新要写进脚本,不能依赖人工。
- 级联从库的重新指向要自动化,手动操作在紧张环境下极易出错。
- 回滚预案要简单直接,触发条件要明确,不要有模糊地带。
- 切换窗口内的时间记录要精确到秒,事后复盘才有依据。
- 旧机房的binlog保留一周,这是最后的保险。
- 应用侧的连接池配置要提前检查,
testOnBorrow和validationQuery是标配。 - 监控告警要在切换后立即更新,不能有监控盲区。
- 切换完成后不要立即解散团队,观察至少24小时再收工。
这套流程后来我们又用了两次,一次是机房搬迁,一次是硬件升级,每次都顺利。工具会过时,但方法论不会。把原理搞清楚,把细节做到位,切换这件事就没有那么可怕。