ToB企服应用市场:ToB评测及商务社交产业平台

标题: MySQL并行复制(MTS)原理(完整版) [打印本页]

作者: 南七星之家    时间: 2022-6-22 12:41
标题: MySQL并行复制(MTS)原理(完整版)
目录

MySQL 5.6并行复制架构

MySQL 5.7并行复制原理

MySQL 5.6基于库的并行复制出来后,基本无人问津,在沉寂了一段时间之后,MySQL 5.7出来了,它的并行复制以一种全新的姿态出现在了DBA面前。
MySQL 5.7才可称为真正的并行复制,这其中最为主要的原因就是slave服务器的回放与master是一致的,即master服务器上是怎么并行执行的,那么slave上就怎样进行并行回放。不再有库的并行复制限制,对于二进制日志格式也无特殊的要求(基于库的并行复制也没有要求)。
从MySQL官方来看,其并行复制的原本计划是支持表级的并行复制和行级的并行复制,行级的并行复制通过解析ROW格式的二进制日志的方式来完成,WL#4648。但是最终出现给小伙伴的确是在开发计划中称为:MTS(Prepared transactions slave parallel applier),可见:WL#6314。该并行复制的思想最早是由MariaDB的Kristain提出,并已在MariaDB 10中出现,相信很多选择MariaDB的小伙伴最为看重的功能之一就是并行复制。MTS实现了事务的并行,从某种程度来说也实现了行的并行(事务对行处理)。
下面来看看MySQL 5.7中的并行复制究竟是如何实现的?
order commit (group commit) -> logical clock ->> MTS
Master

组提交(group commit)

组提交(group commit):通过对事务进行分组,优化减少了生成二进制日志所需的操作数。当事务同时提交时,它们将在单个操作中写入到二进制日志中。如果事务能同时提交成功,那么它们就不会共享任何锁,这意味着它们没有冲突,因此可以在Slave上并行执行。所以通过在主机上的二进制日志中添加组提交信息,这些Slave可以并行地安全地运行事务。
首先,MySQL 5.7的并行复制基于一个前提,即所有已经处于prepare阶段的事务,都是可以并行提交的。这些当然也可以在从库中并行提交,因为处理这个阶段的事务,都是没有冲突的,该获取的资源都已经获取了。反过来说,如果有冲突,则后来的会等已经获取资源的事务完成之后才能继续,故而不会进入prepare阶段。这是一种新的并行复制思路,完全摆脱了原来一直致力于为了防止冲突而做的分发算法,等待策略等复杂的而又效率底下的工作。
MySQL 5.7并行复制的思想一言以蔽之:一个组提交(group commit)的事务都是可以并行回放,因为这些事务都已进入到事务的prepare阶段,则说明事务之间没有任何冲突(否则就不可能提交)。
根据以上描述,这里的重点是——
——为了兼容MySQL 5.6基于库的并行复制,5.7引入了新的变量slave-parallel-type,其可以配置的值有:
支持并行复制的GTID

那么如何知道事务是否在同一组中?原版的MySQL并没有提供这样的信息。
在MySQL 5.7版本中,其设计方式是将组提交的信息存放在GTID中。
那么如果参数gtid_mode设置为OFF,用户没有开启GTID功能呢?
MySQL 5.7又引入了称之为Anonymous_Gtid(ANONYMOUS_GTID_LOG_EVENT)的二进制日志event类型,
如:
  1. mysql> SHOW BINLOG EVENTS in 'mysql-bin.000006';
  2. +------------------+-----+----------------+-----------+-------------+-----------------------------------------------+
  3. | Log_name         | Pos | Event_type     | Server_id | End_log_pos | Info                                         |
  4. +------------------+-----+----------------+-----------+-------------+-----------------------------------------------+
  5. | mysql-bin.000006 | 4   | Format_desc    | 88        | 123          | Server ver: 5.7.7-rc-debug-log, Binlog ver: 4|
  6. | mysql-bin.000006 | 123 | Previous_gtids | 88        | 194          |                                              |
  7. | mysql-bin.000006 | 194 | Anonymous_Gtid | 88        | 259          | SET @@SESSION.GTID_NEXT= 'ANONYMOUS'         |
  8. | mysql-bin.000006 | 259 | Query          | 88        | 330          | BEGIN                                        |
  9. | mysql-bin.000006 | 330 | Table_map      | 88        | 373          | table_id: 108 (aaa.t)                        |
  10. | mysql-bin.000006 | 373 | Write_rows     | 88        | 413          | table_id: 108 flags: STMT_END_F              |
  11. ......
复制代码
这意味着在MySQL 5.7版本中即使不开启GTID,每个事务开始前也是会存在一个Anonymous_Gtid,而这个Anonymous_Gtid事件中就存在着组提交的信息。反之,如果开启了GTID后,就不会存在这个Anonymous_Gtid了,从而组提交信息就记录在非匿名GTID事件中。
slave

