为了缓解这种问题,很自然的想法是提高 SQL 线程重放的并行度,引入并行复制。

    5.6 Schema 级别的并行复制

    开启并行复制后,会启动多个 Worker 线程,原有的 SQL 线程变为 Coordinator 线程。

    可以并行的事务分发给 Worker 线程执行; 不能并行的事务等待 Worker 线程全部结束后,再由 Coordinator 线程自己执行。

    DDL 语句或者是跨 Schema 的语句不能并行执行。

    这种并行复制的模式,对于多个 DB 同时更新才能有较高的并行度,但是更常见的情况是更新集中在同个一个 DB。

    一个简单的改进,把 Schema 级别的并行复制改成 Table 级别,可以大幅度提高单库多表环境下的并行度。但是对于只有一个热点表的情况依然处理不了。

    5.7 基于 Group Commit 的并行复制

    Group Commit

    引入 Group Commit之前,Binlog 和 InnoDB 日志是内部XA,为了保证 InnoDB 和 Binlog 提交顺序一致,实际是串行提交,执行序列如下:

    • InnoDB prepare
    • write/sync Binlog
    • InnoDB commit

    官方的 Group Commit 分为三个阶段,每个阶段有一个线程操作,三个阶段可以并发执行。

    • flush stage:binlog 从 cache 写入文件
    • commit stage:引擎层 commit

    这样 InnoDB prepare 成功的事务可以进入队列,每个阶段可以对队列事务统一做操作,提高了并行度。

    写binlog 和 InnoDB commit 都是按照队列中的顺序,可以保证 binlog 和事务提交顺序一致。

    binlog 中记录了 sequence_number 和 last_commited,如上图,mysqlbinlog 解析日志可以看到这两个值。

    如果两个事务的 last_commited 相同,说明这两个事务是在同一个 Group 内提交的。

    LOGICAL_CLOCK 并行复制

    5.7 引入了变量 slave_parallel_type,可选值 DATABASE、LOGICAL_CLOCK,DATABASE 就是和 5.6 中相同,Schema 级别的并行复制,而 LOGICAL_CLOCK 是基于 Group Commit 的并行复制,相比 5.6 极大提高了并行度。

    Group Commit 实现了主库事务的并行提交。很显然的,主库能同时进入prepare阶段的事务之间不会冲突,那么这些事务在备库回放时也不会冲突。

    Group Commit 中,last_commited 相同的事务,可以在备库并行回放。

    MySQL bin log 里面维护了两个变量

    Logical_clock max_committed_transaction:记录上次 Group commit 时最大的 sequence_number,即上述 mysqlbinlog 中的 last_committed

    Logical_clock transaction_counter:sequence_number 来源,每次分配 sequence_number 时 transaction_counter 进行递增,即当前最大的 sequence_number

    5.7 的并行复制还有一点点弊端,如果如果主库并行度低,那么备库回放时也很难并行。

    为此,5.7 引入了两个参数:

    • binlog_group_commit_sync_delay:等待延迟提交的时间,binlog提交后等待一段时间再 fsync。让每个 group 的事务更多,人为提高并行度。
    • binlog_group_commit_sync_no_delay_count:等待提交的最大事务数,如果等待时间没到,而事务数达到了,就立即 fsync。达到期望的并行度后立即提交,尽量缩小等待延迟。

    8.0 基于 WriteSet 的并行复制

    5.7 为了提高备库回放的速度,需要在主库尽量提高并行度。

    8.0解决了这个问题,即使主库在串行提交的事务,只有互相不冲突,在备库就可以并行回放。

    8.0 引入了参数 binlog_transaction_dependency_tracking 来控制事务依赖模式,让备库根据 commit timestamps 或者 write sets 并行回放事务,有三个取值:

    • COMMIT_ORDERE:使用 5.7 Group commit 的方式决定事务依赖
    • WRITESET:使用 WriteSet 的方式决定判定事务直接的冲突,发现冲突则依赖冲突事务,否则按照 COMMIT_ORDERE 方式决定依赖

    参数 transaction_write_set_extraction 决定 hash 算法,可选值:OFF、MURMUR32、XXHASH64,默认值 XXHASH64,如果

    WriteSet 记录了事务的更新行信息,决定 commit_parent时,使用事务自己的 session WriteSet 和 history WriteSet 进行比对,找到最近的冲突行,设为 commit_parent。如果 WriteSet 找不到 commit_parent,则还是使用 COMMIT_ORDERE 决定 commit_parent

    写入 session write_set 入口在 rpl_write_set_handler.cc:add_pke() (pke 是 primary key equivalent 的缩写)

    add_pke() 在 binlog_write_row() 内部调用,每次插入、更新、删除记录,都会更新 session WriteSet

    add_pke() 会记录每一条主键、唯一键、外键信息,并且会记录是否更新了无主键表,或者更新了外键关联的列

    WriteSet 中每条记录是一个 uint64 hash,hash字符串由更新行的 index,db,table,value 组成

    MySQL_BIN_LOG 里定义的 Transaction_dependency_tracker m_dependency_tracker 是决定事务依赖的入口。

    MYSQL_BIN_LOG::write_gtid 入口处决定调用 m_dependency_tracker.get_dependency() 获取事务依赖

    可以看到,对于三种依赖模式,处理流程是依次递增的。

    COMMIT_ORDERE 是 5.7 引入的,这里就不再深入分析。

    随着 MySQL 版本迭代,备库回放效率越来越高,为了保证主备同步时效性,可以尽量更新版本 MySQL

    同时,为了保证备库回放效率,应该根据业务模型适当设置复制相关参数。

    在 8.0 中根据数据库配置高低设置 binlog_transaction_dependency_history_size,性能有富余的实例可以适当调大该参数,找到更小的 commit parent,提高备库回放并行度。内存和CPU紧张的实例最好避免在 WriteSet上消耗太多资源。binlog_transaction_dependency_history_size 过大,不光消耗内存,还会降低冲突查询的效率。