排查MySQL主从延迟,本质上是在追踪一个二进制日志事件从产生到被从库执行完毕所消耗的时间。不要一上来就盲目地重启或重建,那样只会掩盖问题。最直接的办法是先确认延迟的具体形态,是那种持续增长的延迟,还是偶尔出现的尖峰,亦或是永远卡在某个固定时间点不动。你可以直接在从库执行 SHOW SLAVE STATUS\G,重点看三个字段:Seconds_Behind_MasterRelay_Log_Space 以及 Slave_IO_RunningSlave_SQL_Running 的状态。如果IO线程是No,那是网络或主库连接问题;如果SQL线程是No,那通常是遇到了导致回放中断的错误,比如主键冲突或数据不存在。

从网络层和IO线程入手

很多人习惯性忽略网络,但跨机房同步或带宽被打满时,主库产生的binlog根本无法及时传输到从库。你可以在主库执行 SHOW MASTER STATUS 记下当前的binlog文件和位置,然后到从库看IO线程拉取到了哪里。如果从库的 Master_Log_FileRead_Master_Log_Pos 跟主库差距很大,且 Seconds_Behind_Master 在不断增加,那瓶颈大概率在传输层。这时可以检查主库的 max_allowed_packet 是否过小导致大事务传输失败,或者 slave_net_timeout 设置是否合理。另外,如果是MySQL 8.0版本,可以开启binlog压缩 binlog_transaction_compression=ON,能显著减少网络传输量,但会增加CPU开销,需要权衡。

大事务与批量操作是延迟的罪魁祸首

这是生产环境中最常见的延迟原因。主库上一条更新了500万行数据的SQL,在从库上回放时可能也需要几分钟甚至更久。从库的SQL线程是单线程执行的(在传统复制模式下),当它还在吭哧吭哧地执行那个大事务时,后续的所有小事务都被堵在后面排队。你可以通过 SHOW ENGINE INNODB STATUS\G 查看从库当前的SQL线程正在执行什么,或者结合 performance_schema 里的 replication_applier_status_by_worker 表来定位。根本的解决思路是拆解大事务,把一条影响百万行的DELETE拆成循环小批量删除,每次删除1000行并提交一次。如果无法修改应用逻辑,那就只能从架构上考虑并行复制。

并行复制的配置陷阱

MySQL 5.7之后引入了基于逻辑时钟的并行复制(LOGICAL_CLOCK),但很多人配置了 slave_parallel_workers=8 就以为万事大吉,结果延迟依旧。关键点在于必须同时设置 slave_parallel_type=LOGICAL_CLOCKslave_preserve_commit_order=ON,并且主库的 binlog_transaction_dependency_tracking 参数也要配合调整。如果主库是5.7版本,默认的依赖跟踪机制是 COMMIT_ORDER,这会导致并行度受限于主库的提交顺序,效果大打折扣。升级到MySQL 8.0并设置 binlog_transaction_dependency_tracking=WRITESET,系统会根据事务修改的行数据来判断冲突,不冲突的事务在从库上就能真正并行执行,这对多热点数据的场景提升巨大。但要注意,如果从库要做读写分离,必须开启 slave_preserve_commit_order,否则从库上读到的数据顺序可能会乱,出现幻读。

从库硬件与配置不对等

主库是NVMe SSD,从库却是普通SATA SSD甚至机械硬盘,这种IO能力的不对等是延迟的温床。主库可以并发写入,但从库回放时大量的随机读写会让磁盘IOPS瞬间打满。你可以用 iostat -x 1 观察从库磁盘的 r_awaitw_await,如果经常超过10ms,说明磁盘已经是瓶颈。此时调整 innodb_flush_log_at_trx_commit 为2,或者调整 sync_binlog 为0,虽然会牺牲一点从库的持久性,但能极大缓解磁盘压力。另外,从库的内存也要足够大,确保 innodb_buffer_pool_size 能缓存热点数据,避免回放SQL时频繁发生物理读。

无主键表的灾难

这是一个隐蔽但致命的问题。如果某张表没有显式主键,InnoDB会隐式生成一个6字节的ROW_ID。在主库上执行 DELETEUPDATE 时,如果走全表扫描,从库回放这条SQL时同样会全表扫描。主库上可能因为数据在内存中而执行很快,但从库的buffer pool可能没缓存这张表的数据,导致每次回放都产生大量的随机读IO,延迟瞬间飙升。排查方法很简单,检查 SHOW SLAVE STATUSLast_SQL_Error 是否有类似“无法找到行”的报错,或者通过 sys.schema_unused_indexessys.schema_redundant_indexes 来辅助分析。根治方法就是给所有表加上显式主键,如果是日志表实在没有业务主键,就用自增ID。

