引用:
https://time.geekbang.org/column/article/77083《MySQL实战45讲》
什么是并行复制?
在官方的 5.6 版本之前,MySQL 只支持单线程复制,由此在主库并发高、TPS 高时就会出现严重的主备延迟问题。
其实说到底,所有的多线程复制机制,都是要把一个线程的 sql_thread,拆成多个线程,也就是都符合下面的这个模型:
coordinator ->多个worker
coordinator 就是原来的 sql_thread, 不过现在它不再直接更新数据了,只负责读取中转日志和分发事务。
真正更新日志的,变成了 worker 线程。而 work 线程的个数,就是由参数 slave_parallel_workers 决定的。
并行复制怎么分发?
事务能不能按照轮询的方式分发给各个 worker?
1、事务能不能按照轮询的方式分发给各个 worker,因为CPU 的调度策略无法保证从库执行顺序,可能会更新覆盖。
同一个事务的多个更新语句,能不能分给不同的 worker 来执行呢?
2、同一个事务,从库虽然能保证最终一致性,但是破坏了事务逻辑的隔离性。
并行复制的原则
所以,coordinator 在分发的时候,需要满足以下这两个基本要求:
(1)不能造成更新覆盖。这就要求更新同一行的两个事务,必须被分发到同一个 worker 中。
(2)同一个事务不能被拆开,必须放到同一个 worker 中。
各个版本的多线程复制,都遵循了这两条基本原则。
按表分发
按表分发逻辑
按表分发事务的基本思路是,如果两个事务更新不同的表,它们就可以并行。
因为数据是存储在表里的,所以按表分发,可以保证两个 worker 不会更新同一行。如果有跨表的事务,还是要把两张表放在一起考虑的
也就是说,每个事务在分发的时候,跟所有 worker 的冲突关系包括以下三种情况:
1、如果跟所有 worker 都不冲突,coordinator 线程就会把这个事务分配给最空闲的 woker;
2、如果跟多于一个 worker 冲突,coordinator 线程就进入等待状态,直到和这个事务存在冲突关系的 worker 只剩下 1 个;
3、如果只跟一个 worker 冲突,coordinator 线程就会把这个事务分配给这个存在冲突关系的 worker。
按表分发的问题
这个按表分发的方案,在多个表负载均匀的场景里应用效果很好。但是,如果碰到热点表,比如所有的更新事务都会涉及到某一个表的时候,所有事务都会被分配到同一个 worker 中,就变成单线程复制了。
按行分发
要解决热点表的并行复制问题,就需要一个按行并行复制的方案。
按行分发的逻辑
如果两个事务没有更新相同的行,它们在备库上可以并行执行。显然,这个模式要求 binlog 格式必须是 row。
判断一个事务 T 和 worker 是否冲突,用的就规则就不是“修改同一个表”,而是“修改同一行”。
也是为每个 worker,分配一个 hash 表。只是要实现按行分发,这时候的 key,就必须是“库名 + 表名 + 唯一键的值”。
基于行的策略,事务 hash 表中还需要考虑唯一键,即 key 应该是“库名 + 表名 + 索引 a 的名字 +a 的值”。
按行分发的问题
相比于按表并行分发策略,按行并行策略在决定线程分发的时候,需要消耗更多的计算资源。
1、要能够从 binlog 里面解析出表名、主键值和唯一索引的值。也就是说,主库的 binlog 格式必须是 row;
2、表必须有主键;不能有外键。表上如果有外键,级联更新的行不会记录在 binlog 中,这样冲突检测就不准确。
基于组提交的并行复制
什么是组提交
简单来说就是在双1的设置下,事务提交后即刷盘的操作改为多个事务合并成一组事务再进行统一刷盘,这样处理就降低了磁盘IO的压力。
通过对事务进行分组,优化减少了生成二进制日志所需的操作数。一个组提交(group commit)的事务可以并行回放。
如何判断事务在一个组内呢?
一组事务同时提交也就意味着组内事务不存在冲突,故组内的事务在从节点上就可以并发执行,问题在于如何区分事务是否在同一组中的,于是在binlog中出现了两个新的参数信息last_committed
和 sequence_number,其中last_committed存在重复的情况。
sequence_number # 这个值指的是事务提交的序号,单调递增。
last_committed # 这个值有两层含义,1.相同值代表这些事务是在同一个组内,2.该值同时又是代表上一组事务的最大编号。
方案不足点
基于LOGICAL_CLOCK的同步有个不足点,就是当主节点的事务繁忙度较低的时候,导致时间段内组提交fsync刷盘的事务量较少,于是导致从库回放的并行度并不高,甚至可能一组里面只有一个事务,这样从节点的多线程就基本用不到,可以通过设置下面两个参数,让主节点延迟提交。
binlog_group_commit_sync_delay 参数,表示延迟多少微秒后才调用 fsync;
binlog_group_commit_sync_no_delay_count 参数,表示累积多少次以后才调用 fsync。
发现二进制日志当中last_committed和sequence_number,last_committed表示事务提交的时候,上次提交的事务编号,如果事务具有相同last_committed,则表示这些事务都在一组内,可以进行并行的回放。
MariaDB 的并行复制策略
MariaDB 的并行复制策略利用的组提交就是这个特性:
能够在同一组里提交的事务,一定不会修改同一行;
主库上可以并行执行的事务,备库上也一定是可以并行执行的。
MariaDB 的并行复制逻辑
1、在一组里面一起提交的事务,有一个相同的 commit_id,下一组就是 commit_id+1;
2、commit_id 直接写到 binlog 里面;
3、传到备库应用的时候,相同 commit_id 的事务分发到多个 worker 执行;
4、这一组全部执行完成后,coordinator 再去取下一批。
MariaDB 的并行复制的问题
并没有实现“真正的模拟主库并发度”这个目标。在主库上,一组事务在 commit 的时候,下一组事务是同时处于“执行中”状态的。
但是在备库上执行的时候,要等第一组事务完全执行完成后,第二组事务才能开始执行,这样系统的吞吐量就不够。
MySQL 5.7 的并行复制
官方的 MySQL5.7 版本也提供了类似的功能,由参数 slave-parallel-type 来控制并行复制策略:
1、配置为 DATABASE,表示使用 MySQL 5.6 版本的按库并行策略;
2、配置为 LOGICAL_CLOCK,表示的就是类似 MariaDB 的策略。
不过,MySQL 5.7 这个策略,针对并行度做了优化。
MariaDB 这个策略的核心,是“所有处于 commit”状态的事务可以并行。事务处于 commit 状态,表示已经通过了锁冲突的检验了。
其实,不用等到 commit 阶段,只要能够到达 redo log prepare 阶段,就表示事务已经通过锁冲突的检验了。
MySQL 5.7 并行复制策略逻辑
同时处于 prepare 状态的事务,在备库执行时是可以并行的;
处于 prepare 状态的事务,与处于 commit 状态的事务之间,在备库执行时也是可以并行的。
MySQL 8.0的并行复制
binlog-transaction-dependency-tracking,用来控制是否启用这个新策略。这个参数的可选值有以下三种。
1、COMMIT_ORDER,表示的就是前面介绍的,根据同时进入 prepare 和 commit 来判断是否可以并行的策略。
2、WRITESET,表示的是对于事务涉及更新的每一行,计算出这一行的 hash 值,组成集合 writeset。如果两个事务没有操作相同的行,也就是说它们的 writeset 没有交集,就可以并行。
3、WRITESET_SESSION,是在 WRITESET 的基础上多了一个约束,即在主库上同一个线程先后执行的两个事务,在备库执行的时候,要保证相同的先后顺序。
这个就是按行分发策略的逻辑
对于“表上没主键”和“外键约束”的场景,WRITESET 策略也是没法并行的,也会暂时退化为单线程模型。
总结
MySQL5.6中的并行复制———Schema级别的并行复制
MySQL5.7中的并行复制———基于Group Commit 的并行复制
MySQL8.0 中的并行复制———真正的并行复制writeset