MySQL并行复制原理与调优实战:LOGICAL_CLOCK到WRITESET_SESSION全链路优化
📌今日关键词:MySQL并行复制、主从延迟、LOGICAL_CLOCK、WRITESET、MTS调优
大家好,我是数据库小学妹 👋
之前我们聊过主从同步失败的五大诱因,有粉丝朋友问我:“小学妹,我的从库倒没挂,但 Seconds_Behind_Master 一直飙到几千秒,延迟越来越大怎么办?”
这个问题我太有发言权了。虽然我们讲过主从延迟的5个常见原因和3个排查命令,但那只是入门级别。真正要解决"从库追不上"的问题,得搞懂并行复制这个核心机制。
我一开始以为并行复制就是改个参数的事,设个 slave_parallel_workers 就完事了。结果改完延迟一点没降,还差点搞出数据不一致。后来才明白,MySQL 的并行复制经历了四代演进,每一代的设计思路完全不同,参数之间还有依赖关系。不搞清楚原理就上手调,跟闭眼开车差不多。
今天从单线程为什么慢讲起,把四代演进拆透,再配合排查路径和调优方案,争取看完就能上手。
一、为什么从库会"追不上"主库?
先回忆一下主从复制的基本流程:
主库写入 → binlog → 从库IO线程拉取 → relay log → 从库SQL线程重放 → 数据写入延迟就卡在最后一步:SQL线程重放。
主库能同时处理几百上千个并发连接,binlog里记录的是所有连接混合在一起的事务。但从库呢?MySQL 5.6之前,只有一个SQL线程在那一条一条地重放。
打个比方:主库是10条流水线同时干活,从库只有1个人在那一件一件地做。主库写得越快,从库就落得越远。
说白了,就是干活的人太少。网络没问题,磁盘也没问题,瓶颈出在重放线程上。
二、并行复制的四代演进
MySQL团队为了解决这个瓶颈,前前后后搞了四代方案。每一代都是在上一代的基础上改的。
第一代:基于Schema的并行(MySQL 5.6)
5.6引入了最早的并行方案:如果两个事务操作的是不同数据库(schema),它们之间没有数据冲突,可以并行执行。
# my.cnf slave_parallel_workers = 4 slave_parallel_type = DATABASE # 基于数据库并行局限性很大。现在的业务谁不是一个库搞到底?单库多表的场景下,这个方案基本等于没开。十个表都在同一个库里,全部串行。
我当时的项目就是这种情况,一个业务库里40多张表,开了这个参数之后延迟纹丝不动。
第二代:LOGICAL_CLOCK(MySQL 5.7.2+)
这是真正意义上的突破。思路变了:不再看"是不是同一个库",而是看"在主库是不是一起提交的"。
核心机制叫 Group Commit(组提交)。
什么是Group Commit?
MySQL为了减少fsync次数,会把多个事务的binlog攒在一起写盘。比如同一时刻有事务A、B、C都在commit,MySQL会把它们放在一个组里,只做一次fsync。
时间线: t1: 事务A commit → 进入binlog cache t2: 事务B commit → 进入binlog cache t3: 事务C commit → 进入binlog cache t4: 三个事务一起fsync到binlog文件(一次IO搞定)组提交的关键:同一个组里的事务,在主库就是"同时"提交的,它们之间大概率不冲突。
5.7在binlog里记录了每个事务的"逻辑时钟"(其实就是组提交的序列号)。从库看到两个事务的逻辑时钟相同,就知道它们在主库是同一组提交的,可以并行重放。
# my.cnf slave_parallel_type = LOGICAL_CLOCK slave_parallel_workers = 8 slave_preserve_commit_order = ON比5.6好在哪?不看数据库了,看主库实际的提交关系。单库多表也能并行。
但也容易踩坑:slave_preserve_commit_order 这个参数一定要开。不开的话,从库事务的提交顺序可能跟主库不一致,某些依赖提交顺序的业务逻辑会出问题。开了会损失一些并行度,但数据安全更重要。
第三代:WRITESET(MySQL 5.7.22+)
LOGICAL_CLOCK的问题在哪?粒度还是太粗。同一个组里的事务数量有限,大部分时间从库的worker还是在等。
WRITESET换了思路:不看"主库什么时候提交",而是看"两个事务操作的数据有没有重叠"。
事务A写了行 {id=1, id=2} 事务B写了行 {id=3, id=4} → A和B的写集合(WRITESET)不相交 → 可以并行!即使事务A和事务B在主库不是同一个组提交的,只要它们操作的数据不重叠,从库也可以并行执行。
# my.cnf slave_parallel_type = LOGICAL_CLOCK slave_parallel_workers = 8 binlog_transaction_dependency_tracking = WRITESET transaction_write_set_extraction = XXHASH64 slave_preserve_commit_order = ON并行度比LOGICAL_CLOCK高很多,但也带来了新问题,我们后面展开讲。
第四代:WRITESET_SESSION(MySQL 8.0.27+)
WRITESET的隐患:同一session里的两个事务,如果操作不同行,WRITESET会认为它们不冲突、可以并行。但实际上同一个session的事务是有先后顺序的,乱序可能出问题。
8.0.27加了WRITESET_SESSION:在WRITESET的基础上,保证同一session的事务串行执行,不同session的事务仍然可以按写集合判断并行。
# my.cnf 推荐配置(8.0.27+) binlog_transaction_dependency_tracking = WRITESET_SESSION transaction_write_set_extraction = XXHASH64 slave_parallel_type = LOGICAL_CLOCK slave_parallel_workers = 8 slave_preserve_commit_order = ON8.0.27+的环境,直接用这个就对了。
三、版本选型对照表
| MySQL版本 | 推荐方案 | 关键参数 |
|---|---|---|
| 5.6 | Schema并行(聊胜于无) | slave_parallel_type=DATABASE |
| 5.7.2 ~ 5.7.21 | LOGICAL_CLOCK | slave_parallel_type=LOGICAL_CLOCK |
| 5.7.22 ~ 8.0.26 | WRITESET | binlog_transaction_dependency_tracking=WRITESET |
| 8.0.27+ | WRITESET_SESSION | binlog_transaction_dependency_tracking=WRITESET_SESSION |
不管哪个版本,slave_preserve_commit_order 都建议开。
四、并行度怎么设?不是越大越好
slave_parallel_workers 设多少合适?我最初的想法是"越多越好",直接设了32。结果从库CPU飙到100%,锁争用严重,延迟反而更大了。
经验值:
| 机器配置 | 建议worker数 |
|---|---|
| 4核8G | 4~8 |
| 8核16G | 8~16 |
| 16核32G | 16~32 |
调优步骤:
- 先设8,观察一周
- 看从库CPU使用率和延迟趋势
- CPU低于60%且延迟还在涨 → 加worker
- CPU高于80%或锁争用明显 → 减worker
-- 查看worker状态SELECTworker_id,thread_id,service_state,last_error_number,last_error_messageFROMperformance_schema.replication_applier_status_by_worker;五、延迟排查路径:四种现象四种打法
并行复制配好了,延迟还是高?下面四种场景,我一个个讲。
场景一:延迟持续增长,越来越大
十个延迟问题里有八个是这种。排查路径:
-- 第一步:看从库在干什么SHOWPROCESSLIST;-- 第二步:看有没有大事务阻塞SELECT*FROMinformation_schema.innodb_trxWHEREtrx_started<NOW()-INTERVAL30SECONDORDERBYtrx_started;-- 第三步:看从库锁等待SELECT*FROMperformance_schema.data_lock_waits;常见原因:
- 主库有大事务(DELETE几百万行、ALTER大表)
- 从库IO慢,relay log写入跟不上
- 并行复制没开或者配错了
解法就是大事务拆小批量执行,IO慢就换SSD,并行复制参数按上面配。
场景二:延迟抖动,一阵一阵的
延迟不是持续增长,而是每隔几分钟飙一次,然后又慢慢降下来。
排查方向:
- 定时任务(每5分钟的批量写入)
- 监控/备份脚本(mysqldump、xtrabackup跑的时候IO飙升)
- 主库的checkpoint
这种场景通常是业务层的写入不均匀导致的。
场景三:Seconds_Behind_Master=0,但业务读到老数据
这个最坑。监控看着一切正常,延迟为0,但业务就是查不到新数据。
-- 用performance_schema看真实延迟SELECT*FROMperformance_schema.replication_applier_status;-- 或者用心跳表对比-- 主库写入当前时间UPDATEheartbeatSETts=NOW(6)WHEREid=1;-- 从库读取对比SELECTTIMESTAMPDIFF(SECOND,ts,NOW(6))ASreal_lagFROMheartbeatWHEREid=1;Seconds_Behind_Master是基于binlog里TIMESTAMP字段计算的,当某些事务不包含时间戳信息时,这个指标会显示0,但实际延迟可能存在。
解法就是上心跳表做业务级监控,比Seconds_Behind_Master靠谱得多。
场景四:延迟突然归零又涨上去
通常是某个大事务执行完了,SQL线程瞬间追上,然后下一个大事务又开始堆积。
看relay log空间能辅助判断:
SHOWSLAVESTATUS\G-- 看 Relay_Log_Space 字段-- 如果一直在涨,说明IO线程拉的比SQL线程放的快六、业务层调优:光调数据库参数不够
并行复制能解决SQL线程串行的瓶颈,但有些问题光靠参数搞不定。
拆大事务
大事务是延迟的头号杀手。
-- 错误写法:一次删100万行DELETEFROMlogsWHEREcreated_at<'2024-01-01';-- 正确写法:分批删除DELETEFROMlogsWHEREcreated_at<'2024-01-01'LIMIT10000;-- 应用层循环执行,每批之间sleep(1)我之前接手的一个项目,开发同学写了个定时清理任务,每个月删3000万行日志,一个DELETE搞定。跑了两个月我才发现从库延迟经常飙到1小时。改成每批5000行、间隔0.5秒之后,延迟从没超过10秒。
大表DDL用工具
-- 错误写法:直接ALTERALTERTABLEbig_tableADDCOLUMNxINT;-- 正确写法:用gh-ost或pt-online-schema-change-- gh-ost不会长时间锁表,对复制友好读写分离要注意"读后写"
开了读写分离之后,如果有业务逻辑是"先SELECT再UPDATE",SELECT走从库可能读到老数据,UPDATE走主库写了新数据,中间就出问题了。
要么在代码里强制这类查询走主库,要么用中间件(ProxySQL)配置规则把这类SQL路由到主库。
七、监控方案:光看一个指标不够
复制状态监控
-- 最基础:SHOW SLAVE STATUSSHOWSLAVESTATUS\G-- 关注:Slave_IO_Running, Slave_SQL_Running, Seconds_Behind_Master-- 更准确:performance_schemaSELECT*FROMperformance_schema.replication_connection_status\GSELECT*FROMperformance_schema.replication_applier_status\G业务延迟监控
心跳表方案:
-- 在monitor库建心跳表CREATETABLEheartbeat(idINTPRIMARYKEY,tsTIMESTAMP(6)DEFAULTCURRENT_TIMESTAMP(6)ONUPDATECURRENT_TIMESTAMP(6));INSERTINTOheartbeat(id)VALUES(1);后台任务每秒在主库UPDATE心跳表的ts字段,然后对比主从ts差值。这个差值就是真实延迟。比Seconds_Behind_Master准确,不依赖binlog里的时间戳。
八、升级5.7到8.0的并行复制变化
如果你的环境在做5.7→8.0升级,并行复制方面有几个点要注意:
| 变化 | 说明 |
|---|---|
| WRITESET_SESSION | 8.0.27+新增,比WRITESET更安全,推荐开启 |
| 命令语法更新 | SHOW SLAVE STATUS → SHOW REPLICA STATUS(8.0.22+兼容老语法) |
| redo log格式变化 | 8.0 redo log格式有调整,升级后需重新调innodb_redo_log_capacity |
| 认证插件变更 | caching_sha2_password是8.0默认,从库连主库要确认密码插件兼容 |
升级后的推荐配置:
# 8.0.27+ 推荐 binlog_transaction_dependency_tracking = WRITESET_SESSION transaction_write_set_extraction = XXHASH64 slave_parallel_type = LOGICAL_CLOCK slave_parallel_workers = 8 slave_preserve_commit_order = ON innodb_redo_log_capacity = 4294967296 # 4GB,单位字节九、面试怎么答
面试官:MySQL并行复制经历了哪几个阶段?各自原理是什么?
并行复制经历了四个阶段。5.6是基于Schema的并行,不同数据库的事务可以同时执行,但单库场景没用。5.7.2引入LOGICAL_CLOCK,利用主库的Group Commit机制,同组提交的事务可以在从库并行重放。5.7.22进一步引入WRITESET,通过分析事务的写集合判断是否冲突,不冲突就并行,粒度更细。8.0.27加了WRITESET_SESSION,在WRITESET基础上保证同一session事务串行,避免乱序问题。生产里建议8.0.27+直接用WRITESET_SESSION。
面试官:slave_preserve_commit_order为什么建议开启?
并行重放后,从库事务的提交顺序可能跟主库不一致。如果业务逻辑依赖提交顺序(比如先插入后更新的时序),不开启可能导致数据不一致。开启后会损失一部分并行度,但换来的是数据安全性。生产环境建议始终开启。
面试官:Seconds_Behind_Master为0但业务读到老数据,怎么排查?
Seconds_Behind_Master基于binlog中的TIMESTAMP字段计算,当事务不包含时间戳时会显示0。先用performance_schema的replication_applier_status查真实状态,再上心跳表做业务级监控——主库每秒写入当前时间到心跳表,从库读取对比差值,这个差值就是真实延迟。
避坑清单
| 序号 | 坑点 | 后果 | 正确做法 |
|---|---|---|---|
| 1 | 不开并行复制就指望延迟自己降 | 延迟随写入量线性增长 | 5.7+至少用LOGICAL_CLOCK |
| 2 | slave_parallel_workers设太大 | CPU飙高、锁争用严重 | 从8开始,观察后再调 |
| 3 | 不开slave_preserve_commit_order | 从库提交顺序错乱,数据不一致 | 生产环境必须ON |
| 4 | WRITESET模式下同一session事务冲突 | 从库数据错乱或报错 | 8.0.27+用WRITESET_SESSION |
| 5 | 只看Seconds_Behind_Master | 指标失真以为没延迟 | 上心跳表+performance_schema |
| 6 | 主库跑大事务不拆分 | 从库阻塞几十分钟 | 分批执行,每批LIMIT控制行数 |
| 7 | 大表直接ALTER不走工具 | 复制延迟飙升、锁表 | 用gh-ost或pt-osc |
| 8 | 读写分离不考虑"读后写" | 业务读到旧数据做错误写入 | 关键查询路由到主库 |
| 9 | 升级8.0后不调redo log参数 | redo log格式变化导致IO异常 | 升级后调innodb_redo_log_capacity |
| 10 | 以为调参数就能解决所有延迟 | 忽略业务层大事务、定时任务等根因 | 参数+业务双管齐下 |
总结
并行复制不是"改个参数就完事"的事。四代演进下来,每一代解决了不同层面的问题:
- 5.6 Schema并行 → 解决"完全没有并行"
- 5.7 LOGICAL_CLOCK → 利用组提交实现真正的并行
- 5.7.22 WRITESET → 通过写集合分析,粒度更细
- 8.0.27 WRITESET_SESSION → 兼顾并行度和session顺序
但并行复制只解决SQL线程的瓶颈。真正要做到延迟可控,还得在业务层把大事务拆掉、定时任务分批跑、读写分离路由做对。
并行复制解决"干活的人少"的问题,业务优化解决"活太大"的问题。两个都搞定,从库追不上主库的问题基本就治好了。
我是数据库小学妹,咱们下篇见 👋
