速度提升5~10倍,基于WRITESET的MySQL并行复制 #M1013#

本文转自 https://mp.weixin.qq.com/s/oj-DzpR-hZRMMziq2_0rYg备忘

MySQL主从复制一致性问题早已解决,然而主从复制延迟的问题依然困扰着开发人员和DBA。开发通常想将从机作为读写分离的一种选择,奈何复制延迟导致实际生产上,依然选择主实例(Master)作为查询源。对于DBA来说,高可用切换时,从发现到切换的整个过程堪称秒级切换,只是在最后开放写入这个过程中,需要大量时间等待从机的回放(Applier),整个过程被迫从秒级切换降级为分钟级切换。曾经DBA面试的一道经典题,其实是没有什么比较好的答案,因为这是MySQL复制机制的硬伤:

请问你是如果解决复制延迟的?

为此,MySQL从5.6版本开始支持并行复制机制,官方称为:MTS(Multi-Thread Slave),经过几个版本的迭代,目前MTS支持以下几种机制:

版本MTS机制实现原理
5.6 Database 基于库级的并行复制
5.7 COMMIT_ORDER 基于组提交的并行复制
5.7.22 WRITESET /
WRITESET_SESSION
基于WRITESET的并行复制

基于Database级别的并行复制效果并不特别好,因为大多数生产的架构依然习惯于单库多表的架构,这种情况下MTS依然还是单线程的效果。但Database级别并行复制的好处是可以兼容任何二进制日志,从机都可以进行库级别的并行回放。

基于Commit_Order的并行复制是在主数据库实例事务提交时,写入一些额外信息,从而在从机回放时,可以根据这些信息判断是否可以进行并行的回放。这种实现机制的巧妙之处在于:同一组提交的事务之间是不冲突的,因此可以并行回放

在代码实现中,同一组的事务拥有同一个parent_commit(父亲),在二进制日志中可以看到类似如下的内容:

 

 

last_commit相同可视为具有相同的parent_commit,事务在同一组内提交,因此在从机回放时,可以并行回放。例如last_committed = 0的有7个事务,sequence_number 1 ~ 7,则这7个可以并行执行。last_committed = 7有6个事务,sequence_number 9 ~ 14,可以并行回放执行。

在上面的并行执行中,last_committed = 1 的事务需要等待last_committed = 0的7个事务完成,同理,last_committed = 7的6个事务需要等待last_committed = 1的事务完成。但是MySQL 5.7还做了额外的优化,可进一步增大回放的并行度。思想是LOCK-BASED,即如果两个事务有重叠,则两个事务的锁依然是没有冲突的,依然可以并行回放。

 

在上面的例子中,last_committed = 1的事务可以和last_committed = 0的事务同时并行执行,因为事务有重叠。具体来说,这表示last_committed = 0的事务进入到COMMIT阶段时,last_committed的事务进入到了PREPARE阶段,即事务间依然没有冲突。具体实现思想可见官方的Worklog: WL#7165: MTS: Optimizing MTS scheduling by increasing the parallelization window on master

基于COMMIT_ORDER的并行复制机制虽好,然而需要有一个条件:每组提交事务要足够多。即,业务量要足够大。但是当你的业务量比较小,并发度不够时,基于COMMIT_ORDER的并行复制依然会退化为单线程复制。虽然有经验的小伙伴知道可以通过调整参数binlog_group_commit_sync_delay、binlog_group_commit_sync_no_delay_count来优化组提交的效率,但最终的效果其实并不理想。只能说某些情况有些用,大部分情况依然然并卵。

 

为了进一步解决复制延迟问题,MySQL 5.7.22版本推出了基于WriteSet机制的并行复制,从机并行执行无需依赖组复制机制。简单来说,WriteSet并行复制的思想是:不同事务的不同记录不重叠,则都可在从机上并行回放,可以看到并行的力度从组提交细化为记录级。

所谓不同的记录,在MySQL中用WriteSet对象来记录每行记录,从源码来看WriteSet就是每条记录hash后的值(必须开启ROW格式的二进制日志),具体算法如下:

WriteSet=hash(index_name | db_name | db_name_length | table_name | table_name_length | value | value_length)

上述公式中的index_name只记录唯一索引,主键也是唯一索引。如果有多个唯一索引,则每条记录会产生对应多个WriteSet值。另外,Value这里会分别计算原始值和带有Collation值的两种WriteSet。所以一条记录可能有多个WriteSet对象。举例来说,下面的表t1,有2个唯一索引:

CREATE TABLE t1 (

   a BIGINT NOT NULL AUTO_INCREMENT,

   b VARCHAR(36) NOT NULL,

   c INT NOT NULL,

   PRIMARY KEY(a),

   UNIQUE KEY idx_b(b)

)CHARSET=utf8mb4

当用户执行INSERT INTO test.t1 VALUES (NULL,UUID(),3)时,对产生多个个WriteSet值,分别是:

  • WriteSet1=hash(PRIAMRY|test|4|t1|2|1|8)

  • WriteSet2=hash(PRIAMRY|test|4|t1|2|1(with collation)|8)

  • WriteSet3=hash(idx_b|test|4|t1|2|'2'|1)

  • WriteSet4=hash(idx_b|test|4|t1|2|'2'(with collation)|1)