LOGICAL_CLOCK(由order commit实现),实现的group commit目的

然而,通过上述的SHOW BINLOG EVENTS,我们并没有发现有关组提交的任何信息。但是通过mysqlbinlog工具,就能发现组提交的内部信息——
  1. $ mysqlbinlog mysql-bin.0000006 | grep last_committed
  2. #150520 14:23:11 server id 88 end_log_pos 259  CRC32 0x4ead9ad6 GTID last_committed=0 sequence_number=1
  3. #150520 14:23:11 server id 88 end_log_pos 1483 CRC32 0xdf94bc85 GTID last_committed=0 sequence_number=2
  4. #150520 14:23:11 server id 88 end_log_pos 2708 CRC32 0x0914697b GTID last_committed=0 sequence_number=3
  5. #150520 14:23:11 server id 88 end_log_pos 3934 CRC32 0xd9cb4a43 GTID last_committed=0 sequence_number=4
  6. #150520 14:23:11 server id 88 end_log_pos 5159 CRC32 0x06a6f531 GTID last_committed=0 sequence_number=5
  7. #150520 14:23:11 server id 88 end_log_pos 6386 CRC32 0xd6cae930 GTID last_committed=0 sequence_number=6
  8. #150520 14:23:11 server id 88 end_log_pos 7610 CRC32 0xa1ea531c GTID last_committed=6 sequence_number=7
  9. #150520 14:23:11 server id 88 end_log_pos 8834 CRC32 0x96864e6b GTID last_committed=6 sequence_number=8
  10. #150520 14:23:11 server id 88 end_log_pos 10057 CRC32 0x2de1ae55 GTID last_committed=6 sequence_number=9
  11. #150520 14:23:11 server id 88 end_log_pos 11280 CRC32 0x5eb13091 GTID last_committed=6 sequence_number=10
  12. #150520 14:23:11 server id 88 end_log_pos 12504 CRC32 0x16721011 GTID last_committed=6 sequence_number=11
  13. #150520 14:23:11 server id 88 end_log_pos 13727 CRC32 0xe2210ab6 GTID last_committed=6 sequence_number=12
  14. #150520 14:23:11 server id 88 end_log_pos 14952 CRC32 0xf41181d3 GTID last_committed=12 sequence_number=13
  15. ...
复制代码
上述的last_committed和sequence_number代表的就是所谓的LOGICAL_CLOCK。
可以发现MySQL 5.7二进制日志较之原来的二进制日志内容多了last_committed和sequence_number。
另外,还有一个细节,下一个事务组的last_committed和上一个事务的sequence_number是相等的。这也很容易理解,因为事物是顺序提交的,这么理解起来并不奇怪。本组的 sequence_number最小值肯定大于last_committed。(这一块描述不严谨,在5.7后续版本中,官方优化了slave进行并行apply的规则,但是这里为了便于理解,不做修改,理解这个思路后阅读后面基于锁的并行规则也很容易。)
这两个值的有效作用域都在文件内,只要换一个binlog文件(flush binary logs),这两个值就都会从0开始计数。
MySQL是如何做到将这些事务分组的?

还有一个重要的技术问题:MySQL是如何做到将这些事务分组的?
要搞清楚这个问题,首先需要了解一下MySQL事务提交方式。
1. 事务两阶段提交


事务的提交主要分为两个主要步骤:
(不好理解这段就看上面的图解好了。)
2. Order Commit:是LOGICAL_CLOCK并行复制的基础

关于MySQL是如何提交的,内部使用ordered_commit函数来处理的。先看它的逻辑图,如下:

从图中可以看到,只要事务提交(调用ordered_commit),就都会先加入队列中。
提交有三个步骤,包括FLUSH、SYNC及COMMIT,相应地也有三个队列。
现在应该搞明白关于order commit的原理了,而这也是LOGICAL_CLOCK并行复制的基础
因为order commit使得所有的事务分了组,并且有了序列号,从库拿到这些信息之后,就可以根据序号放心大胆地做分发了。
探索:binlog_group_commit_sync_delay 、binlog_group_commit_sync_no_delay_count对group commit的影响:

从时间上说,从队长开始入队,到取队列中的所有事务出来,这之间的时间是非常非常小的,所以在这段时间内其实不会有多少个事务。
只有在压力很大,提交的事务非常多的时候,才会提高并发度(组内事务数变大)。
不过这个问题也可以解释得通,主库压力小的时候,从库何必要那么大的并发度呢?只有主库压力大的时候,从库才会延迟。
这种情况下也可以通过调整主服务器上的参数binlog_group_commit_sync_delay、binlog_group_commit_sync_no_delay_count。
两个参数都是为了增加主服务器组提交的事务比例,从而增大从机MTS的并行度。
事务group commit,logical clock(order commit)示意图:

假设当前环境配置参数:
  1. binlog_group_commit_sync_delay = 1000
  2. binlog_group_commit_sync_no_delay_count = 5
复制代码
图中:
T0->T1->..->T6,每一个区间表示一个binlog_group_commit_sync_delay = 1000 时间范围,红虚线将该时间范围5等分。
其中,T0为session1 - session10 十个会话同时开启事务的时间节点。
tn-m,为session-n在当前位置进行了第m次提交动作。
从库多线程复制分发原理

知道了order commit原理之后,现在很容易可以想到在从库端是如何分发的:
从库以事务为单位做APPLY的,每个事务有一个GTID事件,因此都有一个last_committed及sequence_number值。
1. 基于last_committed分发原理如下:

因为last_committed值的记录方式是:master将上一组最后一个sequence_number记录为下一组的last_committed,因此本组的sequence_number最小值肯定大于last_committed,下一组的last_committed肯定大于前一组sequence_number的最小值(因为等于sequence_number最大值)
原理示意参考:

Commit-Parent-Based Scheme简介(WL#7165)

此commit-parent就是我们在binlog中看到的last_committed。如果commit-parent相同,即last_committed相同,则被视为同一组,可以并行回放。
基于last_committed分发(Commit-Parent-Based Scheme)存在的问题

一句话:Commit-Parent-Based Scheme会降低复制的并行程度

解释一下图:
说明:上面的步骤是以事务为单位介绍的,其实实际处理中还是一个事件一个事件地分发。如果一个事务已经选定了worker,而新的event还在那个事务中,则直接交给那个worker处理即可。
从上面的分发原理来看,同时执行的都是具有相同last_committed值的事务,不同的只是后面的需要等前面做完了才能执行,这样的执行方式有点如下图所示:

可以看出,事务都是随机分配到了worker线程中,但是执行的话,必须是一行一行地执行。一行事务个数越多,并行度越高,也说明主库瞬时压力越大
2. MySQL 5.7开始基于lock interval的并行规则(WL#7165)

实现:如果两个事务在同一时间持有各自的锁,就可以并发执行。
对前一个原理需要补充为:
因为last_committed值的记录方式是:master将上一组最后一个sequence_number记录为下一组的last_committed,master将MySQL全局变量global.max_committed_transaction(所有已经结束lock interval的事务的最大的sequence_number)记录为下一组的last_committed,因此本组的sequence_number最小值肯定大于last_committed,下一组的last_committed肯定大于前一组sequence_number的最小值(因为等于sequence_number最大值)
# 根据基于锁特性,实际上是与本组第一个Prepare存在时间间隙的上一组C的那个事务的sequence,也就是说,如果前一组的后几个事务与当前组的前几个事务存在lock interval重叠,那么前一组的这几个事务再向前一个事务的sequence才是当前组的last_committed
Lock-Based Scheme简介(WL#7165)

首先,定义了一个称为lock interval的概念,含义:一个事务持有锁的时间间隔
假定:最后一把锁获取是在binlog_prepare阶段。
假设有两个事务:Trx1、Trx2。Trx1先于Trx2。那么,
transaction.sequence_number和transaction.last_committed这两个时间戳都会存放在binlog中。
如果所有正在执行的事务的最小的sequence_number大于一个事务的transaction.last_committed,那么这个事务就可以并发执行。(这句话太绕,不用强求,看下面土味理解好了)
土味理解Lock-Based Scheme

在这先抛开writeset,不要混淆了,理解了这个会有助于理解writeset原理。
  1. t1,last_committed=0, sequence_number=3
  2. t2,last_committed=3, sequence_number=4
  3. t3,last_committed=3, sequence_number=5
  4. t4,last_committed=3, sequence_number=6
  5. t5,last_committed=3, sequence_number=7
  6. t6,last_committed=6, sequence_number=8
  7. t7,last_committed=6, sequence_number=9
  8. t8,last_committed=9, sequence_number=10
复制代码
因为last_committed=6小于正在执行执行事务的sequence_number=7,可以并行。
总结一句话就是:last_committed值取自于前一组中,与本组事务不存在lock interval重叠的最后一个事务的sequence number
MySQL 5.7并行复制测试

下图显示了开启MTS后,Slave服务器的QPS。测试的工具是sysbench的单表全update测试,测试结果显示在16个线程下的性能最好,从机的QPS可以达到25000以上,进一步增加并行执行的线程至32并没有带来更高的提升。而原单线程回放的QPS仅在4000左右,可见MySQL 5.7 MTS带来的性能提升,而由于测试的是单表,所以MySQL 5.6的MTS机制则完全无能为力了。

并行复制配置与调优


这里其中引入了另一个问题,如果主机上的负载不大,那么组提交的效率就不高,很有可能发生每组提交的事务数量仅有1个,那么在从机的回放时,虽然开启了并行复制,但会出现性能反而比原先的单线程还要差的现象,即延迟反而增大了。聪明的小伙伴们,有想过对这个进行优化吗?
说了这么多,要开启enhanced multi-threaded slave其实很简单,只需根据如下设置:
  1. # slave;
  2. slave-parallel-type=LOGICAL_CLOCK
  3. slave-parallel-workers=16
  4. slave_pending_jobs_size_max = 2147483648
  5. slave_preserve_commit_order=1
  6. master_info_repository=TABLE
  7. relay_log_info_repository=TABLE
  8. relay_log_recovery=ON
复制代码
在使用了MTS后,复制的监控依旧可以通过SHOW SLAVE STATUS\G,但是MySQL 5.7在performance_schema架构下多了以下这些元数据表,用户可以更细力度的进行监控:
  1. mysql> show tables like 'replication%';
  2. +---------------------------------------------+
  3. | Tables_in_performance_schema (replication%) |
  4. +---------------------------------------------+
  5. | replication_applier_configuration      |
  6. | replication_applier_status         |
  7. | replication_applier_status_by_coordinator  |
  8. | replication_applier_status_by_worker    |
  9. | replication_connection_configuration    |
  10. | replication_connection_status        |
  11. | replication_group_member_stats       |
  12. | replication_group_members          |
  13. +---------------------------------------------+
  14. 8 rows in set (0.00 sec)
复制代码
通过replication_applier_status_by_worker可以看到worker进程的工作情况:
  1. mysql> select * from replication_applier_status_by_worker;
  2. +--------------+-----------+-----------+---------------+--------------------------------------------+-------------------+--------------------+----------------------+
  3. | CHANNEL_NAME | WORKER_ID | THREAD_ID | SERVICE_STATE | LAST_SEEN_TRANSACTION           | LAST_ERROR_NUMBER | LAST_ERROR_MESSAGE | LAST_ERROR_TIMESTAMP |
  4. +--------------+-----------+-----------+---------------+--------------------------------------------+-------------------+--------------------+----------------------+
  5. |       |     1 |    32 | ON      | 0d8513d8-00a4-11e6-a510-f4ce46861268:96604 |         0 |          | 0000-00-00 00:00:00 |
  6. |       |     2 |    33 | ON      | 0d8513d8-00a4-11e6-a510-f4ce46861268:97760 |         0 |          | 0000-00-00 00:00:00 |
  7. +--------------+-----------+-----------+---------------+--------------------------------------------+-------------------+--------------------+----------------------+
  8. 2 rows in set (0.00 sec)
复制代码
那么怎样知道从机MTS的并行程度又是一个难度不小。简单的一种方法(姜总给出的),可以使用performance_schema库来观察,比如下面这条SQL可以统计每个Worker Thread执行的事务数量,在此基础上再做一个聚合分析就可得出每个MTS的并行度:
  1. SELECT thread_id,count_star FROM performance_schema.events_transactions_summary_by_thread_by_event_name
  2. WHERE thread_id IN (SELECT thread_id FROM performance_schema.replication_applier_status_by_worker);
复制代码
如果线程并行度太高,不够平均,其实并行效果并不会好,可以试着优化。这种场景下,可以通过调整主服务器上的参数binlog_group_commit_sync_delay、binlog_group_commit_sync_no_delay_count。前者表示延迟多少时间提交事务,后者表示组提交事务凑齐多少个事务再一起提交。总体来说,都是为了增加主服务器组提交的事务比例,从而增大从机MTS的并行度。
虽然MySQL 5.7推出的Enhanced Multi-Threaded Slave在一定程度上解决了困扰MySQL长达数十年的复制延迟问题。然而,目前MTS机制基于组提交实现,简单来说在主上是怎样并行执行的,从服务器上就怎么回放。这里存在一个可能,即若主服务器的并行度不够,则从机的并行机制效果就会大打折扣。MySQL 8.0最新的基于writeset的MTS才是最终的解决之道。即两个事务,只要更新的记录没有重叠(overlap),则在从机上就可并行执行,无需在一个组,即使主服务器单线程执行,从服务器依然可以并行回放。相信这是最完美的解决之道,MTS的最终形态。
最后,如果MySQL 5.7要使用MTS功能,必须使用最新版本,最少升级到5.7.19版本,修复了很多Bug。
参考信息
http://www.ywnds.com/?p=3894
运维内参书籍
姜总的公众号文章
http://mysql.taobao.org/monthly/2017/12/03/
https://mp.weixin.qq.com/s/XbWMdVTl9qz1nSwL3l56XQ

来源:https://www.cnblogs.com/konggg/p/16359474.html
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4