MySQL实战45讲学习笔记:第二十六讲

 1、引子

在上一篇文章中,我和你介绍了几种可能致使备库延迟的缘由。你会发现,这些场景里,不管是偶发性的查询压力,仍是备份,对备库延迟的影响通常是分钟级的,并且在备库恢
复正常之后都可以追上来。sql

可是,若是备库执行日志的速度持续低于主库生成日志的速度,那这个延迟就有可能成了小时级别。并且对于一个压力持续比较高的主库来讲,备库极可能永远都追不上主库的节奏。数据库

这就涉及到今天我要给你介绍的话题:备库并行复制能力。bash

2、备库并行复制能力

一、在官方的 5.6 版本以前,MySQL 只支持单线程复制

为了便于你理解,咱们再一块儿看一下第 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,也就是第一个事务分给 worker_1,第二个事务发给 worker_2 呢?

实际上是不行的。由于,事务被分发给 worker 之后,不一样的 worker 就独立执行了。可是,因为 CPU 的调度策略,极可能第二个事务最终比第一个事务先执行。而若是这时候刚
好这两个事务更新的是同一行,也就意味着,同一行上的两个事务,在主库和备库上的执行顺序相反,会致使主备不一致的问题。

接下来,请你再设想一下另一个问题:同一个事务的多个更新语句,能不能分给不一样的worker 来执行呢?

答案是,也不行。

举个例子,一个事务更新了表 t1 和表 t2 中的各一行,若是这两条更新语句被分到不一样 worker 的话,虽然最终的结果是主备一致的,但若是表 t1 执行完成的瞬
间,备库上有一个查询,就会看到这个事务“更新了一半的结果”,破坏了事务逻辑的隔离性。

三、coordinator 在分发的时候,须要知足如下这两个基本要求

1. 不能形成更新覆盖。这就要求更新同一行的两个事务,必须被分发到同一个 worker中。
2. 同一个事务不能被拆开,必须放到同一个 worker 中。各个版本的多线程复制,都遵循了这两条基本原则。接下来,咱们就看看各个版本的并行复制策略。

3、MySQL 5.5 版本的并行复制策略

官方 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. 恢复并行模式。

读到这里,你可能会感到奇怪,这两个策略又没有被合到官方,我为何要介绍这么详细呢?其实,介绍这两个策略的目的是抛砖引玉,方便你理解后面要介绍的社区版本策略。

4、MySQL 5.6 版本的并行复制策略

官方 MySQL5.6 版本,支持了并行复制,只是支持的粒度是按库并行。理解了上面介绍的按表分发策略和按行分发策略,你就理解了,用于决定分发策略的 hash 表里,key 就是
数据库名。

这个策略的并行效果,取决于压力模型。若是在主库上有多个 DB,而且各个 DB 的压力均衡,使用这个策略的效果会很好。

相比于按表和按行分发,这个策略有两个优点:

1. 构造 hash 值的时候很快,只须要库名;并且一个实例上 DB 数也不会不少,不会出现须要构造 100 万个项这种状况。

2. 不要求 binlog 的格式。由于 statement 格式的 binlog 也能够很容易拿到库名

可是,若是你的主库上的表都放在同一个 DB 里面,这个策略就没有效果了;或者若是不一样 DB 的热点不一样,好比一个是业务逻辑库,一个是系统配置库,那也起不到并行的效果。

理论上你能够建立不一样的 DB,把相同热度的表均匀分到这些不一样的 DB 中,强行使用这个策略。不过据我所知,因为须要特意移动数据,这个策略用得并很少。

5、MariaDB 的并行复制策略

在第 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 线程在工做,是对资源的浪费。

不过即便如此,这个策略仍然是一个很漂亮的创新。由于,它对原系统的改造很是少,实现也很优雅。

6、MySQL 5.7 的并行复制策略

在 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 处理备库延迟的时候,能够考虑调整这两个参数值,来达到提高备库复制
并发度的目的。

7、MySQL 5.7.22 的并行复制策略

在 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 策略也是无法并行的,也会暂时退化为单线程模型。

8、小结

在今天这篇文章中,我和你介绍了 MySQL 的各类多线程复制策略。

为何要有多线程复制呢?这是由于单线程复制的能力全面低于多线程复制,对于更新压力较大的主库,备库是可能一直追不上主库的。从现象上看就是,备库上
seconds_behind_master 的值愈来愈大。

在介绍完每一个并行复制策略后,我还和你分享了不一样策略的优缺点:

若是你是 DBA,就须要根据不一样的业务场景,选择不一样的策略;

若是是你业务开发人员,也但愿你能从中获取灵感用到平时的开发工做中。

从这些分析中,你也会发现大事务不只会影响到主库,也是形成备库复制延迟的主要缘由之一。所以,在平时的开发工做中,我建议你尽可能减小大事务操做,把大事务拆成小事务。

官方 MySQL5.7 版本新增的备库并行策略,修改了 binlog 的内容,也就是说 binlog 协议并非向上兼容的,在主备切换、版本升级的时候须要把这个因素也考虑进去。
最后,我给你留下一个思考题吧。

假设一个 MySQL 5.7.22 版本的主库,单线程插入了不少数据,过了 3 个小时后,咱们要给这个主库搭建一个相同版本的备库。

这时候,你为了更快地让备库追上主库,要开并行复制。在 binlog-transaction-dependency-tracking 参数的 COMMIT_ORDER、WRITESET 和 WRITE_SESSION 这三
个取值中,你会选择哪个呢?

你选择的缘由是什么?若是设置另外两个参数,你认为会出现什么现象呢?

你能够把你的答案和分析写在评论区,我会在下一篇文章跟你讨论这个问题。感谢你的收听,也欢迎你把这篇文章分享给更多的朋友一块儿阅读。

9、上节问题

上期的问题是,什么状况下,备库的主备延迟会表现为一个 45 度的线段?评论区有很多同窗的回复都说到了重点:备库的同步在这段时间彻底被堵住了。

产生这种现象典型的场景主要包括两种:

一种是大事务(包括大表 DDL、一个事务操做不少行);
还有一种状况比较隐蔽,就是备库起了一个长事务,好比

begin; 
select * from t limit 1;

而后就不动了。

这时候主库对表 t 作了一个加字段操做,即便这个表很小,这个 DDL 在备库应用的时候也会被堵住,也不能看到这个现象。

一种是大事务(包括大表 DDL、一个事务操做不少行);

还有一种状况比较隐蔽,就是备库起了一个长事务,好比

评论区还有同窗说是否是主库多线程、从库单线程,备库跟不上主库的更新节奏致使的?

今天这篇文章,咱们恰好讲的是并行复制。因此,你知道了,这种状况会致使主备延迟,但不会表现为这种标准的呈 45 度的直线。

评论区留言点赞板:

@易翔 、 @万勇、@老杨同志 等同窗的回复都提到了咱们上面说的场景;

@Max 同窗提了一个很不错的问题。主备关系里面,备库主动链接,以后的binlog 发送是主库主动推送的。之因此这么设计也是为了效率和实时性考虑,毕竟靠备库轮询,会有时间差。

相关文章
相关标签/搜索