并行复制

简介

在主从复制过程中,从库的IO线程用于接收binlog并写入relay log,从库SQL线程用于回放relay log把更新写入本地磁盘。默认情况下,每个从库只有一个SQL线程,客户端在高并发情况下,若主库产生binlog的速度超过从库消费binlog的速度,会导致主库和从库之间存在数据差距。并行复制通过增加从库SQL线程的并行度来降低主库和从库之间的数据差距。

业务流程

MySQL 5.7引入基于Logic clock的并行复制方案。Logic clock并行复制有Commit-Parent-Based和Lock-Based两种模式。而在MySQL 5.7.21版本之后,增加了WriteSet复制。

在介绍这两种模式之前,先介绍一下MySQL事务提交方式。

  1. MySQL事务提交。

    MySQL事务提交主要分为两个阶段,准备阶段和提交阶段。

    准备阶段:SQL执行成功以后,生成xid信息、redo和undo的内存日志,调用prepare方法将事务状态设为TRX_PREPARED,并将redo log写入磁盘。

    提交阶段:事务涉及的所有存储引擎的prepare都已执行成功,调用TC_LOG_BINLOG::log_xid方法将SQL语句写到binlog日志,然后调用存储引擎的commit完成事务的提交,将事务改为TRX_NOT_STARTED状态。

  2. MySQL组提交。

    MySQL 5.6版本引入了组提交(Group Commit),并将提交过程分为Flush stage、Sync stage和Commit stage三个阶段。

    • Flush stage(缓存阶段):将所有已注册线程写入binlog缓存。
    • Sync stage(同步阶段):将binlog缓存同步到磁盘。
    • Commit stage(提交阶段):leader线程根据顺序调用存储引擎提交事务。

    每个阶段都有各自的队列,当一个线程注册到一个空队列,该线程就作为该队列的leader,后注册到该队列的线程均为follower,leader控制队列中follower执行操作。leader线程带领当前队列的所有follower线程到下一个阶段去执行。

    在commit stage,实际做的是存储引擎提交,参数binlog_order_commits会影响提交行为。如果设置On,存储引擎的提交变为串行操作,以队列的顺序为提交顺序。如果设置成OFF,组内的每个事务会在leader线程完成commit之后,各自进行存储引擎层的提交操作。

    组提交(Group Commit)是logic clock并行复制的基础。Group Commit将所有的事务进行了分组,并为每个事务分配了last_committed和sequence_number。last_committed表示数据库中上一个事务的提交编号,同一组事务的last_committed的值相同;sequence_number是顺序增长的,每个事务对应一个序列号。

  1. Commit-Parent-Based和Lock-Based两种模式。
    图1 并行复制的流程

    图1中C代表last_committed,表示事务进入到prepare阶段前获取到的最大commit逻辑时间。

    S代表sequence_number,表示每个事务commit结束后的逻辑时间。

    • Commit-Parent-Base模型:
      1. 拥有相等C的事务属于同一组同一组事务没有冲突,可以并行。
      2. Trx1、Trx2、Trx3、Trx4不属于同一个事务组,无法并行。
      3. Trx5、Trx6、Trx7属于同一个事务组,它们的C是4,可以并行。
      4. Trx8、Trx9属于同一个事务,它们的C是6,可以并行。但必须等待Trx7结束以后才能执行。

      Commit-Parent-Base模式下,如果master的组事务越少,并行度越低。

    • Lock-Based模式:

      出现重叠的事务(事务的C和S之间存在交集),说明这些事务在prepare阶段可以一起执行,意味着没有锁冲突,可以并行。

      1. Trx1、Trx2、Trx3、Trx4没有重叠,不可以并行。
      2. Trx5、Trx6、Trx7重叠,可以并行。
      3. Trx7、Trx8、Trx9重叠,但他们并不是同一组,可以并行,当Trx5、Trx6执行结束后,Trx7、Trx8、Trx9就可以并行,Lock-Based的并行粒度比Commit-Parent-Base更细。

    并行复制的处理流程主要是通过binlog_prepare函数为每个事务生成一个last_committed,进入Flush队列的第一个事务的last_committed作为这个队列的last_commit,调用binlog_cache_data::flush函数可获取每个事务sequence_number,调用Mts_submode_logical_clock:Mts_submode函数实现从库的并行复制。

  2. WriteSet复制。

    WriteSet通过检测不同事务之间是否存在写冲突,在写入relay log时,会将没有冲突的事务的last_committed值设置为相同的值,重新规划事务的并行回放,所以使用WriteSet并行复制,从库上的并发程度不再依赖于主库,而是基于事务本身的更新冲突来确定并行关系。WriteSet会将主库上已提交事务的被修改的主键和非空的唯一性索引值进行hash运算,并将运算后的值和内存中hash map(hash map存放了最近一段时间内提交的记录)里的值进行对比,判断当前事务与已经提交的事务是否存在冲突,如果不存在冲突,当前事务和已经提交的事务共享相同的last_committed值,若存在冲突,在hash map已提交的WriteSet中删除该冲突事务之前提交的所有WriteSet,并退化到commit_order方式计算last_committed。

    图2 WriteSet冲突检测的流程
    1. 在hash map中查找Write Set中row1的hashvalue,如果找到row1的hashvalue ,说明与hash map 存在冲突,将历史map中这行数据的sequence number改为130,同时设置当前提交事务的last_committed为120。
    2. 在hash map中查找Write Set中row7的hashvalue,如果找到row7的hashvalue,说明与hash map 存在冲突,将hash map中这行数据的sequence number改为130,由hash map中的sequence number为114,小于120,Writeset中last_committed不做更改。
    3. 在hash map中查找Write Set中row6的hashvalue,如果找到行row6的hashvalue,说明与hash map 存在冲突,将hash map中这行数据的sequence number改为130,由于hash map中的seq number为105,小于120,Writeset中last_committed不做更改。
    4. 在hash map中查找Write Set中row10的hashvalue,需要将这一行插入到hash map中。

      本次事务row1、row7、row6都检测到冲突,无法在从库中并行执行,而row10没有检测到冲突,可以在从库中执行。

适用场景

并行复制的优点在于提高新增数据在从库上重放的性能,帮助用户更快的查到最新的数据,但并行复制不是实时复制,不保证查到实时更新的数据。