之前的文章谈到的事故原因,不论是偶发性的查询压力,还是备份,对备库延迟的影响一般是分钟级的,而且在备库恢复正常以后都能够追上来。
但若备库执行日志的速度持续低于主库生成日志的速度,那该延迟可能小时级别。而且对于一个压力持续较高的主库,备库可能永远都追不上主库节奏了。
这就牵涉本文话题:备库并行复制能力。
主备流程图
图中两个黑色箭头:
一个代表客户端写入主库
并行度高于下一个
另一个代表备库上sql_thread执行中转日志(relay log)
主库上影响并发度的原因就是锁。由于InnoDB引擎支持行锁,除了所有并发事务都在更新同一行(热点行)这种极端场景,它对业务并发度的支持还是可以的。所以,你在性能测试的时候会发现,并发压测线程32就比单线程时,总体吞吐量高。
而日志在备库执行,即图中备库上sql_thread更新数据(DATA)的逻辑。若用单线程,就会导致备库应用日志不够快,造成主备延迟。
在5.6版本前,MySQL只支持单线程复制,由此在主库并发高、TPS高时就会出现严重主备延迟。
MySQL多线程复制的演进史
所有的多线程复制机制,都是要把图中只有一个线程的sql_thread,拆成多个线程,也就是都符合下面的这个模型:
coordinator就是原来的sql_thread, 不过它不再直接更新数据,只负责读取中转日志、分发事务。真正更新日志的,变成了worker线程。而work线程的个数,就是由参数slave_parallel_workers决定。推荐设为8~16之间最好(32核物理机),毕竟备库还可能要提供读查询,不能把CPU占完。
事务能否按轮询分发给各worker?
不行。因为,事务被分发给worker后,不同的worker就独立执行了。但由于CPU的调度策略,可能第二个事务比第一个事务先执行。而这时刚好这俩事务更新同一行,即同一行上的两个事务,在主库和备库上的执行顺序相反,导致主备不一致。
同一个事务的多个更新语句,能否分给不同worker执行?
不行。比如一个事务更新了表t1和表t2中的各一行,若这两条更新语句被分到不同worker,虽然最终结果是主备一致,但若表t1执行完成瞬间,备库有个查询,就会看到这个事务“更新了一半的结果”,破坏了事务逻辑的隔离性。
所以,coordinator在分发的时候,需要满足:
不能造成更新覆盖。这就要求更新同一行的两个事务,必须被分发到同一个worker中
同一个事务不能被拆开,必须放到同一个worker中
MySQL 5.5的并行复制策略
官方MySQL 5.5版本不支持并行复制。但是有人写了:按表分发策略和按行分发策略,以帮助理解MySQL官方版本并行复制策略的迭代。
按表分发策略
若两个事务更新不同表,它们就可以并行。因为数据是存储在表里的,所以按表分发,可以保证两个worker不会更新同一行。
当然,若有跨表事务,还是要把两张表放在一起考虑
- 按表并行复制程模型
每个worker线程对应一个hash表,保存当前正在这个worker的“执行队列”里的事务所涉及的表。hash的
- key:
库名.表名
- value:数字,队列中有多少个事务修改这个表
在有事务分配给worker时,事务里涉及的表会被加到对应的hash表中。worker执行完成后,这个表会被从hash表中去掉。
上图中的hash_table_1表示,现在worker_1的“待执行事务队列”里,有4个事务涉及到db1.t1表,有1个事务涉及到db2.t2表。
假设图中的情况下,coordinator从中转日志读入一个新事务T,该事务修改的行涉及表t1、t3。
现在用事务T的分配流程,来看一下分配规则:
- 由于事务T中涉及修改t1,而worker_1队列中已经有其它事务Tx在修改t1,T和队列中的Tx事务要修改同一个表的数据:T和worker_1冲突
- 按此逻辑,顺序判断T和每个worker队列是否冲突,会发现事务T跟worker_2也冲突
- T跟多于1个的worker冲突,coordinator线程就进入等待
- 每个worker继续执行,同时修改hash_table。假设hash_table_2里面涉及到修改t3的事务执行完成了,就会去掉hash_table_2中的把
db1.t3
- 这样coordinator会发现跟T冲突的worker只有worker_1(不多于1个了),因此就把它分配给worker_1
- coordinator继续读下一个中转日志,继续分配事务
即每个事务在分发时,跟所有worker的冲突关系如下:
- 和所有worker都不冲突
coordinator线程就会把这个事务分配给最空闲的woker; - 和多于1个的worker冲突
coordinator线程就进入等待状态,直到和这个事务存在冲突关系的worker只剩下1个
- 只和一个worker冲突
coordinator线程就会把这个事务分配给这个存在冲突关系的worker。
按表分发方案在多个表负载均均匀场景里使用很好。但若碰到热点表,比如所有更新事务都会涉及到某个表时,所有事务都会被分配到同一worker,就变成单线程复制。