暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

【华为云MySQL技术专栏】MySQL的WriteSet并行复制介绍

GaussDB数据库 2025-03-18
11

1


MySQL并行复制方案演进


MySQL的主备复制是基于binlog日志。早期版本中,只有单线程复制,即IO线程负责接收主库的binlog,并将其写入到备库的relay log中,SQL线程负责读取relay log中的event(日志事件),再在备库上进行回放。


一般来说,主备之间的复制瓶颈在于SQL线程回放的速度比主库的写入速度慢,导致主备延迟过大,进而影响备节点数据的实时性和备节点的读业务,最终只能将业务切换到主节点上执行,否则会造成业务受损。


为了缓解这个问题,则需要提高SQL线程回放的并行度。因此,MySQL引入了并行复制。


MySQL 5.6 

基于库级别的并行复制

MySQL 5.6中,可以通过设置参数 `slave_parallel_workers` 来启用多个SQL线程并行复制。这种并行复制仅适用于多库场景,即当主节点并发对不同库进行写操作时,备库的复制速度会有显著提升。


然而,大多数情况下,主节点并发操作,都是单库多表的场景,因此这种并行复制方式就有很大的局限性,大部分场景下会退化为单线程回放,并不能提高回放速度。


MySQL 5.7 

基于组提交的并行复制

MySQL 5.7中,新增了slave_parallel_type参数来控制选择并行回放的方式,可选值分别为DATABASE,LOGICAL_CLOCK,DATABASE是基于库级别的并行复制。其中,LOGICAL_CLOCK是基于组提交的并行方式,LOGICAL_CLOCK方式更大的提供并行度。


组提交(group commit):为了解决写redobinlog时频繁刷磁盘的问题,将事务进行分组,让多个事务的刷盘动作合并为一次刷盘,减少磁盘读写,提供数据库并发的性能。如果事务能够同时提交成功,则它们之间就不会存在锁冲突,因此,这些事务也可以在备机上并行执行。


