MySQL 26 备库为什么会延迟好几个小时

上一篇文章介绍的场景中,对备库延迟的影响一般是分钟级的。但如果备库执行日志的速度持续低于主库生成日志的速度,那这个延迟就可能成了小时级别。这就涉及今天介绍的话题:备库并行复制能力。

主备流程图:

主备的并行复制能力主要是上图的两个黑色箭头,一个箭头代表客户端写入主库,另一个箭头代表备库上sql_thread执行中转日志。

主库上影响并发度的原因是各种锁,由于InnoDB支持行锁,除了所有并发事务都在更新同一行这种极端场景外,对业务并发度的支持总体良好。备库上影响并发度是sql_thread更新数据的逻辑,在单线程的情况下会导致备库应用日志不够快,造成主备延迟。

从单线程复制到最新版本的多线程复制,中间的演化经历了好几个版本。多线程复制实际上就是把sql_thread拆成多个线程:

coordinator就是原来的sql_thread,不过现在不再直接更新数据,只负责读取中转日志和分发事务,真正更新日志的变成了worker线程,线程个数由参数slave_paraller_workers决定。

coordinator在分发时需要满足两个基本要求:

  • 更新同一行的两个事务,必须被分发到同一个worker中。不然由于CPU调度策略,后面的事务肯定比前面的事务先执行,会导致主备不一致;

  • 同一个事务不能被拆开,必须放到同一个worker中。不然比如一个事务更新了表t1和t2中各一行,如果表t1执行完的瞬间备库上有一个查询,就会看到事务更新一半的结果,破坏了事务逻辑的隔离性。

各版本的多线程复制都遵循了这两条基本原则。

MySQL 5.5版本的并行复制策略

官方5.5版本不支持并行复制,这里主要介绍两种并行策略。

按表分发策略

如果两个事务更新不同的表,就可以并行。如果有跨表的事务,还是需要把两张表放在一起考虑:

可以看到,每个worker线程对应一个Hash表,用于保存当前正在这个worker的执行队列里的事务所涉及的表。Hash表的key是"库名.表名",value是一个数字,表示队列中有多少个事务修改这个表。当有事务分配给worker,事务里面涉及的表会被加到对应的Hash表,worker执行完后该表会被从Hash表去掉。

图中的hash_table_1表示,现在worker_1的"待执行事务队列"中,有4个事务涉及db1.t1表,有1个事务涉及db2.t2表;hash_table_2表示,现在worker_2中有一个事务涉及到db1.t2表。

假设此时coordinator从中转日志读入一个新事务T,T修改的行涉及到表t1和t3,那么分配流程为:

  • 由于事务T涉及修改表t1,而worker_1队列有事务在修改表t1,事务T和队列中的某个事务要修改同一个表的数据,事务T和worker_1是冲突的;

  • 因此,T和worker_2也冲突;

  • 事务T和多于一个worker冲突,coordinator线程进入等待;

  • 每个worker继续执行,假设hash_table_2中涉及修改t3的事务先执行完成,就会把db1.t3这一项去掉;

  • coordinator发现跟事务T冲突的worker只有worker_1,会把它分配给worker_1;

  • coordinator继续读下一个中转日志,继续分配事务。

即事务分发时跟所有worker的冲突关系包括三种情况:

  • 如果和所有worker都不冲突,coordinator线程会把这个事务分配给最空闲的worker;

  • 如果和多于一个worker冲突,coordinator线程就进入等待状态,直到和这个事务存在冲突关系的worker只剩下1个;

  • 如果只和一个worker冲突,coordinator线程会把这个事务分配给这个存在冲突关系的worker。

按表分发策略在多个表负载均衡的场景里效果很好,但如果碰到所有更新事务都涉及到某个表的时候,所有事务都会被分配到同一个worker中,就退化成单线程复制。

按行分发策略

要解决热点表的并行复制问题就需要一个按行并行复制的方案,其核心思路是:如果两个事务没有更新相同的行,它们在备库上可以并行执行。

这时判断一个事务T和worker是否冲突,用的规则就从"修改同一表"变成了"修改同一行"。

按行复制的数据结构也类似,是为每个worker分配一个hash表,其中key是"库名+表名+唯一键的值",比如对于表和流程:

sql 复制代码
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);

那么对于update t1 set a=1 where id=2的语句,事务hash表就有3个项:

  • key=hash_func(db1+t1+"PRIMARY"+2), value=2value=2是因为修改前后的id值不变,出现了两次;

  • key=hash_func(db1+t1+"a"+2), value=1,表示会影响到这个表a=2的行;

  • key=hash_func(db1+t1+"a"+1), value=1,表示会影响到这个表a=1的行;

可见,相比于按表并行分发策略,按行并行策略在决定线程分发的时候,需要消耗更多的计算资源。

按行分发策略的并行度更高,但是如果是要操作很多行的大事务,该策略有两个问题:

  • 耗费内存,比如语句要删除100万行数据,这时候hash表就要记录100万个项;

  • 耗费CPU,解析binlog然后计算hash值对于大事务来说成本还是很高的。