锁冲突阻塞了SQL线程

如果从库承担了一部分读流量,或者你在从库上手动执行了某些查询或备份任务,这些操作可能会持有MDL锁或行锁,导致SQL线程在回放DDL或DML时被阻塞。你可以通过 SELECT * FROM performance_schema.metadata_locks WHERE OWNER_THREAD_ID != THREAD_ID(); 来查看当前持有的MDL锁。更常见的是,有人用mysqldump或xtrabackup备份时加了 --lock-tables--flush-logs 参数,直接锁住了关键表。排查时,执行 SHOW PROCESSLIST,看有没有状态为 Waiting for table metadata lock 的线程,如果这个线程恰好是SQL线程,那就要果断Kill掉阻塞它的源头连接。

基于GTID的复制故障恢复

当延迟是因为SQL线程报错中断时,如果你开启了GTID,处理起来会从容很多。常见的错误码如1032(更新时找不到行)或1062(主键冲突)。最偷懒但有效的方式是设置 slave_exec_mode=IDEMPOTENT,让从库忽略这些错误继续执行,但这只是临时方案,会掩盖数据不一致的问题。正确的做法是,先 STOP SLAVE SQL_THREAD;,然后通过 SHOW SLAVE STATUS 中的 Retrieved_Gtid_SetExecuted_Gtid_Set 找出差异的GTID。如果是1062错误,说明从库上已经存在这条数据,你可以手动删除从库上冲突的行,然后 START SLAVE SQL_THREAD; 让SQL线程跳过这个事务。更精准的跳过方式是使用 SET GTID_NEXT='冲突的GTID'; BEGIN; COMMIT; SET GTID_NEXT='AUTOMATIC'; 来注入一个空事务,从而优雅地跳过。

半同步复制与AFTER_SYNC的抉择

为了减少主从延迟带来的数据丢失风险,很多公司会开启半同步复制。但要注意,MySQL 5.7之后默认的 rpl_semi_sync_master_wait_pointAFTER_SYNC,它是在主库提交事务之前等待从库确认收到binlog。这虽然保证了主从数据的绝对一致,但如果从库的IO线程因为网络抖动稍有延迟,主库的写入就会被卡住,导致主库出现性能尖刺。排查时如果发现主库频繁出现 Waiting for semi-sync ACK from slave 状态,且 Rpl_semi_sync_master_status 频繁在ON和OFF之间切换,说明半同步退化了。此时可以考虑适当调大 rpl_semi_sync_master_timeout,或者检查网络链路的稳定性。

实战排查脚本与监控指标

不要每次都用肉眼去扫 SHOW SLAVE STATUS。你可以把下面的脚本部署到从库上,每分钟执行一次,把输出重定向到日志文件里,方便回溯问题发生时刻的现场。

#!/bin/bash
mysql -u root -p'password' -e "SHOW SLAVE STATUS\G" | grep -E "Slave_IO_Running|Slave_SQL_Running|Seconds_Behind_Master|Last_SQL_Error|Master_Log_File|Relay_Master_Log_File|Exec_Master_Log_Pos|Read_Master_Log_Pos" | while read line; do echo "$(date '+%Y-%m-%d %H:%M:%S') $line"; done

除了 Seconds_Behind_Master,你还需要监控从库的 Relay_Log_Space 变化趋势。如果这个值一直在增长,说明SQL线程消费速度跟不上IO线程接收速度,这是典型的回放瓶颈。另外,通过 SELECT * FROM sys.x$io_global_by_file_by_bytes WHERE file LIKE '%ibd'; 可以找出读写最频繁的表文件,结合 performance_schema.replication_applier_status_by_worker 里的 APPLYING_TRANSACTION 字段,你甚至能精确定位到当前正在执行慢回放的是哪张表。

终极方案:读写分离架构下的容忍与隔离

有时候,在业务逻辑层面容忍一定程度的延迟,比在数据库层面死磕要划算得多。如果你的应用做了读写分离,可以设计一个“主库写后立即读”的强制走主库机制。比如在用户刚发布文章后跳转到文章详情页时,在代码里通过Hint或特定数据源强制读主库,避免出现“发布成功但刷新后看不到”的诡异体验。对于报表或后台统计这类对实时性要求不高的查询,可以专门挂在延迟较大的从库上,并在代码里加上重试机制。如果延迟大到无法接受,那就考虑彻底重构复制架构,比如引入MySQL Group Replication (MGR) 或者使用中间件如ProxySQL、ShardingSphere来做更智能的读写分离和延迟感知路由,当从库延迟超过预设阈值时,自动将其流量摘除。