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

举报
GaussDB 数据库 发表于 2025/03/14 14:28:54 2025/03/14
【摘要】 1. MySQL并行复制方案演进MySQL的主备复制是基于binlog日志。早期版本中,只有单线程复制,即IO线程负责接收主库的binlog,并将其写入到备库的relay log中,SQL线程负责读取relay log中的event(日志事件),再在备库上进行回放。一般来说,主备之间的复制瓶颈在于SQL线程回放的速度比主库的写入速度慢,导致主备延迟过大,进而影响备节点数据的实时性和备节点的读...

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

在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.PNG

图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 相关参数说明

截图2.PNG

根据数据库配置高低设置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 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

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。