一、备库并行复制能力:
1、主备流程图:
??对于主备的并行复制能力,要关注的是图中黑色的两个箭头。一个箭头代表了客户端写入主库,另一箭头代表的是备库上sql_thread执行中转日志(relay log)。如果用箭头的粗细来代表并行度的话,那么第一个箭头要明显粗于第二个箭头。
??在主库上,影响并发度的原因是各种锁。由于InnoDB引擎支持行锁,除了所有并发事务都在更新同一行(热点行)这种极端场景外,它对业务并发度的支持是很友好的。
??日志在备库上的执行,就是图中备库上sql_thread更新数据(DATA)的逻辑。如果是用单线程的话,就会导致备库应用日志不够快,造成主备延迟。在官方的5.6版本之前,MySQL只支持单线程复制,由此在主库并发高、TPS高时就会出现严重的主备延迟问题。
2、多线程复制演进过程:
??coordinator就是原来的sql_thread, 不过现在它不再直接更新数据了,只负责读取中转日志和分发事务。真正更新日志的,变成了worker线程。而work线程的个数,就是由参数slave_parallel_workers决定的。一般把这个值设置为8~16之间最好(32核物理机的情况),毕竟备库还有可能要提供读查询,不能把CPU都吃光了。
<1>、事务能不能按照轮询的方式分发给各个worker,也就是第一个事务分给worker_1,第二个事务发给worker_2呢?
??不行。因为事务被分发给worker以后,不同的worker就独立执行了。但是,由于CPU的调度策略,很可能第二个事务最终比第一个事务先执行。而如果这时候刚好这两个事务更新的是同一行,也就意味着,同一行上的两个事务,在主库和备库上的执行顺序相反,会导致主备不一致的问题。
<2>、同一个事务的多个更新语句,能不能分给不同的worker来执行呢?
??不行。比如说一个事务更新了表t1和表t2中的各一行,如果这两条更新语句被分到不同worker的话,虽然最终的结果是主备一致的,但如果表t1执行完成的瞬间,备库上有一个查询,就会看到这个事务“更新了一半的结果”,破坏了事务逻辑的原子性。
<3>、coordinator在分发的时候,需要满足以下这两个基本要求:
?①、能造成更新覆盖。这就要求更新同一行的两个事务,必须被分发到同一个worker中。
?②、同一个事务不能被拆开,必须放到同一个worker中。
二、各个版本的并行复制策略:
1、MySQL 5.5版本的并行复制策略:
?<1>、按表分发策略:
??(1)、基本思路:
??如果两个事务更新不同的表,它们就可以并行。因为数据是存储在表里的,所以按表分发,可以保证两个worker不会更新同一行。如果有跨表的事务,需要把两张表放在一起考虑的。
??(2)、
?①、每个worker线程对应一个hash表,用于保存当前正在这个worker的“执行队列”里的事务所涉及的表。hash表的key是“库名.表名”,value是一个数字,表示队列中有多少个事务修改这个表。
?②、在有事务分配给worker时,事务里面涉及的表会被加到对应的hash表中。worker执行完成后,这个表会被从hash表中去掉。
?③、hash_table_1表示,现在worker_1的“待执行事务队列”里,有4个事务涉及到db1.t1表,有1个事务涉及到db2.t2表;hash_table_2表示,现在worker_2中有一个事务会更新到表t3的数据。
??(3)、假设在图中的情况下,coordinator从中转日志中读入一个新事务T,这个事务修改的行涉及到表t1和t3。分配规则为:
?①、由于事务T中涉及修改表t1,而worker_1队列中有事务在修改表t1,事务T和队列中的某个事务要修改同一个表的数据,这种情况事务T和worker_1是冲突的。
?②、按照这个逻辑,顺序判断事务T和每个worker队列的冲突关系,会发现事务T跟worker_2也冲突。
?③、事务T跟多于一个worker冲突,coordinator线程就进入等待。
?④、每个worker继续执行,同时修改hash_table。假设hash_table_2里面涉及到修改表t3的事务先执行完成,就会从hash_table_2中把db1.t3这一项去掉。
?⑤、这样coordinator会发现跟事务T冲突的worker只有worker_1了,因此就把它分配给worker_1。
?⑥、coordinator继续读下一个中转日志,继续分配事务。
??(4)、每个事务在分发的时候,跟所有worker的冲突关系情况:
?①、如果跟所有worker都不冲突,coordinator线程就会把这个事务分配给最空闲的woker;
?②、如果跟多于一个worker冲突,coordinator线程就进入等待状态,直到和这个事务存在冲突关系的worker只剩下1个;
?③、如果只跟一个worker冲突,coordinator线程就会把这个事务分配给这个存在冲突关系的worker。
??这个按表分发的方案,在多个表负载均匀的场景里应用效果很好。但是,如果碰到热点表,比如所有的更新事务都会涉及到某一个表的时候,所有事务都会被分配到同一个worker中,就变成单线程复制了。
?<2>、按行分发策略:
??(1)、基本思路:
??如果两个事务没有更新相同的行,它们在备库上可以并行执行。为每个worker,分配一个hash表。显然,这个模式要求binlog格式必须是row。
??(2)、按行分发的策略有两个问题:
?①、耗费内存。
?②、耗费CPU。
??相比于按表并行分发策略,按行并行策略在决定线程分发的时候,需要消耗更多的计算资源。
?<3>、分发策略的约束条件:
?①、要能够从binlog里面解析出表名、主键值和唯一索引的值。也就是说,主库的binlog格式必须是row;
?②、表必须有主键;
?③、不能有外键。表上如果有外键,级联更新的行不会记录在binlog中,这样冲突检测就不准确。
?④、事务hash表中唯一键key应该是“库名+表名+索引a的名字+a的值”。
2、MySQL 5.6版本的并行复制策略:
?<1>、基本思路:
??支持并行复制,只是支持的粒度是按库并行。用于决定分发策略的hash表里,key就是数据库名。这个策略的并行效果,取决于压力模型。如果在主库上有多个DB,并且各个DB的压力均衡,使用这个策略的效果会很好。
?<2>、相比于按表和按行分发,这个策略有两个优势:
?①、构造hash值的时候很快,只需要库名;而且一个实例上DB数也不会很多,不会出现需要构造100万个项这种情况。
?②、不要求binlog的格式。因为statement格式的binlog也可以很容易拿到库名。
??如果主库上的表都放在同一个DB里面,这个策略就没有效果了;或者如果不同DB的热点不同,比如一个是业务逻辑库,一个是系统配置库,那也起不到并行的效果。
3、MariaDB的并行复制策略:
?<1>、MariaDB的并行复制策略利用的特性:
?①、能够在同一组里提交的事务,一定不会修改同一行;
?②、主库上可以并行执行的事务,备库上也一定是可以并行执行的。
?<2>、MariaDB的实现方法:
?①、在一组里面一起提交的事务,有一个相同的commit_id,下一组就是commit_id+1;
?②、commit_id直接写到binlog里面;
?③、传到备库应用的时候,相同commit_id的事务分发到多个worker执行;
?④、这一组全部执行完成后,coordinator再去取下一批。
?<3>、MariaDB的缺点:
?①、在备库上执行的时候,要等第一组事务完全执行完成后,第二组事务才能开始执行,这样系统的吞吐量就不够。
?②、这个方案很容易被大事务拖后腿。假设第一组事务中trx2是一个超大事务,那么在备库应用的时候,trx1和trx3执行完成后,就只能等trx2完全执行完成,下一组才能开始执行。这段时间,只有一个worker线程在工作,是对资源的浪费。
4、MySQL 5.7的并行复制策略:
?<1>、由参数slave-parallel-type来控制并行复制策略:
?①、配置为DATABASE,表示使用MySQL 5.6版本的按库并行策略;
?②、配置为 LOGICAL_CLOCK,表示的就是类似MariaDB的策略。不过,MySQL 5.7这个策略,针对并行度做了优化。
?<2>、MySQL 5.7并行复制策略的思想:
?①、同时处于prepare状态的事务,在备库执行时是可以并行的;
?②、处于prepare状态的事务,与处于commit状态的事务之间,在备库执行时也是可以并行的。
5、MySQL 5.7.22的并行复制策略:
?<1>、MySQL 5.7.22版本里,MySQL增加了一个新的并行复制策略,基于WRITESET的并行复制。
?<2>、参数binlog-transaction-dependency-tracking,用来控制是否启用这个新策略,参数的可选值有:
?①、COMMIT_ORDER,表示的就是前面介绍的,根据同时进入prepare和commit来判断是否可以并行的策略。
?②、WRITESET,表示的是对于事务涉及更新的每一行,计算出这一行的hash值,组成集合writeset。如果两个事务没有操作相同的行,也就是说它们的writeset没有交集,就可以并行。这个hash值是通过“库名+表名+索引名+值”计算出来的。如果一个表上除了有主键索引外,还有其他唯一索引,那么对于每个唯一索引,insert语句对应的writeset就要多增加一个hash值。
?③、WRITESET_SESSION,是在WRITESET的基础上多了一个约束,即在主库上同一个线程先后执行的两个事务,在备库执行的时候,要保证相同的先后顺序。