MySQL并行复制已是老生常谈,笔者从2010年开始就着手处理线上这个问题,刚开始两三年也乐此不疲分享,如今再提这个话题原本是不免“炒冷饭”嫌疑。
最近触发再谈这个话题,是由于有些同窗以为“5.7的并行复制终于完全解决了复制并发性问题”, 感受仍是有必要分析一下。你们都说没有银弹,可是又期待银弹。。
既然要说5.7的并行复制,干脆顺手把各个版本的并行复制都说明一下,也好有个对比。即是本次分享的初衷。
【背景】
一句话说完,由于这几年太多这样文章了, 就是MySQL一直以来的备库复制都是单线程apply。
【解决基本思路】
改为多线程复制。
备库有两个线程与复制相关:io_thread 负责从主库拿binlog并写到relaylog, sql_thread 负责读relaylog并执行。
多线程的思路就是把sql_thread 变成分发线程,而后由一组worker_thread来负责执行。
几乎全部的并行复制都是这个思路,有不一样的,即是sql_thread 的分发策略。
而这些策略里面又分红两类:利用传统binlog格式、修改binlog。
使用传统的binlog格式的几类,因为binlog里面的信息就那些,所以只能按照粒度来分,也就是:按库、按表、按行
另外有两个策略是修改了binlog格式的,在binlog里面增长了别的信息,用于体现提交分组。
下面咱们分别介绍几个并行复制的实现。
【5.5】
MySQL官方5.5是不支持并行复制的。可是在阿里的业务须要并行复制的年份,尚未官方版本支持,只好本身实现。并且从兼容性角度说,不修改binlog格式,因此采用的是利用传统binlog格式的改造。
阿里的版本支持两种分发策略:按表和按行。
前情说明,因为MySQLbinlog日志还有用于别的系统的要求,所以阿里的binlog格式都是row----这也给并行复制的实现减小了难度。
按表分发策略:row格式的binlog,每一个DML前面都是有Table_map event的。所以很容易拿到库名/表名。一个简单的思路是,不一样表的更新之间是不须要严格按照顺序的。
所以按照表名hash,hash key是 库名+表名,相同的表的更新放到同一个worker上。这样就保证同一个表的更新顺序,跟主库上是同样的。
应用场景:对于多表更新的场景效果特别好。缺点是反之的,如果热点表更新,则本策略无效。并且因为hash表的维护,性能反而降低。
按行分发策略:row格式的binlog中,也不难拿到主键ID. 有同窗说若是没有主键怎么办,答案是"起开,如今谁还没主键:)"。好吧,正经答案是没有主键就不支持这个策略。
一样的,咱们认为不一样行的更新,能够无序并发的。只要保证同一行的数据更新,在备库上的顺序与主库上的相同便可。
所以按照主键id hash,因此这个hash key更长,必须是 库名+表名+主键id。相同行的更新放到同一个worker上。
须要注意的是,上面的描述看上去都是对单个event的操做,实际上
并不能!由于备库可能接受读,所以事务的原子性是要保证的,也就是说,对于涉及多个更新操做的事务,每次用于决策的不是一个hash key,而是一组。
应用场景:热点表更新。缺点,hash key计算冲突的代价大。尤为是大事务,计算hash key的cpu消耗大,并且耗内存。这须要业务DBA作判断得失。
【5.6】
官方的5.6支持的是按库分发。有了上面的背景,你们就知道,这个feature出来之后,在中国并无什么反响。
可是这个策略也要说也是有优势的:
一、对于能够按表分发的场景,能够经过将表迁到不一样的库,来应用此策略,有可操做性
二、速度更快,由于hash key就一个库名
三、不要求binlog格式,你们知道不管是row仍是statement格式,都是可以轻松获取库名的。
因此并非彻底没有用的。仍是习惯问题。
【MariaDB】
MariaDB的并行复制策略看上去有好几个选项,然而生产上可用的也就是默认值的 CONSERVATIVE。
因为maraiaDB支持多主复制,一个domain_id字段是用来标示事务来源的。若是来自于不一样的主,天然能够并行(这个其实也是通用概念,还得业务DBA本身判断)。
对于同一个主库来的binlog,用commit_id 来决定分组。
想法是这样的:在主库上同时提交的事务设置成相同的commit_id。在备库上apply时,相同的commit_id能够并行执行,由于这意味着这些事务之间是没有行冲突的(不然不可能同时提交)。
这个思路跟最初从单线程改为多线程同样,我的认为是划时代的。
可是也并无解决了全部的问题。这个策略最怕的是,拖后腿事务。
设想一下这个场景,假设某个DB里面正在做大量小更新事务(好比每一个事务更新一行),这样在备库就并行得很欢乐。
而后忽然,在同一个实例,另一个库下,或者同一个库的另一个跟目前的更新无关的表,忽然有一个delte操做删除了10w行。
delete事务在提交的时候,跟当时一块儿提交的事务都算同一个commit_id。假设为N.
以后的小事务更新提交组commit_id为N+1。
到备库apply时,就会发现N这个组里面,其余小事务都执行完了,线程进入空闲状态,可是不能继续执行N+1这个commit_id的事务,由于N里面还有一个大事务没有执行完成,这个咱们认为是拖后腿的。
而基于传统binlog格式的上面三个策略,反而没有这个问题。只要是策略上可以判断不冲突,大事务本身有个线程跑,其余事务继续并行。
【5.7】
MySQL官方5.7版本也是及时跟进,先引入了上述MariaDB的策略。固然从版权安全上,oracle是不会容许直接port代码的。
而后官方5.7的新版本在此之上继续优化。
实际上按组直接分段这个策略略显粗暴。实际上事务提交并非一个点,而是一个阶段。至少咱们能够分红:准备提交、提交中、提交完成。
这三个阶段都是在事务已经完成了主要操做逻辑,进入commit状态了。
同时进入“提交中”状态的算同一个commit_id. 可是实际上,在任意时刻,处于”准备提交”的事务,与“提交中”的事务,也是能够并行的。可是明显他们会被分红两个不一样的commit_id。
这意味着这个策略还有提高并发度的空间。
咱们来看一下两种策略的对比差异。
假设主库有以下面示意图的事务序列。每一个事务提交过程当作两个阶段,prepare ... commit. 分别给不一样的编号。其中commit对应的数字是天然数递增,sequence_no。而prepare是对应的数字是X+1,这个X表示的是当前已经提交完成的sequence_no。
trx1 1…..2
trx2 1………….3
trx3 1…………………….4
trx4 2………………………….5
trx5 3………………………………..6
trx6 3………………………………………………7
trx7 6……………………..8
分析:
在MariaDB的策略里面,并发执行序列以下:
trx1, trx2, trx3 ----group 1
trx4 -----group 2
trx 5, trx6 ----group 3
trx 7 ----group 4
每一个group 执行完成后,下一个group 才能够开始。
彻底执行完成的时间是每一个group的最大事务时间之和,即 trx3 + trx4+trx6+trx7。
所以,若是某个group里面有一个很大的事务,则整个序列的执行时间就会被拖久。
再来看5.7的改进策略:
虽然也是group1先启动,可是在trx1完成后, trx4就能够开始执行;
一样的,trx7能够在trx4执行完成后就开始执行,与trx5和trx6并发。
所以能够说上面这个例子中,备库apply过程彻底达到了主库执行的并发度。
可是对于大事务,好比trx2 commit 很是久的状况,仍然存在拖后腿的问题。
【小结】
咱们看到,就并行复制,有5种策略。
按粒度区分的三个策略,粒度从粗到细是按库、按表、按行。
这三个的对比中,并行度愈来愈大,额外损耗也是。无关大事务不会影响并发度。
按照commit_id 的两个策略,适用范围更广,额外消耗也低。
5.7的改进策略并发性更优。但出现大事务会拖后腿。
另外,很重要的一点,5.7的策略目的是“模拟主库并发”,因此对于主库单线程更新是无加速做用的。而基于冲突的前三个策略,若知足并发条件,会出现备库比主库执行速度快的状况。这种需求在搭备库或者延迟复制的场景中可能触发。
实际上仍是老话,没有万用的策略。
策略的选择取决于应用场景,这是架构师的工做之一。
PS:具体5.7的实现原理可参考咱们团队的@印风 同窗的博客 http://mysqllover.com/?p=1370 (最后一个例子的case也今后摘录)