备库延迟
一、背景:备库延迟的根源
-
偶发性延迟 vs 持续性延迟:
-
偶发性查询压力、备份等导致的延迟通常是分钟级,备库能追上。
-
若备库执行日志速度持续低于主库生成速度,延迟可能达小时级,甚至永远追不上。
-
-
核心问题: 主库高并发写入 vs 备库单线程应用日志。
-
主库: InnoDB行锁支持高并发(除非极端热点行)。
-
备库 (5.6前): sql_thread 单线程应用中转日志 (relay log),成为瓶颈。
-
二、并行复制的基本模型
-
目标: 将单线程的 sql_thread 拆分为多线程。
-
通用架构:
-
Coordinator (协调器): 原 sql_thread 角色转变,负责读取中转日志、分发事务给 Worker。
-
Worker (工作线程): 真正执行事务,更新数据。
-
slave_parallel_workers 参数: 控制 Worker 线程数量 (经验值8-16,视CPU核数定)。
-
三、并行复制的核心原则 (保证数据一致性)
-
不能造成更新覆盖: 更新同一行的两个事务,必须被分发到同一个 Worker 中(并按原序执行)。
- 原因:若分发到不同 Worker,CPU 调度可能导致执行顺序与主库相反,引发数据不一致。
-
同一个事务不能被拆开: 一个事务内的所有更新语句,必须由同一个 Worker 执行。
- 原因:若拆分,可能导致查询看到事务“更新了一半”的结果,破坏事务隔离性。
四、MySQL并行复制策略的演进
-
MySQL 5.5 时代 (作者个人实现)
-
1.1 按表分发 (Table-based Parallelism):
-
思路: 不同表更新的事务可以并行。
-
机制:
-
每个 Worker 维护一个哈希表,Key为“库名.表名”,Value为涉及该表的事务数。
-
事务分发:
-
与所有 Worker 均不冲突:分配给最空闲的 Worker。
-
与多个 Worker 冲突:Coordinator 等待,直到冲突 Worker 减至1个。
-
与单个 Worker 冲突:分配给该 Worker。
-
-
-
优点: 对多表负载均匀场景效果好。
-
缺点: 遇热点表(所有更新都涉及时)则退化为单线程。
-
-
1.2 按行分发 (Row-based Parallelism):
-
思路: 不同行更新的事务可以并行。
-
前提: binlog_format=ROW。
-
机制:
-
Worker 的哈希表 Key 为“库名+表名+唯一键名+唯一键值”。
-
关键: 必须考虑所有唯一键(包括主键),而不只是主键。否则,若事务顺序颠倒,可能因唯一键约束导致错误(如先执行了后一个事务,导致前一个事务插入/更新时唯一键冲突)。
-
例:UPDATE t1 SET a=1 WHERE id=2 (a为唯一键),事务哈希表项包括:主键id=2的项,旧唯一键a=2的项,新唯一键a=1的项。
-
-
约束:
-
Binlog 格式为 ROW。
-
表必须有主键。
-
不能有外键 (外键级联更新不记录在binlog,冲突检测不准)。
-
-
优点: 并行度更高。
-
缺点 (针对大事务):
-
耗内存:如删除100万行,哈希表需记录100万项。
-
耗CPU:解析binlog、计算哈希值成本高。
-
优化: 设置行数阈值,超阈值的大事务临时退化为单线程(Coordinator暂停分发,等所有Worker空闲后,Coordinator亲自执行大事务,再恢复并行)。
-
-
-
-
MySQL 5.6 官方并行复制 (按库并行 - Database-level Parallelism):
-
思路: 不同数据库 (DB) 的事务可以并行。
-
机制: 哈希表 Key 为数据库名。
-
优点:
-
构造哈希值快,内存占用少。
-
不要求 binlog_format=ROW,STATEMENT 格式亦可。
-
-
缺点:
-
若所有表在同一DB,或各DB热点不均,则效果不佳。
-
实践中较少为利用此策略而特意迁移数据。
-
-
-
MariaDB 的并行复制策略 (基于组提交 - Group Commit based):
-
思路: 模拟主库并行模式。主库上能在同一组提交(group commit)的事务,一定不修改同一行,因此在备库上可以并行。
-
机制:
-
主库同一组提交的事务有相同的 commit_id。
-
commit_id 写入 binlog。
-
备库 Coordinator 将相同 commit_id 的事务分发给多个 Worker。
-
当前组所有事务执行完毕后,才取下一批。
-
-
优点: 实现优雅,对原系统改动小。
-
缺点:
-
吞吐量瓶颈: 未真正模拟主库并发。主库上一组提交时,下一组已在执行;备库需等当前组完全执行完。
-
大事务拖累: 组内若有大事务,其他 Worker 完成后需等待该大事务,造成资源浪费。
-
-
-
MySQL 5.7 的并行复制策略 (LOGICAL_CLOCK):
-
参数: slave-parallel-type
-
DATABASE:即 MySQL 5.6 的按库并行。
-
LOGICAL_CLOCK:类似 MariaDB 策略并优化。
-
-
优化思路:
-
事务进入 redo log prepare 阶段即表示已通过锁冲突检验。
-
并行条件:
-
同时处于 prepare 状态的事务可并行。
-
处于 prepare 状态的事务与处于 commit 状态的事务可并行。
-
-
-
利用组提交参数提升并行度:
-
binlog_group_commit_sync_delay (延迟fsync)
-
binlog_group_commit_sync_no_delay_count (累积N次后fsync)
-
这两个参数可“故意”拉长 binlog 从 write 到 fsync 的时间,使更多事务同时处于 prepare 阶段,从而增加备库并行度。它们既让主库提交稍慢(减少fsync次数),又让备库复制更快。
-
-
-
MySQL 5.7.22 的并行复制策略 (WRITESET - 基于写集):
-
参数: binlog-transaction-dependency-tracking 控制策略。
-
COMMIT_ORDER:即上述 MySQL 5.7 的 LOGICAL_CLOCK 策略。
-
WRITESET:
-
对事务涉及更新的每一行,计算其哈希值 (基于“库名+表名+索引名+值”),构成该事务的写集 (writeset)。
-
若两事务的 writeset 无交集,则可并行。
-
关键: Writeset 在主库生成后直接写入 binlog。
-
-
WRITESET_SESSION:在 WRITESET 基础上增加约束:主库上同一线程(session)先后执行的两个事务,在备库执行时也保证相同顺序。
-
-
优点 (相较于5.5按行分发):
-
备库性能高: Writeset 已在主库生成并写入binlog,备库无需解析行数据计算,节省CPU。
-
内存效率高: 无需扫描整个事务binlog来决定分发。
-
通用性强: 因分发不依赖binlog内容解析,STATEMENT 格式 binlog 也支持。
-
-
限制: 表上无主键或存在外键约束时,无法并行,退化为单线程。
-
五、核心小结与实践建议
-
多线程复制的必要性: 解决单线程复制瓶颈,避免高压主库下备库延迟持续增大。
-
策略选择: DBA需根据业务场景、MySQL版本选择合适的并行复制策略。
-
避免大事务: 大事务是主库性能瓶颈,也是备库复制延迟的重要原因。应尽量拆分大事务。
-
版本兼容性: MySQL 5.7起,新的并行策略可能修改binlog内容格式,主备切换、版本升级时需考虑binlog协议的兼容性。
-
slave_parallel_workers 设置: 建议 8-16 (32核物理机),避免耗尽备库CPU,兼顾可能的读请求。
六、延伸思考:文末问题解答
问题: 一个MySQL 5.7.22版本的主库,单线程插入了很多数据,过了3小时后,搭建相同版本的备库。为了更快追上主库,开启并行复制。在 binlog-transaction-dependency-tracking 参数的 COMMIT_ORDER、WRITESET 和 WRITESET_SESSION 三个取值中,会选择哪一个?原因?其他两个参数的表现?
-
场景分析:
-
主库执行的是单线程的大量插入操作。
-
目标是备库快速追赶。
-
MySQL版本为 5.7.22。
-
-
策略选择与原因:
-
首选:WRITESET
-
原因:
-
高并行潜力: 即使主库是单线程插入,但每次插入的是新行,这些新行之间通常不冲突 (除非有特殊触发器或非常密集的唯一键生成逻辑,但一般批量插入不同行)。WRITESET 策略能识别出这些事务的写集不重叠,从而允许它们在备库并行执行。
-
效率: Writeset 信息已在主库生成并记录在 binlog 中,备库应用时开销小。
-
-
-
次选:COMMIT_ORDER
- 原因: 主库单线程执行的大量插入操作,很可能会因为组提交(binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count 参数的作用)而被划分到同一个或少数几个 "commit group" 中。如果这些事务被识别为可以一起进入 prepare 状态,那么备库也能实现一定程度的并行。其并行度可能略低于 WRITESET,但仍然比单线程快很多。
-
-
其他策略的表现:
-
WRITESET_SESSION:
-
表现: 效果最差,可能接近单线程。
-
原因: 此策略要求主库上同一会话(线程)的事务在备库上按序执行。由于主库是“单线程插入了很多数据”,这些事务都源于同一个会话。因此,在备库上它们会被强制串行执行,无法发挥并行复制的优势来快速追赶。
-
-
COMMIT_ORDER (对比 WRITESET)
-
表现: 通常良好,但并行度可能不如 WRITESET 极致。
-
原因: COMMIT_ORDER 依赖于事务能否同时进入 prepare 或 commit 阶段。虽然单线程的批量插入也可能因组提交而批次化,但 WRITESET 直接分析数据依赖,只要行不冲突就能并行,不依赖于事务在主库的提交时机是否“恰好”凑在一起。
-
-
-
总结:
-
要快速追赶,应选择能最大化并行度的策略。
-
WRITESET 基于实际数据写入的行级冲突检测,对于主库单线程写入不冲突数据的情况,能提供最佳并行度。
-
COMMIT_ORDER 也能提供较好的并行度,但可能受主库组提交行为影响。
-
WRITESET_SESSION 在此场景下会强制串行,无法实现快速追赶。
-
七、上期问题回顾:备库延迟呈45度线段
-
原因: 备库同步在该时间段内完全被堵住。
-
典型场景:
-
大事务: 包括大表DDL(如ALTER TABLE)、一个事务操作极多行。
-
备库长事务阻塞DDL: 备库上有一个长时间未提交的事务(如 BEGIN; SELECT ...; 然后无操作),此时主库执行了一个DDL(即使是很小的表),该DDL在备库应用时会被此长事务阻塞。
-