• 速度提升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的区别,以及具体的实现?

  • 相关阅读:
    C# 中的 ConfigurationManager类引用方法
    添加Word,Excel等dll时如何操作。
    Win7(64位)中IIS配置Access数据库的asp.net程序中出现“未在本地计算机上注册“Microsoft.Jet.OLEDB.4.0”提供程序”
    sql模糊查询
    Spring中AOP的使用
    MongoDB的孤儿文档是如何产生的
    Docker
    MySql索引优化
    Kafka(分布式流式系统)
    synchronized的底层实现
  • 原文地址:https://www.cnblogs.com/VicLiu/p/14653400.html
Copyright © 2020-2023  润新知