1. MySQL并行复制性能异常案例分析:为什么并行复制比单线程更慢?
最近在排查一个MySQL从库复制延迟的案例时,发现了一个非常反直觉的现象:在特定场景下,启用并行复制后,从库的重放性能反而比单线程复制更差。这个案例涉及MySQL 8.0.40版本,事务隔离级别为Read Committed(RC),并行重放线程数设置为8。
1.1 问题现象描述
最初发现从库的复制延迟持续增加,且没有下降趋势。通过show slave status\G查看,发现Relay_Master_Log_File和Exec_Master_Log_Pos都在变化,只是变化速度明显慢于主库。
使用mysql-binlog-time-extractor工具分析主库binlog后发现一个矛盾现象:在主库写入量较大的时间段(04:57-05:02)并未出现延迟,反而在写入量较小的时段(09:30左右)出现了明显的延迟增长。
1.2 初步分析过程
检查从库错误日志时,发现了大量锁等待超时的报错。这在RC隔离级别下显得尤为奇怪,因为RC通常只加记录锁,不会出现间隙锁。同时,该实例启用了WRITESET并行复制,理论上事务间如果没有修改相同的主键或唯一索引,应该可以并行执行。
使用binlog_summary.py工具对延迟时段的binlog文件分析后发现:
- 操作频率最高的是一张名为
biz_schema.tbl_product_service_mapping01的表 - 该表的DELETE和INSERT操作占据了绝大多数事务
进一步分析发现,业务逻辑是通过先DELETE再INSERT的方式实现数据更新,且INSERT操作总是出现在对应DELETE操作之后。
2. 问题复现与深入排查
2.1 实验环境搭建
为了准确复现问题,我搭建了测试环境:
- 使用相同版本的MySQL(8.0.40)
- 设置相同的事务隔离级别(RC)
- 配置相同的并行复制参数(replica_parallel_workers=8)
通过以下步骤模拟从库重放:
sql复制-- 初始化relay log
CHANGE MASTER TO MASTER_HOST='dummy';
STOP SLAVE;
RESET SLAVE ALL;
-- 替换relay log文件
cp binary-log.005636 /data/mysql/3306/data/instance-20250903-0701-relay-bin.000001
chown mysql.mysql /data/mysql/3306/data/instance-20250903-0701-relay-bin.000001
-- 启动SQL线程重放
CHANGE MASTER TO RELAY_LOG_FILE='instance-20250903-0701-relay-bin.000001',
RELAY_LOG_POS=1, MASTER_HOST='dummy';
START SLAVE SQL_THREAD;
2.2 性能对比测试
进行了三组对比测试:
| 测试场景 | 平均重放时间 | 错误日志表现 |
|---|---|---|
| 并行复制(replica_parallel_workers=8) | 359.06秒 | 大量锁等待超时报错 |
| 单线程复制(replica_parallel_workers=1) | 83.07秒 | 无锁等待报错 |
| 并行复制+关闭提交顺序(replica_preserve_commit_order=OFF) | 21.11秒 | 少量锁等待报错 |
这个结果非常令人惊讶:并行复制反而比单线程慢了4倍多!
3. 锁等待问题的根本原因分析
3.1 锁等待环路的形成
通过sys.innodb_lock_waits和performance_schema.data_locks视图,我们观察到了一个典型的锁等待环路:
- 线程10(插入c1=10512475)被线程14(插入c1=10512476)阻塞
- 线程14又被线程12(插入c1=10512477)阻塞
- 线程12由于
replica_preserve_commit_order=ON的设置,必须等待线程10提交
这种循环等待导致所有相关线程都被阻塞,直到锁等待超时。
3.2 RC级别下为何会出现GAP锁
在Read Committed隔离级别下,通常不会出现间隙锁。但在这个案例中,由于业务采用了DELETE+INSERT的更新方式,导致了特殊的加锁行为:
- DELETE操作不会立即物理删除记录,而是标记为"delete-marked"
- 当INSERT相同唯一键值时,MySQL会:
- 对delete-marked记录加S锁
- 对记录间隙加S,GAP锁
- 这种机制是为了确保唯一性约束,防止并发事务插入相同键值
通过以下实验可以验证这一行为:
sql复制-- 会话1:创建测试表
CREATE TABLE test.t1(
id BIGINT AUTO_INCREMENT PRIMARY KEY,
c1 INT,
c2 INT,
UNIQUE KEY(c1,c2)
);
-- 会话2:暂停purge线程
FLUSH TABLES test.t2 FOR EXPORT;
-- 会话1:插入并删除数据
INSERT INTO test.t1(c1,c2) VALUES(10512476,1),(10512476,2);
DELETE FROM test.t1;
-- 会话3:尝试插入
BEGIN;
INSERT INTO test.t1(c1,c2,id) VALUES(10512476,1,18158557178);
-- 查看锁信息
SELECT object_schema, object_name, index_name,
lock_type, lock_mode, lock_data
FROM performance_schema.data_locks;
4. 解决方案与优化建议
4.1 应用层优化方案
-
索引设计优化:
- 将唯一索引改为普通二级索引
- 考虑使用原唯一索引列作为主键(主键插入不触发S,GAP锁)
-
更新逻辑优化:
- 避免使用DELETE+INSERT方式更新数据
- 改用UPDATE语句直接修改记录
- 如果必须删除后插入,考虑使用临时表过渡
4.2 数据库层优化方案
-
调整并行复制参数:
sql复制SET GLOBAL replica_parallel_workers=4; -- 适当减少并行线程数 SET GLOBAL replica_preserve_commit_order=OFF; -- 关闭严格提交顺序注意:Group Replication要求必须保持
replica_preserve_commit_order=ON -
监控与告警:
sql复制-- 监控锁等待 SELECT * FROM sys.innodb_lock_waits; -- 监控复制延迟 SHOW SLAVE STATUS\G -
版本升级考虑:
MySQL 8.0后续版本对并行复制有持续优化,考虑升级到最新稳定版
5. 经验总结与最佳实践
通过这个案例,我总结了以下几点重要经验:
-
DELETE+INSERT模式的风险:
- 这种更新方式在唯一索引下容易引发锁问题
- 在RC级别下也可能产生GAP锁
- 建议评估改用UPDATE的可能性
-
并行复制的适用场景:
- 并非所有场景都适合开启并行复制
- 写冲突较多时可能适得其反
- 建议先进行充分的测试验证
-
监控策略建议:
- 除了监控复制延迟,还应关注锁等待情况
- 定期分析binlog模式,识别潜在问题模式
- 对关键表操作建立专门的监控指标
-
测试验证方法:
- 使用relay log替换法可以精准复现生产问题
- 性能对比测试应该包含多种场景
- 压力测试要模拟真实业务模式
这个案例再次证明,数据库性能优化不能仅凭理论或默认配置,必须结合具体业务场景进行针对性调优。看似合理的配置(如启用并行复制),在特定业务模式下可能会产生完全相反的效果。