MySQL支持基于组提交的并行复制后,主备延迟有较大程度的改善,但是一定程度上会受到主节点的并发度的影响。当主节点的每次组提交的事务数较少的时候,binlog在备节点上的回放的并发度也会因此而降低,即使这些事务之间并没有任何冲突。例如:

    Trx1 -----L----C---------------------------------->
    Trx2 ---------------L----C------------------------>
    Trx3 ------------------------L----C--------------->
    Trx4 --------------------------------L----C------->
    复制


    假定这些事务之间并没有任何冲突,由于它们的LOGICAL CLOCK周期没有重叠,在备机上也只能串行回放。


    为了解决这个问题,MySQL 8.0.1引入了基于WriteSet的复制。


    MySQL 8.0 

    基于WRITESET的并行复制


    MySQL 8.0中, WriteSet并行复制的基本思想是:不同事务的不同的记录不重叠,如果不同事务修改的记录不重叠,则这些事务都可在备节点上并行回放。因此,并行方式的粒度从事务组内并行到记录级别。在写binlog时,会将事务所修改的行的hash值添加到事务的写集合中,根据写集合计算事务间的依赖关系,在备机实现并行回放。


    2


    MySQL 8.0 

    基于WRITESET的并行复制详情


    2.1 WriteSet源码分析


    事务writeset更新入口在add_pke()函数中,其中,pke  primary key equivalent 的缩写。add_pke处理逻辑,如图1所示:


    图1 add_pke处理逻辑


    如果表中不存在索引,则直接结束。如果存在索引,则进行如下步骤:

    步骤一,将数据库名和表名信息写入临时变量。

    步骤二,循环扫描表中的每个索引:如果索引不是唯一索引,跳过本次循环,继续下一次。

    步骤三,循环两种生成数据的方式(MySQL格式和字符串格式):

     - 将索引名字写入到 `pke` 中,接着,再将临时变量信息写入到 `pke` 中。通过循环扫描索引中的每一个字段,将每个字段的信息写入到 `pke` 中。

    - 如果字段扫描完成,将生成 `pke` 的哈希值,写入到写集合中。


    事务依赖函数入口为get_dependency,根据参数binlog_transaction_dependency_tracking选择不同的依赖模式,处理流程是依次递增。


      void Transaction_dependency_tracker::get_dependency()
      {
        switch (m_opt_tracking_mode) {
          case DEPENDENCY_TRACKING_COMMIT_ORDER:
            m_commit_order.get_dependency(thd, sequence_number, commit_parent);
            break;
          case DEPENDENCY_TRACKING_WRITESET:
            m_commit_order.get_dependency(thd, sequence_number, commit_parent);
            m_writeset.get_dependency(thd, sequence_number, commit_parent);
            break;
          case DEPENDENCY_TRACKING_WRITESET_SESSION:
            m_commit_order.get_dependency(thd, sequence_number, commit_parent);
            m_writeset.get_dependency(thd, sequence_number, commit_parent);
            m_writeset_session.get_dependency(thd, sequence_number, commit_parent);
            break;
          default:
            DBUG_ASSERT(0); 
            m_commit_order.get_dependency(thd, sequence_number, commit_parent);
        }
      }
      复制


      WriteSet依赖模式关键代码:


        void Writeset_trx_dependency_tracker::get_dependency() {
          // 检查是否能使用writeset
          bool can_use_writesets =…
          bool exceeds_capacity = false;
          if (can_use_writesets) {
            int64 last_parent = m_writeset_history_start;
            // 遍历一个事务所有修改的行的hash值
            for (std::vector<uint64>::iterator it = writeset->begin();
                 it != writeset->end(); ++it) {
              // 对每一个hash值都去history中寻找是否有对应的hash
              Writeset_history::iterator hst = m_writeset_history.find(*it);
              if (hst != m_writeset_history.end()) {
                // 如果一个行的hash存在于history中且对应的事务先于当前事务
                if (hst->second > last_parent && hst->second < sequence_number)
                  // 修改当前事务所依赖的事务的sequence number
                  last_parent = hst->second;
                // 标记该行由当前事务修改
                hst->second = sequence_number;
              } else {
                // 将hash值和事务的sequence number插入到history中
                if (!exceeds_capacity)
                  m_writeset_history.insert(
                      std::pair<uint64, int64>(*it, sequence_number));
              }
            }
            // 同时没有主键和非空唯一键的表不能使用writeset
            if (!write_set_ctx->get_has_missing_keys()) {
              // 当前事务所操作的table都有主键的前提下
              // 取last parent和commit parent中的较小值
              // 作为当前事务的commit parent (last_committed)
              commit_parent = std::min(last_parent, commit_parent);
            }
            if (exceeds_capacity || !can_use_writesets) {
              // 超过history最大值或者当前事务不能使用writeset则清空当前history
              m_writeset_history_start = sequence_number;
              m_writeset_history.clear();
            }
          }
        }
        复制


        使用WriteSet时,表必须要有主键或唯一键。如果表无主键或唯一键,则会回退到commit_order方式进行并行复制。


        下面是无主键表和主键表生成的last_commitedsequence对比:


        测试脚本:

          SET GLOBAL
          binlog_transaction_dependency_tracking='writeset';
          drop table if exists tb1001;
          CREATE TABLE `tb1001` (
           
          `id` int(11NOT NULL AUTO_INCREMENT,
           
          `c1` int(11DEFAULT NULL,
           
          `c2` datetime DEFAULT NULL
          ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
          flush logs;
          insert into tb1001(c1,c2)select 1,now();
          insert into tb1001(c1,c2)select 1,now();
          insert into tb1001(c1,c2)select 1,now();
          insert into tb1001(c1,c2)select 1,now();
          update tb1001 set c2=now() where id=2;
          insert into tb1001(c1,c2)select 1,now();
          复制


          表无主键生成的last_committed和sequence_number信息:

            #GTID      last_committed=0   sequence_number=1
            #GTID       last_committed=1   sequence_number=2
            #GTID       last_committed=2   sequence_number=3       
            #GTID       last_committed=3   sequence_number=4       
            # GTID      last_committed=4   sequence_number=5
            #GTID       last_committed=5   sequence_number=6  
            复制


            表有主键生成的last_committed和sequence_number信息:

              # GTID      last_committed=0   sequence_number=1
              #GTID       last_committed=1   sequence_number=2
              #GTID       last_committed=1   sequence_number=3       
              #GTID       last_committed=1   sequence_number=4       
              # GTID      last_committed=2   sequence_number=5
              #GTID       last_committed=1   sequence_number=6
              复制

              除了表要有主键或者唯一键外,还需要合理设计主键。例如,使用随机散列生成的主键可能会导致哈希冲突率变高,从而降低并行度。


              2.2 相关参数说明



              根据数据库配置高低设置binlog_transaction_dependency_history_size,默认配置为25000。对于性能有富余的实例,可以适当调大该参数,比如提高至100000,以找到更小的 commit parent,提高备库的回放并行度。对于内存和CPU紧张的实例,最好避免在 WriteSet上消耗太多资源。


              binlog_transaction_dependency_history_size过大,不光消耗更多内存,还会降低冲突查询的效率。


              3


              总结


              MySQL 5.6MySQL 8.0,并行复制技术经历了从库级别到WRITESET级别的演进,每一次改进都显著提升了复制性能和效率。在MySQL 8.0中,基于WRITESET的并行复制机制,通过更细粒度的并行控制,进一步优化了复制性能,提升了系统的并发处理能力。不过,要充分发挥并行复制的优势,需要根据具体场景合理配置相关参数,定期监控和优化系统性能。


              参考资料

              1. WL#7165: MTS: Optimizing MTS scheduling by increasing the parallelization window on masterhttps://dev.mysql.com/worklog/task/?id=7165

              2WL#9556: Writeset-based MTS dependency tracking on master

              https://dev.mysql.com/worklog/task/?id=9556


              END


              华为云数据库 新用户


              Flexus云数据库RDS 1年612元

              (原价2400元)

              RDS for MySQL单机版 1年611.5元


              扫码抢购

                      

              活动时间:2025/3/1-2025/4/15

                戳“阅读原文”,了解更多

              文章转载自GaussDB数据库,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

              评论