MySQL主从复制(三):主从延迟

主备流程图:

谈到主备的复制能力,要关注的是上图中的两个黑色箭头。

一个箭头代表了客户端写入主库,另一个箭头代表的是sql_thread执行中转日志(relay log)。如果用箭头的粗细来代表并行度的话,那么真实情况就如图所示,第一个箭头要明显粗于第二个箭头。

1)在主库上,影响并发度的原因就是各种锁了。由于InnoDB引擎支持行锁, 除了所有并发事务都在更新同一行(热点行) 这种极端场景外, 它对业务并发度的支持还是很友好的。 所以, 你在性能测试的时候会发现, 并发压测线程32就比单线程时, 总体吞吐量高。

2)日志在备库上的执行, 就是图中备库上sql_thread更新数据(DATA)的逻辑。 如果是用单线程的话, 就会导致备库应用日志不够快, 造成主备延迟。

注:在官方的5.6版本之前, MySQL只支持单线程复制, 由此在主库并发高、 TPS高时就会出现严重的主备延迟问题。

备库多线程复制机制,都是要把上图中的sql_thread,拆成多个线程,也就是都符合下面的这个模型:

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

注:参数slave_parallel_workers的值,设置为8~16之间最好(32核物理机的情况) , 毕竟备库还有可能要提供读查询, 不能把CPU都吃光了。

问1:事务能不能按照轮询的方式分发给各个worker, 也就是第一个事务分给worker_1, 第二个事务发给worker_2呢?

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

问2:同一个事务的多个更新语句, 能不能分给不同的worker来执行呢?

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

所以, coordinator在分发的时候, 需要满足以下这两个基本要求:

1)不能造成更新覆盖。 这就要求更新同一行的两个事务, 必须被分发到同一个worker中。

2)同一个事务不能被拆开, 必须放到同一个worker中。

MySQL 5.5版本的并行复制策略


官方MySQL 5.5版本是不支持并行复制的。

常见并行策略:按表分发策略和按行分发策略。

按表分发策略

按表分发事务的基本思路:如果两个事务更新不同的表,它们就可以并行。因为数据是存储在表里的,所以按表分发,可以保证两个worker不会更新同一行。

当然如果有跨表的事务,还是要把两张表放在一起考虑,以保证事务逻辑的隔离性。按表分发规则如下:

从上图可以看到, 每个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。

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

注:按表分发的方案, 在多个表负载均匀的场景里应用效果很好。 但是, 如果碰到热点表, 比如所有的更新事务都会涉及到某一个表的时候, 所有事务都会被分配到同一个worker中, 就变成单线程复制了。

按行分发策略

要解决热点表的并行复制问题, 就需要一个按行并行复制的方案。

按行复制的核心思路是: 如果两个事务没有更新相同的行, 它们在备库上可以并行执行。 显然, 这个模式要求binlog格式必须是row。

按行复制和按表复制的数据结构差不多, 也是为每个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 t1values(1,1,1),(2,2,2),(3,3,3),(4,4,4),(5,5,5);

假设, 接下来我们要在主库执行这两个事务:

这两个事务要更新的行的主键值不同, 但是如果它们被分到不同的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中, 这样冲突检测就不准确。

对比按表分发和按行分发这两个方案的话, 按行分发策略的并行度更高。 不过, 如果是要操作很多行的大事务的话, 按行分发的策略有两个问题:

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

2)耗费CPU。 解析binlog, 然后计算hash值, 对于大事务, 这个成本还是很高的。

所以, 在实现这个策略的时候会设置一个阈值, 单个事务如果超过设置的行数阈值(比如, 如果单个事务更新的行数超过10万行) , 就暂时退化为单线程模式, 退化过程的逻辑大概是这样的:

1)coordinator暂时先hold住这个事务。

2)等待所有worker都执行完成, 变成空队列。

3)coordinator直接执行这个事务。

4)恢复并行模式。

注:上述按表分发策略和按行分发策略是MySQL45讲作者自主研发。

MySQL 5.6版本的并行复制策略


官方MySQL5.6版本, 支持了并行复制, 只是支持的粒度是按库并行。

与按表分发策略和按行分发策略同理,按库分发策略的hash表里,key就是数据库名。

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

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

1)构造hash值的时候很快, 只需要库名; 而且一个实例上DB数也不会很多, 不会出现需要构造100万个项这种情况。

2)不要求binlog的格式。 因为statement格式的binlog也可以很容易拿到库名。

注1:如果主库上的表都放在同一个DB里面,这个策略就没有效果了。

注2:如果不同DB的热点不同,该策略也起不到并行的效果。如,一个是业务逻辑库,一个是系统配置库。

注3:理论上可以创建不同的DB, 把相同热度的表均匀分到这些不同的DB中, 强行使用这个策略。但由于需要特地移动数据, 所以这个策略用得并不多。

MariaDB的并行复制策略


MariaDB的并行复制策略特性:

1)能够在同一组里提交的事务, 一定不会修改同一行。

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

在实现上, MariaDB是这么做的:

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

2)commit_id直接写到binlog里面。

3)传到备库应用的时候, 相同commit_id的事务分发到多个worker执行。

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

MariaDB的并行复制策略缺点:

1)它并没有实现“真正的模拟主库并发度”这个目标。 在主库上, 一组事务在commit的时候, 下一组事务是同时处于“执行中”状态的。

假设了三组事务在主库的执行情况, 你可以看到在trx1、 trx2和trx3提交的时候, trx4、 trx5和trx6是在执行的。 这样, 在第一组事务提交完成的时候, 下一组事务很快就会进入commit状态。主库并行事务执行效果:

2)并行过程中,如果同组有大事务,则需要等待大事务执行完成后才能继续。即容易被大事务推后退。

按照MariaDB的并行复制策略, 备库上的执行效果:

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

MySQL 5.7的并行复制策略


在MariaDB并行复制实现之后, 官方的MySQL5.7版本也提供了类似的功能, 由参数slaveparallel-type来控制并行复制策略:

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

2)配置为 LOGICAL_CLOCK, 表示的就是类似MariaDB的策略。 不过, MySQL 5.7这个策略, 针对并行度做了优化。 这个优化的思路也很有趣儿。

问:同时处于“执行状态”的所有事务,是否可以并行?

答:不能。因为, 这里面可能有由于锁冲突而处于锁等待状态的事务。 如果这些事务在备库上被分配到不同的worker, 就会出现备库跟主库不一致的情况。但所有处于“commit状态”的事务是可以并行的,因为事务处于commit状态,表示已经通过了锁冲突的检验了。

其实, 不用等到commit阶段, 只要能够到达redo log prepare阶段, 就表示事务已经通过锁冲突的检验了。两阶段提交流程图:

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

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

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

讲binlog的组提交的时候, 介绍过两个参数:

  • binlog_group_commit_sync_delay参数, 表示延迟多少微秒后才调用fsync。
  • binlog_group_commit_sync_no_delay_count参数, 表示累积多少个事务以后才调用fsync。

这两个参数是用于故意拉长binlog从write到fsync的时间, 以此减少binlog的写盘次数。 在MySQL 5.7的并行复制策略里, 它们可以用来制造更多的“同时处于prepare阶段的事务”。 这样就增加了备库复制的并行度。

也就是说, 这两个参数, 既可以“故意”让主库提交得慢些, 又可以让备库执行得快些。 在MySQL 5.7处理备库延迟的时候, 可以考虑调整这两个参数值, 来达到提升备库复制并发度的目的。

MySQL 5.7.22的并行复制策略


在2018年4月份发布的MySQL 5.7.22版本里, MySQL增加了一个新的并行复制策略, 基于WRITESET的并行复制。

相应地, 新增了一个参数binlog-transaction-dependency-tracking, 用来控制是否启用这个新策略。 这个参数的可选值有以下三种:

  • COMMIT_ORDER, 表示的就是前面介绍的, 根据同时进入prepare和commit来判断是否可以并行的策略。
  • WRITESET, 表示的是对于事务涉及更新的每一行, 计算出这一行的hash值, 组成集合writeset。 如果两个事务没有操作相同的行, 也就是说它们的writeset没有交集, 就可以并行。
  • WRITESET_SESSION, 是在WRITESET的基础上多了一个约束, 即在主库上同一个线程先后执行的两个事务, 在备库执行的时候, 要保证相同的先后顺序。

注:为了唯一标识, 这个hash值是通过“库名+表名+索引名+值”计算出来的。 如果一个表上除了有主键索引外, 还有其他唯一索引, 那么对于每个唯一索引, insert语句对应的writeset就要多增加一个hash值。

虽然这和前面介绍的MySQL 5.5版本的按行分发的策略是差不多的。但MySQL官方的这个实现还是有很大的优势:

1)writeset是在主库生成后直接写入到binlog里面的, 这样在备库执行的时候, 不需要解析binlog内容(event里的行数据) , 节省了很多计算量。

2)不需要把整个事务的binlog都扫一遍才能决定分发到哪个worker, 更省内存。

3)由于备库的分发策略不依赖于binlog内容, 所以binlog是statement格式也是可以的。

注1:对于“表上没主键”和“外键约束”的场景, WRITESET策略也是没法并行的, 也会暂时退化为单线程模型。

注2:大事务不仅会影响到主库,也是造成备库复制延迟的主要原因之一。因此, 在平时的开发工作中, 我建议你尽量减少大事务操作, 把大事务拆成小事务。

小结:思考题


思考:假设一个MySQL 5.7.22版本的主库, 单线程插入了很多数据, 过了3个小时后, 我们要给这个主库搭建一个相同版本的备库。这时候, 你为了更快地让备库追上主库, 要开并行复制。 在binlog-transaction-dependencytracking参数的COMMIT_ORDER、 WRITESET和WRITE_SESSION这三个取值中, 你会选择哪一个呢?你选择的原因是什么? 如果设置另外两个参数, 你认为会出现什么现象呢?

答:选择WRITESET策略。

1)由于主库是单线程压力模式, 所以每个事务的commit_id都不同, 那么设置为COMMIT_ORDER模式的话, 从库也只能单线程执行。

2)WRITESET模式通过对比更新的事务是否存在冲突的行,可以并发执行。

3)由于WRITESET_SESSION模式要求在备库应用日志的时候, 同一个线程的日志必须与主库上执行的先后顺序相同, 也会导致主库单线程压力模式下退化成单线程复制。

  • 7
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

数据库内核

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值