参数transaction_write_set_extraction用来选择hash函数,推荐设置为XXHASH64,相比MURMUR32有更好的散列性。产生的WriteSet对象会插入到WriteSet哈希表,哈希表的大小由参数binlog_transaction_dependency_history_size设置,默认25000。WriteSet哈希表的类型为std::map<uint64,int64>,保存每条记录的WriteSet值和对应的sequence_number。

当事务每次提交时,会计算修改的每个行记录的WriteSet值,然后查找哈希表中是否已经存在有同样的WriteSet,若无,WriteSet插入到哈希表,写入二进制日志的last_committed值不变。若有,则last_committed值更新为sequnce_number。

对于上面的INSERT语句,在插入后WriteSet哈希表中记录的内容为:

KeyValue
WriteSet1 1
WriteSet2 1
WriteSet3 1
WriteSet4 1

若这时另一个事务再次执行了INSERT INTO test.t1 VALUES (NULL,UUID(),3),则会产生新的WriteSet对象,但和上述的WriteSet没有冲突,直接插入WriteSet哈希表,表中内容更新为:

KeyValue
WriteSet1 1
WriteSet2 1
WriteSet3 1
WriteSet4 1
WriteSet5 2
WriteSet6 2
WriteSet7 2
WriteSet8 2

接着当用户执行DELETE FROM test.t1 WHERE a = 1,这时事务提交时会发现能在WriteSet哈希表中找到之前a=1对应的WriteSet,因此需要更新对应的sequence_number值,并且这时last_committed值也要更新为对应的sequence_number值:

KeyValue
WriteSet1 3
WriteSet2 3
WriteSet3 3
WriteSet4 3
WriteSet5 2
WriteSet6 2
WriteSet7 2
WriteSet8 2

回放时和基于COMMIT_ORDER的并行复制一样,具有相同的last_committed值可以并行回放,但是由于是基于WriteSet机制的,因此不同的记录能并行执行。同一条记录回放,last_committed值必然不同,必须等待之前的一条记录回放完成后才能执行。

 

默认MySQL依然还是基于库级别的并行复制配置,因此开启基于WriteSet并行复制需要进行如下的设置:

# master
loose-binlog_transaction_dependency_tracking = WRITESET

loose-transaction_write_set_extraction = XXHASH64
#slave
slave-parallel-type = LOGICAL_CLOCK

slave-parallel-workers = 32

接着用命令mysqlslap来执行上述SQL语句:INSERT INTO test.t1 VALUES (NULL,UUID(),3),线程设置为1,即单线程执行插入操作:

mysqlslap --query=insert_one.sql --number-of-queries=10 -c 1

虽然是单线程运行,但在二进制日志中可以看到大量相同last_committed值得记录,因此记录在从机都是可以并行执行。其实若继续插入,整个二进制日志中显示的last_committed可能都是1,因为测试的插入操作没有行冲突:

 

 

但若是基于Commit_Order的并行复制,由于是单线程中运行,在二进制日志中看到每个事物的last_committed值都不同,回访时依然只能是单线程回放:

 

根据上述单线程的测试对比,WriteSet的性能比Commit_Order要快5~6倍,效果非常明显。如果是有延迟要追的,WriteSet毫无疑问是胜者。Commit_Order的瓶颈依然是需要主有足够的并发度,实际生产上确很难达到,除非是业务高峰期。MySQL官方测试(来源:https://mysqlhighavailability.com/improving-the-parallel-applier-with-writeset-based-dependency-tracking/)结果如下,当然看看即可,最主要还是自己生产上的实际效果,不过姜老师坚信这次的效果会远好于Commit_Order。

 

 

 

对于源码看兴趣的同学推荐官方的WorkLog,写的超详细。WL#9556: Writeset-based MTS dependency tracking on master

若想快速过下源码,推荐几个关键函数调用:

binlog_log_row   

    -> add_pke      

         -> generate_hash_pke           

            -> Rpl_transaction_write_set_ctx::add_write_set

 

binlog_cache_data::flush   

    -> MYSQL_BIN_LOG::write_gtid       

        -> Writeset_trx_dependency_tracker::get_dependency

 

最后,留几个思考题给同学们,答出者请务必直接联系姜老师,我们需要有梦想,能背锅的实力派加盟:

  • 若WriteSet哈希表满了,MySQL会如何处理?这时last_committed的处理逻辑是怎样?

  • 为什么WRITESET中还要记录非主键的唯一索引?举例说明这种场景

  • 在哪种场景下,WriteSet复制依然无法很好的解决延迟问题?怎样优化呢?

  • 除了WriteSet并行复制,还有一种WriteSet_Session的并行复制机制,请问其和WriteSet的区别,以及具体的实现?

 

posted @ 2021-04-13 15:22  VicLW  阅读(1018)  评论(0编辑  收藏  举报