因此,这时候一般会设置一个阈值,单个事务如果超过设置的行数阈值,就暂时退化为单线程模式,退化过程的逻辑:

  • coordinator暂时先hold住这个事务;

  • 等待所有worker都执行完成变成空队列;

  • coordinator直接执行这个事务;

  • 恢复并行。

这两个策略没有被合到官方,其目的主要是抛砖引玉,方便理解后面介绍的社区版本策略。

MySQL 5.6版本的并行复制策略

官方5.6版本支持了并行复制,支持的粒度是按库并行,相当于上面用于决定分发策略hash表里,key是数据库名。

该策略的并行效果取决于压力模型,如果主库上有多个DB,且各个DB的压力均衡,使用这个策略的效果很好。相比于按表和按行,该策略有两个优势:

  • 构造hash值很快,只需要库名;

  • 不要求binlog的格式,statement格式的binlog也可以很容易拿到库名。

但如果主库的表都放在同一个DB里,或不同DB的热点不同,比如一个业务逻辑库一个系统配置库,那就起不到并行效果。

MariaDB的并行复制策略

MariaDB并行复制策略利用的就是redo log组提交优化的特性:

  • 能够在同一组里提交的事务,一定不会修改同一行;

  • 主库上可以并行执行的事务,备库上也一定是可以并行执行的。

其实现:

  • 在一组里一起提交的事务,有一个相同的commit_id,下一组就是commit_id+1;

  • commit_id直接写到binlog;

  • 传到备库应用时,相同commit_id的事务会分发到多个worker执行;

  • 这一组全部执行完成后,coordinator再取下一批。

该策略的问题是,并没有真正实现模拟主库并发度的目标。假如有三组事务,那么主库执行情况为:

而备库执行情况为:

可以看到,在备库上执行的时候,要等第一组事务完全执行完成后,第二组事务才能开始执行,这样系统的吞吐量就不够。

此外该方案还容易被大事务拖后腿,假设trx2是大事务,那么即使trx1和trx3完成,也需要等待trx2完成后才能开始执行下一组。

MySQL 5.7版本的并行复制策略

5.7版本提供了类似MariaDB的功能,由参数slave-parallel-type来控制并行复制策略:

  • 配置为DATABASE,表示使用MySQL 5.6版本的按库并行策略;

  • 配置为LOGICAL_CLOCK,表示的是类似MariaDB的策略,不过针对并行度做了优化。

MariaDB的核心是"所有处于commit"状态的事务可以并行,但同时处于"执行状态"的所有事务是不能并行的,因为事务处于commit状态表示已经通过锁冲突的检验。但在两阶段提交过程中,实际上只要能达到redo log prepare,就表示事务已经通过锁冲突检验。

因此5.7并行复制策略思想是:

  • 同时处于prepare状态的事务,在备库执行时是可以并行的;

  • 处于prepare状态的事务,与处于commit状态的事务之间,在备库执行时也是可以并行的。

MySQL 5.7.22的并行复制策略

5.7.22版本增加了一个新的策略------基于WRITESET的并行复制。新增参数binlog-transaction-dependency-tracking来控制是否启用新策略。参数的值有三种:

  • COMMIT_ORDER,表示根据同时进入prepare和commit来判断是否可以并行;

  • WRITESET,表示对于事务涉及更新的每一行,计算出这一行的hash值组成集合writeset,如果两个事务的writeset没有交集就可以并行;

  • WRITESET_SESSION,在WRITESET基础上多了一个约束,即主库上同一个线程先后执行的两个事务,在备库执行时要保证相同的先后顺序。

hash值是通过"库名+表名+索引名+值"计算出来的。

该策略和最前面的按行分发策略类似,但还是有一定优势:

  • writeset是在主库生成后直接写入到binlog里的,这样在备库执行时不需要解析binlog内容;

  • 不需要把整个事务的binlog都扫一遍才能确定分发到哪个worker,更省内存;

  • 备库分发策略不依赖binlog内容,binlog可以是statement格式。

相关推荐
布朗克1681 小时前
MySQL UNION 操作符详细说明
数据库·mysql·union
喵桑..4 小时前
视图是什么?有什么用?什么时候用?MySQL中的视图
数据库·mysql
陈壮实的搬砖日记8 小时前
一文读懂 Hive、Trino 和 SparkSQL:三大大数据 SQL 引擎的全面对比
mysql
苹果醋39 小时前
React Native jpush-react-native极光推送 iOS生产环境接收不到推送
java·运维·spring boot·mysql·nginx
加油吧zkf14 小时前
MySQL索引优化全攻略:提升查询性能30%
数据库·mysql
iknow18115 小时前
【Web安全】Sql注入之SqlServer和MySQL的区别
sql·mysql·sqlserver
布朗克16815 小时前
MySQL 临时表详细说明
数据库·mysql·临时表
布朗克16816 小时前
MySQL 复制表详细说明
数据库·mysql·复制表
iceland91 天前
mysql 8递归查询
数据库·mysql