本文分享自华为云社区【华为云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数据库”公众号,了解更多动态

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

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