在上一篇文章中,我和你介绍了几种可能致使备库延迟的缘由。你会发现,这些场景里,不管是偶发性的查询压力,仍是备份,对备库延迟的影响通常是分钟级的,并且在备库恢
复正常之后都可以追上来。sql
可是,若是备库执行日志的速度持续低于主库生成日志的速度,那这个延迟就有可能成了小时级别。并且对于一个压力持续比较高的主库来讲,备库极可能永远都追不上主库的节奏。数据库
这就涉及到今天我要给你介绍的话题:备库并行复制能力。bash
为了便于你理解,咱们再一块儿看一下第 24 篇文章《MySQL 是怎么保证主备一致的?》的主备流程图。session
图 1 主备流程图数据结构
谈到主备的并行复制能力,咱们要关注的是图中黑色的两个箭头。一个箭头表明了客户端写入主库,另外一箭头表明的是备库上 sql_thread 执行中转日志(relay log)。若是用箭
头的粗细来表明并行度的话,那么真实状况就如图 1 所示,第一个箭头要明显粗于第二个箭头。多线程
在主库上,影响并发度的缘由就是各类锁了。因为 InnoDB 引擎支持行锁,除了全部并发事务都在更新同一行(热点行)这种极端场景外,它对业务并发度的支持仍是很友好的。
因此,你在性能测试的时候会发现,并发压测线程 32 就比单线程时,整体吞吐量高。而日志在备库上的执行,就是图中备库上 sql_thread 更新数据 (DATA) 的逻辑。若是是用
单线程的话,就会致使备库应用日志不够快,形成主备延迟。并发
在官方的 5.6 版本以前,MySQL 只支持单线程复制,由此在主库并发高、TPS 高时就会出现严重的主备延迟问题。性能
从单线程复制到最新版本的多线程复制,中间的演化经历了好几个版本。接下来,我就跟你说说 MySQL 多线程复制的演进过程。测试
其实说到底,全部的多线程复制机制,都是要把图 1 中只有一个线程的 sql_thread,拆成多个线程,也就是都符合下面的这个模型:优化
图 2 多线程模型
图 2 中,coordinator 就是原来的 sql_thread, 不过如今它再也不直接更新数据了,只负责读取中转日志和分发事务。真正更新日志的,变成了 worker 线程。而 work 线程的个
数,就是由参数 slave_parallel_workers 决定的。根据个人经验,把这个值设置为 8~16之间最好(32 核物理机的状况),毕竟备库还有可能要提供读查询,不能把 CPU 都吃光了。
接下来,你须要先思考一个问题:事务能不能按照轮询的方式分发给各个 worker,也就是第一个事务分给 worker_1,第二个事务发给 worker_2 呢?
实际上是不行的。由于,事务被分发给 worker 之后,不一样的 worker 就独立执行了。可是,因为 CPU 的调度策略,极可能第二个事务最终比第一个事务先执行。而若是这时候刚
好这两个事务更新的是同一行,也就意味着,同一行上的两个事务,在主库和备库上的执行顺序相反,会致使主备不一致的问题。
接下来,你须要先思考一个问题:事务能不能按照轮询的方式分发给各个 worker,也就是第一个事务分给 worker_1,第二个事务发给 worker_2 呢?
实际上是不行的。由于,事务被分发给 worker 之后,不一样的 worker 就独立执行了。可是,因为 CPU 的调度策略,极可能第二个事务最终比第一个事务先执行。而若是这时候刚
好这两个事务更新的是同一行,也就意味着,同一行上的两个事务,在主库和备库上的执行顺序相反,会致使主备不一致的问题。
接下来,请你再设想一下另一个问题:同一个事务的多个更新语句,能不能分给不一样的worker 来执行呢?
答案是,也不行。
举个例子,一个事务更新了表 t1 和表 t2 中的各一行,若是这两条更新语句被分到不一样 worker 的话,虽然最终的结果是主备一致的,但若是表 t1 执行完成的瞬
间,备库上有一个查询,就会看到这个事务“更新了一半的结果”,破坏了事务逻辑的隔离性。
1. 不能形成更新覆盖。这就要求更新同一行的两个事务,必须被分发到同一个 worker中。
2. 同一个事务不能被拆开,必须放到同一个 worker 中。各个版本的多线程复制,都遵循了这两条基本原则。接下来,咱们就看看各个版本的并行复制策略。
官方 MySQL 5.5 版本是不支持并行复制的。可是,在 2012 年的时候,我本身服务的业务出现了严重的主备延迟,缘由就是备库只有单线程复制。而后,我就前后写了两个版本
的并行策略。
这里,我给你介绍一下这两个版本的并行策略,即按表分发策略和按行分发策略,以帮助你理解 MySQL 官方版本并行复制策略的迭代。
按表分发事务的基本思路是,若是两个事务更新不一样的表,它们就能够并行。由于数据是存储在表里的,因此按表分发,能够保证两个 worker 不会更新同一行。
固然,若是有跨表的事务,仍是要把两张表放在一块儿考虑的。如图 3 所示,就是按表分发的规则。
图 3 按表并行复制程模型
能够看到,每一个 worker 线程对应一个 hash 表,用于保存当前正在这个 worker 的“执行队列”里的事务所涉及的表。hash 表的 key 是“库名. 表名”,value 是一个数字,表示
队列中有多少个事务修改这个表。
在有事务分配给 worker 时,事务里面涉及的表会被加到对应的 hash 表中。worker 执行完成后,这个表会被从 hash 表中去掉。
图 3 中,hash_table_1 表示,如今 worker_1 的“待执行事务队列”里,有 4 个事务涉及到 db1.t1 表,有 1 个事务涉及到 db2.t2 表;hash_table_2 表示,如今 worker_2 中
有一个事务会更新到表 t3 的数据。
假设在图中的状况下,coordinator 从中转日志中读入一个新事务 T,这个事务修改的行涉及到表 t1 和 t3。
如今咱们用事务 T 的分配流程,来看一下分配规则。
1. 因为事务 T 中涉及修改表 t1,而 worker_1 队列中有事务在修改表 t1,事务 T 和队列中的某个事务要修改同一个表的数据,这种状况咱们说事务 T 和 worker_1 是冲突的。
2. 按照这个逻辑,顺序判断事务 T 和每一个 worker 队列的冲突关系,会发现事务 T 跟worker_2 也冲突。
3. 事务 T 跟多于一个 worker 冲突,coordinator 线程就进入等待。
4. 每一个 worker 继续执行,同时修改 hash_table。假设 hash_table_2 里面涉及到修改表t3 的事务先执行完成,就会从 hash_table_2 中把 db1.t3 这一项去掉。
5. 这样 coordinator 会发现跟事务 T 冲突的 worker 只有 worker_1 了,所以就把它分配给 worker_1。
6. coordinator 继续读下一个中转日志,继续分配事务。
也就是说,每一个事务在分发的时候,跟全部 worker 的冲突关系包括如下三种状况:
1. 若是跟全部 worker 都不冲突,coordinator 线程就会把这个事务分配给最空闲的woker;
2. 若是跟多于一个 worker 冲突,coordinator 线程就进入等待状态,直到和这个事务存在冲突关系的 worker 只剩下 1 个;
3. 若是只跟一个 worker 冲突,coordinator 线程就会把这个事务分配给这个存在冲突关系的 worker。
这个按表分发的方案,在多个表负载均匀的场景里应用效果很好。可是,若是碰到热点表,好比全部的更新事务都会涉及到某一个表的时候,全部事务都会被分配到同一个
worker 中,就变成单线程复制了。
要解决热点表的并行复制问题,就须要一个按行并行复制的方案。按行复制的核心思路是:若是两个事务没有更新相同的行,它们在备库上能够并行执行。显然,这个模式要求
binlog 格式必须是 row。
这时候,咱们判断一个事务 T 和 worker 是否冲突,用的就规则就不是“修改同一个表”,而是“修改同一行”。
按行复制和按表复制的数据结构差很少,也是为每一个 worker,分配一个 hash 表。只是要实现按行分发,这时候的 key,就必须是“库名 + 表名 + 惟一键的值”。
可是,这个“惟一键”只有主键 id 仍是不够的,咱们还须要考虑下面这种场景,表 t1 中除了主键,还有惟一索引 a:
CREATE TABLE `t1` ( `id` int(11) NOT NULL, `a` int(11) DEFAULT NULL, `b` int(11) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `a` (`a`) ) ENGINE=InnoDB; insert into t1 values(1,1,1),(2,2,2),(3,3,3),(4,4,4),(5,5,5);
假设,接下来咱们要在主库执行这两个事务:
图 4 惟一键冲突示例
能够看到,这两个事务要更新的行的主键值不一样,可是若是它们被分到不一样的 worker,就有可能 session B 的语句先执行。这时候 id=1 的行的 a 的值仍是 1,就会报惟一键冲突。
所以,基于行的策略,事务 hash 表中还须要考虑惟一键,即 key 应该是“库名 + 表名 +索引 a 的名字 +a 的值”。
好比,在上面这个例子中,我要在表 t1 上执行 update t1 set a=1 where id=2 语句,在binlog 里面记录了整行的数据修改前各个字段的值,和修改后各个字段的值。
所以,coordinator 在解析这个语句的 binlog 的时候,这个事务的 hash 表就有三个项:
1. key=hash_func(db1+t1+“PRIMARY”+2), value=2; 这里 value=2 是由于修改先后的行 id 值不变,出现了两次。
2. key=hash_func(db1+t1+“a”+2), value=1,表示会影响到这个表 a=2 的行。
3. key=hash_func(db1+t1+“a”+1), value=1,表示会影响到这个表 a=1 的行。
可见,相比于按表并行分发策略,按行并行策略在决定线程分发的时候,须要消耗更多的
计算资源。你可能也发现了,这两个方案其实都有一些约束条件:
1. 要可以从 binlog 里面解析出表名、主键值和惟一索引的值。也就是说,主库的 binlog格式必须是 row;
2. 表必须有主键;
3. 不能有外键。表上若是有外键,级联更新的行不会记录在 binlog 中,这样冲突检测就不许确。
但,好在这三条约束规则,原本就是 DBA 以前要求业务开发人员必须遵照的线上使用规范,因此这两个并行复制策略在应用上也没有碰到什么麻烦。
对比按表分发和按行分发这两个方案的话,按行分发策略的并行度更高。不过,若是是要操做不少行的大事务的话,按行分发的策略有两个问题:
1. 耗费内存。好比一个语句要删除 100 万行数据,这时候 hash 表就要记录 100 万个项。
2. 耗费 CPU。解析 binlog,而后计算 hash 值,对于大事务,这个成本仍是很高的。
因此,我在实现这个策略的时候会设置一个阈值,单个事务若是超过设置的行数阈值(好比,若是单个事务更新的行数超过 10 万行),就暂时退化为单线程模式,退化过程的逻
辑大概是这样的:
1. coordinator 暂时先 hold 住这个事务;
2. 等待全部 worker 都执行完成,变成空队列;
3. coordinator 直接执行这个事务;
4. 恢复并行模式。
读到这里,你可能会感到奇怪,这两个策略又没有被合到官方,我为何要介绍这么详细呢?其实,介绍这两个策略的目的是抛砖引玉,方便你理解后面要介绍的社区版本策略。
官方 MySQL5.6 版本,支持了并行复制,只是支持的粒度是按库并行。理解了上面介绍的按表分发策略和按行分发策略,你就理解了,用于决定分发策略的 hash 表里,key 就是
数据库名。
这个策略的并行效果,取决于压力模型。若是在主库上有多个 DB,而且各个 DB 的压力均衡,使用这个策略的效果会很好。
相比于按表和按行分发,这个策略有两个优点:
1. 构造 hash 值的时候很快,只须要库名;并且一个实例上 DB 数也不会不少,不会出现须要构造 100 万个项这种状况。
2. 不要求 binlog 的格式。由于 statement 格式的 binlog 也能够很容易拿到库名
可是,若是你的主库上的表都放在同一个 DB 里面,这个策略就没有效果了;或者若是不一样 DB 的热点不一样,好比一个是业务逻辑库,一个是系统配置库,那也起不到并行的效果。
理论上你能够建立不一样的 DB,把相同热度的表均匀分到这些不一样的 DB 中,强行使用这个策略。不过据我所知,因为须要特意移动数据,这个策略用得并很少。
在第 23 篇文章中,我给你介绍了 redo log 组提交 (group commit) 优化, 而 MariaDB的并行复制策略利用的就是这个特性:
1. 可以在同一组里提交的事务,必定不会修改同一行;
2. 主库上能够并行执行的事务,备库上也必定是能够并行执行的。
在实现上,MariaDB 是这么作的:
1. 在一组里面一块儿提交的事务,有一个相同的 commit_id,下一组就是 commit_id+1;
2. commit_id 直接写到 binlog 里面;
3. 传到备库应用的时候,相同 commit_id 的事务分发到多个 worker 执行;
4. 这一组所有执行完成后,coordinator 再去取下一批。
当时,这个策略出来的时候是至关惊艳的。由于,以前业界的思路都是在“分析 binlog,并拆分到 worker”上。而 MariaDB 的这个策略,目标是“模拟主库的并行模式”。
可是,这个策略有一个问题,它并无实现“真正的模拟主库并发度”这个目标。在主库上,一组事务在 commit 的时候,下一组事务是同时处于“执行中”状态的。
如图 5 所示,假设了三组事务在主库的执行状况,你能够看到在 trx一、trx2 和 trx3 提交的时候,trx四、trx5 和 trx6 是在执行的。这样,在第一组事务提交完成的时候,下一组事
务很快就会进入 commit 状态。
图 5 主库并行事务
而按照 MariaDB 的并行复制策略,备库上的执行效果如图 6 所示。
图 6 MariaDB 并行复制,备库并行效果
另外,这个方案很容易被大事务拖后腿。假设 trx2 是一个超大事务,那么在备库应用的时候,trx1 和 trx3 执行完成后,就只能等 trx2 彻底执行完成,下一组才能开始执行。这段
时间,只有一个 worker 线程在工做,是对资源的浪费。
不过即便如此,这个策略仍然是一个很漂亮的创新。由于,它对原系统的改造很是少,实现也很优雅。
在 MariaDB 并行复制实现以后,官方的 MySQL5.7 版本也提供了相似的功能,由参数slave-parallel-type 来控制并行复制策略:
1. 配置为 DATABASE,表示使用 MySQL 5.6 版本的按库并行策略;
2. 配置为 LOGICAL_CLOCK,表示的就是相似 MariaDB 的策略。不过,MySQL 5.7 这个策略,针对并行度作了优化。这个优化的思路也颇有趣儿。
你能够先考虑这样一个问题:同时处于“执行状态”的全部事务,是否是能够并行?
答案是,不能。
由于,这里面可能有因为锁冲突而处于锁等待状态的事务。若是这些事务在备库上被分配到不一样的 worker,就会出现备库跟主库不一致的状况。
而上面提到的 MariaDB 这个策略的核心,是“全部处于 commit”状态的事务能够并行。事务处于 commit 状态,表示已经经过了锁冲突的检验了。
这时候,你能够再回顾一下两阶段提交,我把前面第 23 篇文章中介绍过的两阶段提交过程图贴过来。
图 7 两阶段提交细化过程图
其实,不用等到 commit 阶段,只要可以到达 redo log prepare 阶段,就表示事务已经经过锁冲突的检验了。
所以,MySQL 5.7 并行复制策略的思想是:
1. 同时处于 prepare 状态的事务,在备库执行时是能够并行的;
2. 处于 prepare 状态的事务,与处于 commit 状态的事务之间,在备库执行时也是能够并行的。
我在第 23 篇文章,讲 binlog 的组提交的时候,介绍过两个参数:
1. binlog_group_commit_sync_delay 参数,表示延迟多少微秒后才调用 fsync;
2. binlog_group_commit_sync_no_delay_count 参数,表示累积多少次之后才调用fsync。
这两个参数是用于故意拉长 binlog 从 write 到 fsync 的时间,以此减小 binlog 的写盘次数。在 MySQL 5.7 的并行复制策略里,它们能够用来制造更多的“同时处于 prepare 阶
段的事务”。这样就增长了备库复制的并行度。
也就是说,这两个参数,既能够“故意”让主库提交得慢些,又可让备库执行得快些。在 MySQL 5.7 处理备库延迟的时候,能够考虑调整这两个参数值,来达到提高备库复制
并发度的目的。
在 2018 年 4 月份发布的 MySQL 5.7.22 版本里,MySQL 增长了一个新的并行复制策略,基于 WRITESET 的并行复制。
相应地,新增了一个参数 binlog-transaction-dependency-tracking,用来控制是否启用这个新策略。这个参数的可选值有如下三种。
1. COMMIT_ORDER,表示的就是前面介绍的,根据同时进入 prepare 和 commit 来判断是否能够并行的策略。
2. WRITESET,表示的是对于事务涉及更新的每一行,计算出这一行的 hash 值,组成集合 writeset。若是两个事务没有操做相同的行,也就是说它们的 writeset 没有交集,就能够并行。
3. WRITESET_SESSION,是在 WRITESET 的基础上多了一个约束,即在主库上同一个线程前后执行的两个事务,在备库执行的时候,要保证相同的前后顺序。
固然为了惟一标识,这个 hash 值是经过“库名 + 表名 + 索引名 + 值”计算出来的。若是一个表上除了有主键索引外,还有其余惟一索引,那么对于每一个惟一索引,insert 语句
对应的 writeset 就要多增长一个 hash 值。
你可能看出来了,这跟咱们前面介绍的基于 MySQL 5.5 版本的按行分发的策略是差很少的。不过,MySQL 官方的这个实现仍是有很大的优点:
1. writeset 是在主库生成后直接写入到 binlog 里面的,这样在备库执行的时候,不须要解析 binlog 内容(event 里的行数据),节省了不少计算量;
2. 不须要把整个事务的 binlog 都扫一遍才能决定分发到哪一个 worker,更省内存;
3. 因为备库的分发策略不依赖于 binlog 内容,因此 binlog 是 statement 格式也是能够的。
所以,MySQL 5.7.22 的并行复制策略在通用性上仍是有保证的。
固然,对于“表上没主键”和“外键约束”的场景,WRITESET 策略也是无法并行的,也会暂时退化为单线程模型。
在今天这篇文章中,我和你介绍了 MySQL 的各类多线程复制策略。
为何要有多线程复制呢?这是由于单线程复制的能力全面低于多线程复制,对于更新压力较大的主库,备库是可能一直追不上主库的。从现象上看就是,备库上
seconds_behind_master 的值愈来愈大。
在介绍完每一个并行复制策略后,我还和你分享了不一样策略的优缺点:
若是你是 DBA,就须要根据不一样的业务场景,选择不一样的策略;
若是是你业务开发人员,也但愿你能从中获取灵感用到平时的开发工做中。
从这些分析中,你也会发现大事务不只会影响到主库,也是形成备库复制延迟的主要缘由之一。所以,在平时的开发工做中,我建议你尽可能减小大事务操做,把大事务拆成小事务。
官方 MySQL5.7 版本新增的备库并行策略,修改了 binlog 的内容,也就是说 binlog 协议并非向上兼容的,在主备切换、版本升级的时候须要把这个因素也考虑进去。
最后,我给你留下一个思考题吧。
假设一个 MySQL 5.7.22 版本的主库,单线程插入了不少数据,过了 3 个小时后,咱们要给这个主库搭建一个相同版本的备库。
这时候,你为了更快地让备库追上主库,要开并行复制。在 binlog-transaction-dependency-tracking 参数的 COMMIT_ORDER、WRITESET 和 WRITE_SESSION 这三
个取值中,你会选择哪个呢?
你选择的缘由是什么?若是设置另外两个参数,你认为会出现什么现象呢?
你能够把你的答案和分析写在评论区,我会在下一篇文章跟你讨论这个问题。感谢你的收听,也欢迎你把这篇文章分享给更多的朋友一块儿阅读。
上期的问题是,什么状况下,备库的主备延迟会表现为一个 45 度的线段?评论区有很多同窗的回复都说到了重点:备库的同步在这段时间彻底被堵住了。
产生这种现象典型的场景主要包括两种:
一种是大事务(包括大表 DDL、一个事务操做不少行);
还有一种状况比较隐蔽,就是备库起了一个长事务,好比
begin; select * from t limit 1;
而后就不动了。
这时候主库对表 t 作了一个加字段操做,即便这个表很小,这个 DDL 在备库应用的时候也会被堵住,也不能看到这个现象。
一种是大事务(包括大表 DDL、一个事务操做不少行);
还有一种状况比较隐蔽,就是备库起了一个长事务,好比
评论区还有同窗说是否是主库多线程、从库单线程,备库跟不上主库的更新节奏致使的?
今天这篇文章,咱们恰好讲的是并行复制。因此,你知道了,这种状况会致使主备延迟,但不会表现为这种标准的呈 45 度的直线。
@易翔 、 @万勇、@老杨同志 等同窗的回复都提到了咱们上面说的场景;
@Max 同窗提了一个很不错的问题。主备关系里面,备库主动链接,以后的binlog 发送是主库主动推送的。之因此这么设计也是为了效率和实时性考虑,毕竟靠备库轮询,会有时间差。