本篇介绍主备延迟产生时,备库如何追上主库的策略和思路;内容包括:为什么单线程下备库难以"追上"主库?MySQL 5.5版本下的按表分发策略、MySQL 5.6版本的库并行复制策略、MariaDB基于组提交 (group commit) 优化的并行复制策略、MySQL 5.7 版本在MariaDB的策略基础上的优化;
前文介绍了主备延迟的原因,可能是主备库物理机性能差距,可能是备库承担额外的读压力,或是主库提交了大事务导致sbm (seconds_behind_master) 持续增加;
根据日常工作经验,主从延迟一般发生在主库短时间内执行大量更新后发生,并且在集中更新结束后的一段时间后,主从延迟逐渐降低;
考虑一个问题:如果主/备机器性能一样,但备库采用单线程,主从延迟会出现吗?答案是——会!
原因很简单:在主库上,影响并发度的原因就是各种锁了;由于InnoDB引擎支持行锁(粒度小),除了所有并发事务都在更新同一行(热点行)这种极端场景外,它对业务并发度的支持还是很友好的;如果此时,备库单线程的去消费relaylog中转日志,哪怕主/备机器性能一样,肯定是追不上主库的;
回忆下主备同步的流程图;谈到主备的并行复制能力,我们要关注的是图中黑色的两个箭头;一个箭头代表了客户端写入主库,另一箭头代表的是备库上sql_thread执行中转日志(relaylog);
如果用箭头的粗细来代表并行度的话,那么如图所示,第一个箭头要明显粗于第二个箭头;如果sql_thread是用单线程的话,就会导致备库应用日志不够快,造成主备延迟;
通过上述分析,为了追上主库,备库需要使用多线程来做主备复制,类似下面的这个模型:
coordinator就是原来的sql_thread,不过现在它不再直接更新数据了,只负责读取中转日志和分发事务;真正更新日志的,变成了worker线程;
备库能并行执行数据同步任务的前提:由于备库在执行主备同步的同时也提供查询,因此备库的数据不光要保证最终一致性,在执行主备同步任务期间也要保证与主库一致;
所以,coordinator在分发的时候,需要满足以下这两个基本要求:
原则1:不能造成更新覆盖;这就要求更新同一行的两个事务,必须被分发到同一个worker中,从而保证按时间顺序先后执行;
原则2:同一个事务不能被拆开,必须放到同一个worker中,保证事务的隔离性不被破坏;
下面介绍的MySQL各个版本的备库多线程复制,都遵循了这两条基本原则;
按表分发事务的基本思路是:如果两个事务更新不同的表,它们就可以并行,天然保证了两个worker不会更新同一行;该思路下,需要关注——如果有跨表的事务,需要如何分配任务;
如图,每个worker线程对应一个hash表,用于保存当前正在这个worker的“执行队列”里的事务所涉及的表;hash表的key是“库名.表名”,value是一个数字,表示队列中有多少个事务修改这个表;
hash表更新规则:在有事务分配给worker时,事务里面涉及的表会被加到当前worker的hash表中;worker执行完当前事务后,这个表会被从hash表中去掉;
举个例子说明任务分配规则,图中,hash_table_1表示,现在worker_1的“待执行事务队列”里,有4个事务涉及到db1.t1表,有1个事务涉及到db1.t2表;hash_table_2表示,现在worker_2中有1个事务涉及到db1.t3表;此时,coordinator从中转日志中读入一个新事务T,这个事务修改的行涉及到表db1.t1和db1.t3,执行任务分配的步骤如下:
事务T涉及到表db1.t1和db1.t3,而worker_1中有事务在修改表t1,说明当前事务T和worker_1是冲突的;
按照这个逻辑,顺序判断事务T和每个worker队列的冲突关系;一遍下来,发现事务T跟worker_2也冲突;
事务T跟多于一个worker冲突,coordinator线程就进入等待;同时,每个worker继续执行,同时修改hash_table;
假设worker_2执行完db1.t3相关的事务,就会从hash_table_2中把db1.t3这一项去掉;
此时,coordinator发现跟事务T冲突的worker只有worker_1这1个工作线程,因此就把它分配给worker_1;
coordinator继续读下一个中转日志,继续分配事务;
核心就是,每个事务在分发的时候,跟所有worker的冲突关系包括以下三种情况:
如果跟所有worker都不冲突,coordinator线程就会把这个事务分配给最空闲的woker;
如果跟多于一个worker冲突,coordinator线程就进入等待状态,直到和这个事务存在冲突关系的worker只剩下1个;
如果只跟一个worker冲突,coordinator线程就会把这个事务分配给这个存在冲突关系的worker;
优点:事务涉及的表大部分都为单表时,任务被同时均匀分配给各个worker线程,执行效率很高;
缺点:碰到热点表,比如所有的更新事务都会涉及到某一个表的时候,所有事务都会被分配到同一个worker中,就变成单线程复制了,效率低;
这个策略相对于上面的按表并行策略,粒度更大,也更简单;区别就是,在用于决定分发策略的hash表里,将key从"库名.表名"改成了"库名";
优点:相比于按表分发,构造hash值的时候很快,只需要库名;这个策略的并行效果,取决于压力模型,如果在主库上有多个DB,并且各个DB的压力均衡,使用这个策略的效果会很好;
缺点:与按表分发类似,如果主库上的表都放在同一个DB里面,或者只有一个库是热点库,那这个策略就没有效果了;
简单介绍下,MariaDB是MySQL的一个分支,主要由开源社区在维护,而MySQL是Oracle维护,MariaDB完全兼容MySQL;
简单回顾下MySQL的组提交(group commit)机制:在第一个事务写完redolog buffer / binlog files以后,尽量晚调用fsync,尽量在fsync之前,让更多的redolog/binlog加入批次(提交组),然后再对整个组一次性fsync;一次组提交里面,组员越多,节约磁盘IOPS的效果越好;
而MariaDB的并行复制策略利用的就是这个特性:
能够在同一组里提交的事务,一定不会修改同一行;
主库上可以并行执行的事务,备库上也一定是可以并行执行的;
实现上,MariaDB是这么做的:
在一组里面一起提交的事务,有一个相同的commit_id,下一组就是commit_id+1;
commit_id直接写到binlog里面;
传到备库应用的时候,相同commit_id的事务分发到多个worker执行;
这一组全部执行完成后,coordinator再去取下一批;
如图,这个策略有一个问题:它并没有实现“真正的模拟主库并发度”这个目标;在主库上,一组事务在commit的时候,下一组事务是同时处于“执行中”状态的;而在备库上执行的时候,要等第一组事务完全执行完成后,第二组事务才能开始执行,这样系统的吞吐量就不够;
优点:这个策略是一个很漂亮的创新,之前业界的思路都是在“分析binlog,并拆分到worker”上;而MariaDB的这个策略,目标是“模拟主库的并行模式”;它对原系统的改造非常少,实现也很优雅;
缺点:这个方案很容易被大事务拖后腿;假设图里同个组内trx2是一个超大事务,那么在备库应用的时候,trx1和trx3执行完成后,就只能等trx2完全执行完成,下一组才能开始执行;这段时间,相当于只有一个worker线程在工作,是对资源的浪费;
官方的MySQL5.7版本对MySQL5.6和MariaDB的方案进行了合并,通过参数slave-parallel-type控制并行复制策略:
配置为DATABASE,使用MySQL5.6版本的按数据库并行策略;
配置为LOGICAL_CLOCK,使用类似MariaDB的策略,并且对并行度做了优化;
先思考个问题:为什么MariaDB选择主库上能够在"同一组里提交committing"的事务并行执行?选择上图中同时处于"执行状态running"的所有事务并行执行可以吗?
答案是——不能!因为,同时处于"执行状态running"的所有事务,这里面可能存在由于锁冲突而处于锁等待状态的事务;如果这些事务在备库上被分配到不同的worker,就会出现备库跟主库不一致的情况;而上面提到的MariaDB这个策略的核心,是"所有处于commit"状态的事务可以并行;事务处于committing状态,表示已经通过了锁冲突的检验了;
回顾下前文MySQL针对组提交(group commit)的优化 :
如图,其实不用等到commit阶段,只要能够到达redolog prepare阶段,就表示事务已经通过锁冲突的检验了;因此,MySQL5.7并行复制策略的思想是:
同时处于prepare状态的事务,在备库执行时是可以并行的;
处于prepare状态的事务,与处于commit状态的事务之间,在备库执行时也是可以并行的;
MySQL针对组提交的优化思路是通过控制fsync前delay的参数(延迟时间、延迟事务数量),从而拉长binlog从write到fsync的时间,以此减少binlog的写盘次数;在MySQL5.7的并行复制策略里,它们可以用来制造更多的“同时处于prepare阶段的事务”,这样就增加了备库复制的并行度;
下篇文章:《MySQL实战45讲》——学习笔记28 “读写分离/主从延迟的解决方案/GTID“
本章参考:26 | 备库为什么会延迟好几个小时?