Skip to content

备库延迟

一、背景:备库延迟的根源

  1. 偶发性延迟 vs 持续性延迟:

    • 偶发性查询压力、备份等导致的延迟通常是分钟级,备库能追上。

    • 若备库执行日志速度持续低于主库生成速度,延迟可能达小时级,甚至永远追不上。

  2. 核心问题: 主库高并发写入 vs 备库单线程应用日志。

    • 主库: InnoDB行锁支持高并发(除非极端热点行)。

    • 备库 (5.6前): sql_thread 单线程应用中转日志 (relay log),成为瓶颈。

二、并行复制的基本模型

  1. 目标: 将单线程的 sql_thread 拆分为多线程。

  2. 通用架构:

    • Coordinator (协调器): 原 sql_thread 角色转变,负责读取中转日志、分发事务给 Worker。

    • Worker (工作线程): 真正执行事务,更新数据。

    • slave_parallel_workers 参数: 控制 Worker 线程数量 (经验值8-16,视CPU核数定)。

三、并行复制的核心原则 (保证数据一致性)

  1. 不能造成更新覆盖: 更新同一行的两个事务,必须被分发到同一个 Worker 中(并按原序执行)。

    • 原因:若分发到不同 Worker,CPU 调度可能导致执行顺序与主库相反,引发数据不一致。
  2. 同一个事务不能被拆开: 一个事务内的所有更新语句,必须由同一个 Worker 执行。

    • 原因:若拆分,可能导致查询看到事务“更新了一半”的结果,破坏事务隔离性。

四、MySQL并行复制策略的演进

  1. 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亲自执行大事务,再恢复并行)。

  2. MySQL 5.6 官方并行复制 (按库并行 - Database-level Parallelism):

    • 思路: 不同数据库 (DB) 的事务可以并行。

    • 机制: 哈希表 Key 为数据库名。

    • 优点:

      • 构造哈希值快,内存占用少。

      • 不要求 binlog_format=ROW,STATEMENT 格式亦可。

    • 缺点:

      • 若所有表在同一DB,或各DB热点不均,则效果不佳。

      • 实践中较少为利用此策略而特意迁移数据。

  3. MariaDB 的并行复制策略 (基于组提交 - Group Commit based):

    • 思路: 模拟主库并行模式。主库上能在同一组提交(group commit)的事务,一定不修改同一行,因此在备库上可以并行。

    • 机制:

      • 主库同一组提交的事务有相同的 commit_id。

      • commit_id 写入 binlog。

      • 备库 Coordinator 将相同 commit_id 的事务分发给多个 Worker。

      • 当前组所有事务执行完毕后,才取下一批。

    • 优点: 实现优雅,对原系统改动小。

    • 缺点:

      • 吞吐量瓶颈: 未真正模拟主库并发。主库上一组提交时,下一组已在执行;备库需等当前组完全执行完。

      • 大事务拖累: 组内若有大事务,其他 Worker 完成后需等待该大事务,造成资源浪费。

  4. 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次数),又让备库复制更快。

  5. 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 也支持。

    • 限制: 表上无主键或存在外键约束时,无法并行,退化为单线程。

五、核心小结与实践建议

  1. 多线程复制的必要性: 解决单线程复制瓶颈,避免高压主库下备库延迟持续增大。

  2. 策略选择: DBA需根据业务场景、MySQL版本选择合适的并行复制策略。

  3. 避免大事务: 大事务是主库性能瓶颈,也是备库复制延迟的重要原因。应尽量拆分大事务。

  4. 版本兼容性: MySQL 5.7起,新的并行策略可能修改binlog内容格式,主备切换、版本升级时需考虑binlog协议的兼容性。

  5. 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

      • 原因:

        1. 高并行潜力: 即使主库是单线程插入,但每次插入的是新行,这些新行之间通常不冲突 (除非有特殊触发器或非常密集的唯一键生成逻辑,但一般批量插入不同行)。WRITESET 策略能识别出这些事务的写集不重叠,从而允许它们在备库并行执行。

        2. 效率: 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度线段

  • 原因: 备库同步在该时间段内完全被堵住

  • 典型场景:

    1. 大事务: 包括大表DDL(如ALTER TABLE)、一个事务操作极多行。

    2. 备库长事务阻塞DDL: 备库上有一个长时间未提交的事务(如 BEGIN; SELECT ...; 然后无操作),此时主库执行了一个DDL(即使是很小的表),该DDL在备库应用时会被此长事务阻塞。