对于一个压力持续比较高的主库来说,备库执行日志的速度持续低于主库生成日志的速度,备库可能永远都追不上主库的节奏。
在主库上,影响并发度的原因是各种锁。InnoDB引擎支持行锁,除了所有并发事务都在更新同一行这种极端场景外,它对业务并发度的支持很友好。
日志在备库上的执行,sql_thread执行中转日志relay log更新数据的逻辑,如果是用单线程的话,就会导致备库应用日志不够快,造成主备延迟。
多线程复制机制,把只有一个线程的sql_thread,拆成多个线程。coordinator代替原来的sql_thread,但不再直接更新数据,只负责读取中转日志和分发事务。真正更新日志的,变成worker线程。work线程的个数由参数slave_parallel_workers决定。1/4-1/2的CPU核数最佳,其他的供查询的使用。
事务不能按照轮询的方式分发给各个worker。事务分发给worker以后,不同的worker独立执行,但是CPU的调度策略无法保证更新同一行的两个事务按照主库传过来的顺序执行。
同一个事务的多个更新语句,不能分给不同的worker执行。一个事务更新了表t1和t2中各一行,如果两条更新语句被分到不同的worker,最终的结果是主备一致的,但是表t1执行完成的瞬间,备库上有一个查询,会看到这个事务更新了一半的结果,破坏了事务逻辑的隔离性。
coordinator分发要求:
- 不能造成更新覆盖。要求更新同一行的两个事务,必须被分到同一个worker中。
- 同一个事务不能被拆开,必须放到同一个worker中。
MySQL5.5版本并行复制策略
官方MySQL5.5版本不支持并行复制。
按表分发策略
如果两个事务更新不同的表,它们就可以并行。数据是存储在表里的,按表分发可以保证两个worker不会更新同一行。如果有跨表的事务,要把两张表放在一起考虑。
每个worker线程对应一个hash表,用于保存当前正在这个worker的“执行队列”里的事务所涉及的表。hash表的key是”库名.表名”,value是一个数字,表示队列中有多少个事务修改这个表。
有事务分配给worker时,事务里面涉及的表会被加到对应的hash表中。worker执行完成后,这个表会被从hash表中去掉。
每个事务在分发的时候,跟所有worker中的待执行事务队列(一个worker可能分配了多个事务,每个事务可能对应多张表,分配前挨个判断)的冲突关系包括三种情况:
- 如果跟所有worker都不冲突,coordinator线程就会把这个事务分配给最空闲的worker;
- 如果跟多于一个worker冲突,coordinator线程就进入等待状态,直到和这个事务存在冲突关系的worker只剩下1个;
- 如果只跟一个worker冲突,coordinator线程就会把这个事务分配给这个存在冲突关系的worker。
按表分发的方案,在多个表负载均匀的场景里应用效果很好,但是,碰到热点表,所有事务都会被分配到同一个worker中,变成单线程复制。
按行分发策略
如果两个事务没有更新相同的行,它们在备库上可以并行执行。该模式要求binlog格式必须是row。
为每个worker分配一个hash表。key必须是”库名+表名+唯一键的值”。
“唯一键”除了主键还有唯一索引列,避免两个事务要更新的行的主键值不同,但被分到不同的worker后,唯一索引列的值后先更新导致唯一键冲突。一条语句可能对应多个hash表项,key=hash_func(db+t+唯一索引+唯一索引值),value=n。
在表 t1 上执行 update t1 set a=1 where id=2 语句,在 binlog 里面记录了整行的数据修改前各个字段的值,和修改后各个字段的值。
coordinator 在解析这个语句的 binlog 的时候,这个事务的 hash 表就有三个项(primary主键唯一索引,a普通唯一索引):
- key=hash_func(db1+t1+“PRIMARY”+2), value=2; 这里 value=2 是因为修改前后的行 id 值不变,出现了两次。
- key=hash_func(db1+t1+“a”+2), value=1,表示会影响到这个表 a=2 的行。
- key=hash_func(db1+t1+“a”+1), value=1,表示会影响到这个表 a=1 的行。
相比于按表并行分发策略,按行并行策略在决定线程分发的时候,需要消耗更多的计算资源:
- 要能够从binlog里面解析出表名、主键值、唯一索引的值。主库binlog格式必须是row。
- 表必须有主键。
- 不能有外键。表上如果有外键,级联更新的行不会记录在binlog中,这样冲突检测就不准确。
按行分发问题:
- 耗费内存。一个语句要删除100万行数据,hash表就要记录100万个项。
- 耗费CPU。解析binlog,然后计算hash值,对于大事务,成本很高。
单个事务如果超过行数阈值,就暂时退化为单线程模式:
- coordinator暂时先hold住这个事务;
- 等待所有worker都执行完成,变成空队列;
- coordinator直接执行这个事务;
- 恢复并行模式。
MySQL5.6版本并行复制策略
官方MySQL5.6版本,支持并行复制。
按库分发策略
key是数据库名。适用主库上有多个DB,并且各个DB的压力均衡。
优点:
- 构造hash值的时候很快,只需要库名;一个实例上DB数也不会很多,不会出现需要构造100W个项的情况。
- 不要求binlog的格式。statement格式的binlog可以很容易拿到库名。
问题:
- 主库上的表都放在同一个DB里,按库并行策略没有效果;
- 不同DB的热点不同,如业务逻辑库+系统配置库,没有并行效果。
解决:
创建不同的DB,把相同热度的表均匀分到不同的DB中,强行使用该策略。
MariaDB并行复制策略
redo log组提交(group commit)优化特性:
- 能够在同一组里提交的事务,一定不会修改同一行;
- 主库上可以并行执行的事务,备库上也一定是可以并行执行的。
MariaDB原理:
- 在一组里一起提交的事务,有一个相同的commit_id,下一组就是commit_id+1;
- commit_id直接写到binlog里;
- 传到备库应用的时候,相同commit_id的事务分发到多个worker中执行;
- 这一组全部执行完成后,coordinator再去取下一批。
问题:
主库上,一组事务在commit的时候,下一组事务是同时处于“执行中”状态的。
在备库上执行的时候,要等第一组事务完全执行完成后,第二组事务才能开始执行,系统的吞吐量不够。
容易被大事务拖后腿,同一组的多个事务(多个worker)要等最大的事务完成(只剩一个worker在工作),下一组才能开始执行。
MySQL5.7版本并行复制策略
由参数slave-parallel-type控制并行复制策略:
- 配置为DATABASE,表示使用MySQL5.6版本的按库并行策略;
- 配置为LOGICAL——CLOCK,表示使用类似MariaDB的策略。针对并行度做了优化。
原理:
- 同时处于“执行状态”的所有事务,可能有由于锁冲突而处于锁等待状态的事务,而并不能并行。这些事务在备库上被分配到不同的worker,会出现备库跟主库不一致的情况。
- MariaDB核心:所有处于commit状态的事务可以并行。事务处于commit转态,表示已经通过了锁冲突的检验。
- 根据两阶段提交细化过程,只要能够到达redo log prepare阶段(事务处于开始“提交状态”),就表示事务已经通过锁冲突的检验。
- 同时处于redo log prepare状态的事务,在备库执行时时可以并行的;
- 处于redo log prepare状态的事务,与处于redo log commit状态的事务之间,在备库执行时也是可以并行的。
每个事务都有两个数字表示它在执行提交阶段的时间范围,构成区间(c1,c2)。c1是事务启动的时候,当前系统里最大的commit_id。一个事务提交的时候,才会commit_id+1,+1之前启动的事务拥有相同的commit_id。如果两个事务的区间有交集,就是可以并行的。
主库在写binlog的时候,会在binlog里记录commit_id和sequence_no,来说明事务之间在主库上并行prepare的状态。备库里是通过解析binlog拿到commit_id和sequence_no来决定怎么并发。
binlog组提交的两个参数:
- binlog_group_commit_sync_delay参数,表示延迟多少微妙后才调用fsync;
- binlog_group_commit_sync_no_delay_count参数,表示累计多少次以后才调用fsync。
这两个参数,用于故意拉binlog从write到fsync的时间,以此减少binlog的写盘次数。在并行复制策略里,可以用来制造更多的“同时处于prepare阶段的事务”,增加了备库复制的并行度。
这两个参数,既可以故意让主库提交的慢些,又可以让备库执行的快些。
MySQL5.7.22并行复制策略
基于WRITESET的并行复制。参数binlog-transaction-dependency-tracking控制是否启用该策略(如果同时设置了slave_parallel_type参数,则以该spt参数为准):
- COMMIT_ORDER,表示的是根据同时进入prepare和commit来判断是否可以并行的策略。
- WRITESET,表示的是对于事务涉及更新的每一行,计算出这一行的hash值,组成集合writeset。如果每个事务没有操作相同的行,他们的writeset没有交集,就可以并行。
- WRITESET_SESSION,在writeset的基础上多了一个约束,在主库上同一个线程先后执行的两个事务,在备库执行的时候,要保证相同的先后顺序。
唯一标识hash值是通过“库名+表名+索引名+值”计算出来的。如果一个表上除了有主键索引外,还有其它唯一索引,那么对于每一个唯一索引,insert语句对应的writeset就要多增加一个hash值。(类似5.5的按行分发策略,但有优化):
- writeset是在主库生成后直接写入到binlog里面的,这样在备库执行的时候,不需要解析binlog内容(event里的行数据),节省了很多计算量;
- 不需要把整个事务的binlog都扫一遍才能决定分发到那个worker,更省内存;
- 由于备库的分发策略不依赖于binlog内容,所以binlog是statement格式也是可以的。
对于“表上没主键”和“外键约束”的场景,writeset策略无法并行,也会暂时退化为单线程模型。