大家好,我是小耶,写功课只是为了我踩过的坑,你们别再踩了!
早上10点,业务方在群里@你:“用户反馈刚下的单查不到了,是不是数据库有问题?”你打开监控,Seconds_Behind_Master显示0,复制状态正常,从库也没报错。你回了一句“看起来没问题”,然后用户开始疯狂刷新页面——订单回来了。
这是主从延迟最典型也最让人崩溃的场景:业务先感知,监控后知后觉。
今天把主从延迟的三种本质成因和完整排查路径彻底拆开讲一遍。
一、为什么监控不报延迟,业务已经感知到了?
Seconds_Behind_Master是主从延迟最常用的监控指标。但这个值的计算方式有一个前提:从库的IO线程和SQL线程都在正常运转,且主从间没有binlog积压时,这个值才有参考意义。
三种情况会让这个值“骗人”:
情况一:从库SQL线程卡住了,但IO线程还在拉binlog
IO线程把binlog源源不断地拉过来,Seconds_Behind_Master反映的是IO线程已经接收的最后一个事件与SQL线程已执行事件之间的时间偏移。如果SQL线程卡住了,这个值会不断增大,但你看到它的时候可能已经不是最新的状态了。
情况二:延迟是瞬间发生的,还没被监控采样到
监控系统通常每分钟采一次样。一个10秒内产生的延迟峰值,可能在采样间隙发生,又被追平了。业务方感知到了抖动,但监控曲线平滑得像什么事都没发生。
情况三:从库的查询慢了,不是复制慢了
从库上的SELECT查询被阻塞了,但复制线程还在正常工作。用户查不到数据不是因为数据没同步过来,而是查询本身被卡住了。
二、主从延迟的三大本质成因
成因一:大事务阻塞(最核心的“元凶”)
主从延迟最常见、最隐蔽的根源,就是主库的大事务。
主库执行了一个大事务——比如一次DELETE百万行,或者一次UPDATE … LIMIT 100000——事务在主库跑了30秒,binlog生成量巨大。从库必须完整回放完这一整段才能继续跟上。
从库回放是单线程的(即便是MTS模式,也存在协调开销),一个行数过大的事务等于在回放通道上投下了一颗“阻塞弹”。
真实案例:某电商平台定时任务在业务高峰期执行了一次UPDATE … LIMIT 100000,从库延迟瞬间从0拉到30秒以上。读写分离架构下的读请求立刻出现脏数据。
主库可能是这样写的:
-- 危险写法:大事务一次性处理
START TRANSACTION;
UPDATE orders SET status = 'archived'
WHERE create_time < '2025-01-01'; -- 可能影响几十万行
COMMIT; -- 从库要等这个事务完全回放完才能继续
根本原因:主库写入的速度远快于从库单线程回放的速度。主库是10条流水线同时干活,从库只有1个人在一件一件地做。
成因二:从库硬件配置低于主库
主库8核32G SSD,从库2核8G机械盘——同步怎么可能不慢?从库硬件配置最好不低于主库,尤其是磁盘IO。很多团队把从库当作“备胎”,用淘汰下来的旧机器跑从库,结果延迟问题从上线第一天就埋下了。
成因三:从库在跑大查询,抢了复制线程的资源
从库上跑了一个大范围的报表查询,扫描了百万行数据,CPU和IO都被占满了。复制线程的执行时间片被挤占,延迟曲线同步抬头。这不是复制链路本身的问题,而是从库被“本不该在此执行”的查询拖住了。
三、并行复制:从单线程到四代演进
要解决从库延迟,除了避开大事务,更重要的是让从库的SQL线程“跑得更快”。
MySQL的并行复制经历了四代演进:
| 代际 | 版本 | 并行依据 | 局限 |
|---|---|---|---|
| 第一代 | MySQL 5.6 | 基于Schema(数据库) | 单库多表场景基本无效 |
| 第二代 | MySQL 5.7.2+ | LOGICAL_CLOCK(组提交) | 真正意义上的突破,但仍有协调开销 |
| 第三代 | MySQL 5.7/8.0 | WRITESET | 基于行级冲突检测,并行度更高 |
| 第四代 | MySQL 8.0+ | WRITESET_SESSION | 兼顾并行度与事务顺序 |
LOGICAL_CLOCK的核心思路:不再看“是不是同一个库”,而是看“在主库是不是一起提交的”。MySQL通过Group Commit把多个事务的binlog攒在一起写盘,同一组的事务可以被并行回放。
WRITESET的进一步突破:基于行级冲突检测,只有真正修改了同一行的冲突事务才需要串行,其他都可以并行。并行度比LOGICAL_CLOCK更高。
四、排查路径:从现象到根因的三步法
第一步:确认延迟是否真实存在
SHOW SLAVE STATUS\G
重点关注三个字段:
Seconds_Behind_Master:延迟秒数,持续增长说明有问题Slave_IO_Running/Slave_SQL_Running:必须都是YesLast_IO_Error/Last_SQL_Error:报错信息,问题源头可能就在这里
如果Seconds_Behind_Master=0但还是查不到数据,可能是业务读到了旧快照(MVCC),不是延迟问题。
第二步:找到根因——是IO慢还是SQL慢?
在SHOW SLAVE STATUS中,看两个状态:
Relay_Log_Pos和Exec_Master_Log_Pos是否在持续增长如果
Relay_Log_Pos增长但Exec_Master_Log_Pos不变 → SQL线程卡住了
第三步:定位具体阻塞源
-- 查看从库当前执行的SQL
SHOW PROCESSLIST;
-- 查看是否有长时间运行的查询
SHOW FULL PROCESSLIST;
如果发现从库上有个大查询跑了30秒,binlog堆积,延迟飙升——那就是从库慢查询拖住了复制线程。
五、实战优化策略
策略一:拆分大事务,别让单次操作“堵死”从库
-- 正确做法:分批处理
SET @batch_size = 10000;
REPEAT
UPDATE orders SET status = 'archived'
WHERE create_time < '2025-01-01'
LIMIT 10000;
COMMIT;
-- 每批之间sleep一小段,让从库有时间追上
UNTIL ROW_COUNT() = 0 END REPEAT;
策略二:开启并行复制(MySQL 5.7+)
STOP SLAVE;
SET GLOBAL slave_parallel_workers = 4;
SET GLOBAL slave_parallel_type = 'LOGICAL_CLOCK';
START SLAVE;
MySQL 8.0+建议使用WRITESET,并行度更高:
SET GLOBAL slave_parallel_type = 'WRITESET';
slave_parallel_workers建议设置为CPU核心数或innodb_thread_concurrency的1/2左右。可以先设为4观察效果,再逐步调高。如果CPU低于60%且延迟还在涨,可以适当增加worker数量;如果CPU高于80%或锁争用明显,则要减少worker。
策略三:将复杂查询从从库挪走
从库只承担实时性不敏感的读请求,把复杂查询推到分析型节点或只读实例。
六、总结
主从延迟的排查,核心是三个认知:
监控会骗人:
Seconds_Behind_Master不是万能的,需要结合SHOW SLAVE STATUS的多个字段交叉验证大事务是最大的元凶:拆大事务比调任何参数都管用
并行复制是解决从库延迟的根本手段:但不是开了就完事,需要根据版本选择合适的模式
下次业务方在群里@你的时候,希望你不是回复“看起来没问题”,而是已经有了清晰的排查思路。
小耶在手,SQL 不愁
还有什么想了解的,欢迎留言!小耶一定知无不言言无不尽……我们下次见~