写点什么

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

  • 2025-04-24
    广东
  • 本文字数:3919 字

    阅读完需:约 13 分钟

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

​本文分享自华为云社区【华为云MySQL技术专栏】MySQL的WriteSet并行复制介绍

作者:GaussDB 数据库


一、MySQL 并行复制方案演进


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


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


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


1)MySQL 5.6 基于库级别的并行复制


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


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


2)MySQL 5.7 基于组提交的并行复制


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


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


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


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


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


3)MySQL 8.0 基于 WRITESET 的并行复制


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


二、MySQL 8.0 基于 WRITESET 的并行复制详情


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_commited 和 sequence 对比:


测试脚本:

SET GLOBAL binlog_transaction_dependency_tracking='writeset';

drop table if exists tb1001;

CREATE TABLE `tb1001` (

`id` int(11) NOT NULL AUTO_INCREMENT,

`c1` int(11) DEFAULT 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 过大,不光消耗更多内存,还会降低冲突查询的效率。


三、总结


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


参考资料


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


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


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


关注“GaussDB 数据库”公众号,了解更多动态


点击关注,第一时间了解华为云新鲜技术~


用户头像

提供全面深入的云计算技术干货 2020-07-14 加入

生于云,长于云,让开发者成为决定性力量

评论

发布
暂无评论
【华为云MySQL技术专栏】MySQL的WriteSet并行复制介绍_,华为云_华为云开发者联盟_InfoQ写作社区