文章目录
什么是MGR?
官方文档:https://dev.mysql.com/doc/refman/5.7/en/group-replication.html
MGR(MySQL Group Replication)是MySQL官方在MySQL 5.7.17版本中以插件形式推出的主从复制高可用技术,它基于原生的主从复制,将各节点归入到一个组中,通过组内节点的通信协商(组通信协议基于Paxos算法),实现数据的强一致性、故障探测、冲突检测、节点加组、节点离组等等功能。
例如,具有3个节点的组:
这3个节点互相通信,每当有事件发生,都会向其他节点传播该事件,然后协商,如果大多数节点都同意这次的事件,那么该事件将通过,否则该事件将失败或回滚。
这些节点可以是单主模型的(single-primary),也可以是多主模型的(multi-primary)。单主模型只有一个主节点可以接受写操作,主节点故障时可以自动选举主节点。多主模型下,所有节点都可以接受写操作,所以没有master-slave的概念。
优点:
强一致性、高容错性、高扩展性、高灵活性
局限:
仅支持InnoDB表且每张表一定要有一个主键(用于做写入集的冲突检测)、必须打开GTID特性、日志格式为ROW、不支持大事务(大小最好不超过143MB)、一个MGP集群最多支持9个节点(节点过多性能负载大)、不支持外键约束、二进制日志不支持Binlog Event Checksum
适用场景:
- 金融交易、重要数据存储、对主从一致性要求高的场景
- 核心数据总量未过亿
- 读多写少的应用场景,如互联网电商
单主模型 Single-Primary Mode
一旦选择了新的主节点,它会自动设置为读写,而其他辅助节点保持为辅助节点,因此是只读的。
多主模型 Multi-Primary Mode
想支持n台服务器故障而正常运行就至少需要n+(n+1)台服务器,因为要保证在线服务器占多数
MGR集群最多支持9个节点,节点过多性能负载太大。
组复制实现原理
简单阐述:
- 执行源 1 收到的事务。然后,源 1 向复制组发送一条消息,该组由其自身、源 2 和源 3 组成。当所有三个成员达成共识时,他们就对交易进行认证。然后,源 1 将事务写入其二进制日志,提交它,并向客户端应用程序发送响应。源 2 和 3 将事务写入其中继日志,然后应用它,将其写入二进制日志并提交它。
组由多个服务器构成,通过传递消息进行交互,通信层保证原子消息传递。MGR构建于此通信层抽象之上,并实现了多主更新复制协议。
组中的每个服务器独立地执行事务,但是一个事务的提交,必须经过组内大多数节点(N/2+1)决议并通过,才能得以提交。如上图所示,由3个节点组成一个复制组,Consensus层为一致性协议层,在事务提交过程中,发生组间通讯,由2个节点决议(certify)通过这个事务,事务才能够最终得以提交并响应。只读事务在组内不需要协调,因此立即提交。
对于任何读写事务,当事务准备好在始发服务器处提交时,服务器以原子方式广播写入值(write values,已更改的行)和对应的写入集(write set,已更新的行的唯一标识符),然后将该事务加入全局事务列表。最终所有服务器都以相同的顺序接收(并为这些事务建立一个全局总顺序)并应用相同的事务集,所以它们在组内保持一致。
原子广播——要么组中的所有服务器都接收到事务,要么没有服务器接收到事务。
不同服务器上并发执行的事务之间可能存在冲突。MGR在certify过程中检查并发事务的写集来检测这种冲突。如果在不同服务器上执行的两个并发事务更新同一行,则存在冲突。解决方案是先到事务提交,后到事务回滚,即按顺序第一个事务在所有服务器提交,而第二个事务在在原始服务器上回滚并在组中的其它服务器中删除。这实际上体现的是多主分布式事务的首个提交获胜原则。
三个线程
**常规复制有两个复制线程:io线程和sql线程,在组复制中,不再称之为io_thread和sql_thread,取而代之的是receiver、certifier和applier。**这里简单介绍一下它们的作用:
- receiver的作用类似于io线程,用于接收组内个节点之间传播的消息和事务。也用于接收外界新发起的事务。
- applier的作用类似于sql线程,用于应用relay log中的记录。不过,组复制relay log的名称不再是relay log,而是
relay-log-group_replication_applier.00000N
。 - certifier的作用在receiver接收到消息后,验证是否有并发事务存在冲突问题。冲突检测通过后,这条消息就会写入到组复制的relay log中,等待applier去应用。
分布式恢复原理
MGR中的一组服务器构成一个复制组,组是动态的,服务器可以离开(主动或被动)并随时加入组。服务器加入或离开时,组会自行调整。如果服务器加入组,组会通过从现有服务器获取状态自动更新新加入的服务器。状态通过MySQL异步复制进行传输。如果服务器离开该组,其余服务器会知道它已离开并自动重新配置该组。
组复制分布式恢复可以概括为服务器从组中获得丢失事务的过程,以便它可以加入具有已处理相同事务集成员的组。在分布式恢复期间,加入组的服务器会缓冲其正在接收的,组中所需的事务和成员事件。一旦加入该组的服务器收到了该组的所有事务,它就会应用在恢复过程中缓冲的事务。此过程结束时,服务器随之作为在线成员加入组。分布式恢复分为两个阶段。
1)第一阶段:加入该组的服务器选择该组上的一个在线服务器作为其缺失状态的捐赠者(donor)。捐赠者负责为新服务器提供加入该组的所有数据,直到它加入该组为止。
在配置组复制时,需要指定种子节点列表。当新节点加入组时,只会联系种子节点,也即是说,只有种子节点列表中的节点才有机会成为donor,没有在种子节点列表中的节点不会被新节点选中。但建议,将组中所有节点都加入到种子列表中。
当联系第一个donor失败后,会向后联系第二个donor,再失败将联系第三个donor,如果所有种子节点都联系失败,在等待一段时间后再次从头开始联系第一个donor。依此类推,直到加组失败报错。
数据的恢复是通过在捐赠者和加入该组的服务器之间建立的标准异步复制通道来实现的。复制通道是MySQL 5.7 中提出的概念。简单讲一个复制通道表示从主库到从库的一条复制路径,在多源复制中主到从可以存在多条复制通道。通过此复制通道复制捐赠者的二进制日志,直到加入该组的服务器成为该组的一部分并发生视图更改。
加入该组的服务器在收到捐赠者的二进制日志时应用它们。复制二进制日志时,加入该组的服务器还会缓存在组内交换的每个事务。也就是说,它监听在加入该组之后发生的事务,同时应用来自捐赠者的数据。
2)第二阶段:追赶。在此阶段,加入组的服务器继续执行高速缓存的事务。排队等待执行的事务数最终达到零时,该成员将在线声明。
当加入组的服务器从捐赠者获取二进制日志时,恢复过程可以承受捐赠者故障。在这种情况下,捐赠者在第一阶段期间失败时,加入该组的服务器将故障转移到新的捐赠者并从新捐赠者恢复。加入该组的服务器将关闭与失败的捐赠者的连接,并打开与新捐赠者的连接,这些都是自动进行的。
总的来说,新加入组的成员会先通过选择的donor进行异步复制来恢复该组的所有数据,并在恢复后执行在恢复期间接收到的且进行了缓存的事务,在待执行事务数清零时进行在线声明。
组复制的要求与局限
基本要求
- InnoDB存储引擎:数据必须存储在事务型的InnoDB存储引擎中。事务以乐观形式执行,然后在提交前会检测冲突问题。如果有冲突,为了维护组中一致性,有些事务必须回滚。这意味着需要事务型的存储引擎。此外,InnoDB 存储引擎提供了一些额外的功能,它们结合组复制时能更好地管理和处理冲突。
- Primary Keys:每张需要被组复制的表都必须显式定义一个主键。主键在判断事务是否冲突扮演极其重要的角色:通过主键来准确识别每个事务中修改了表中的哪些行。(实际上是将主机hash成写集,然后由certifier来并发事务之间的检测冲突性)
- 使用IPv4 地址:MySQL组复制使用的组通信引擎组件只支持 IPv4。因此,必须使用IPv4的网络。
- 良好的网络性能:组复制设计的初衷是部署在集群环境中,集群中的节点相互之间都非常近,因此除了网络延迟,网络带宽也会影响组复制。
配置上的要求
组中的每个成员都必须配置以下选项:
- 必须开启二进制日志:设置
--log-bin[=log_file_name]
。MySQL组复制会复制二进制日志的内容,因此必须开启二进制日志。 - Slave Updates Logged:设置
--log-slave-updates
。节点需要记录applier已应用的日志。组中的每个节点都需要记录它们所接收到并应用的所有事务,这是必须的,因为恢复过程是依赖于组中参与者的二进制日志来进行的。因此,组中每个成员都必须保留每个事务的副本,即使某事务不是在该节点上开始的。 - Row Format的二进制日志:设置
--binlog-format=row
。组复制依赖于基于行格式的二进制日志,以便在组中传播所发生的更改能保持一致性。而且,在探测组中不同节点间发生的并发事务是否冲突时,需要从行格式的日志中提取一些内容来做比较。 - 开启GTID复制:设置
--gtid-mode=ON
。组复制使用GTID(全局事务ID)来精确跟踪每个节点上已经提交了哪些事务。也因此可以推断出某节点上要执行的事务是否和已执行的事务(每个节点上都有副本)冲突。换句话说,GTID是整个组复制判断事务是否冲突的基础。 - Replication Information Repositories:设置
--master-info-repository=TABLE
和--relay-log-info-repository=TABLE
。applier需要将 master 和 relay log 的元数据信息写入到系统表 mysql.slave_master_info 和 mysql.slave_relay_log_info 中。这保证了组复制插件具有一致性恢复的能力和复制的元数据事务管理能力。 - Transaction Write Set Extraction:设置
--transaction-write-setextraction=XXHASH64
,以便将行写入到二进制日志中时,节点也收集写集。写集基于每行的主键,是唯一标识被更改行的标签的简化形式,该标签后续会用于探测事务冲突性。 - Multithreaded Appliers:(某些旧版本没有该要求)可以将组复制成员配置为多线程appliers,使得可以并行应用事务。需要设置
--slave-parallel-workers=N
(N是applier线程数量)、--slavepreserve-commit-order=1
以及--slave-parallel-type=LOGICAL_CLOCK
。--slaveparallel-workers=N
表示启用多applier线程,组复制依赖于建立在所有参与节点都以相同顺序接收和应用、提交事务的一致性机制,因此还必须设置--slave-preserve-commit-order=1
以保证并行事务的最终提交是和原事务所在顺序位置一致的。最后,为了决定哪些事务可以并行执行,relay log 必须包含由--slave-parallel-ype=LOGICAL_CLOCK
生成的事务父信息(transaction parent information)。当尝试加入一个只设置了--slave-parallel-workers
大于0,却没有设置其他两项的新成员,将会报错并阻止它的加入。
局限性
下面是使用组复制已知的限制:
- Replication Event Checksums:由于对复制事件校验的设计缺陷,目前组复制不能使用它们。因此,需要设置
--binlog-checksum=NONE
。 - Gap Locks:在验证阶段中(certification process),不会考虑 Gap Locks,因此在 InnoDB 的外部无法获取任何关于Gap 锁的信息。
注意:
除非你的应用程序或业务需求依赖于REPEATABLE READ(MySQL默认该隔离级别),否则建议在组复制中使用READ COMMITTED隔离级别。在READ COMMITTED隔离级别中,InnoDB基本上不会使用Gap Locks,这将使得InnoDB自带的冲突探测能和组复制的冲突探测相互对齐从而保持一致。
- Table Locks and Named Locks:验证阶段(certification process)中不考虑表锁和命名锁(见get_lock())。
- 不支持 SERIALIZABLE 隔离级别:在多主模型下,默认不支持该隔离级别。如果在多主模型下设置了该隔离级别,将拒绝提交事务。
- 不支持并发的 DDL 和 DML 操作:不支持在多主模型下不同节点上同时执行DDL和DML修改同一对象。在某节点上对某对象执行DDL语句时,如果在其他节点同时执行DML修改该对象,将有一定风险探测到冲突。(译注:是 DDL+DML 的并发,DDL+DDL 的并发也不允许。这是因为MySQL中没有DDL事务,不能保证DDL的原子性,当DDL和DML同时操作某一个对象,可能DDL修改后,DML将因为对象结构的改变而无法执行,继而回滚)
- 不支持级联的外键约束:多主模型的组(所有节点都配置了
group_replication_single_primary_mode=OFF
)不支持多级外键依赖,特别是表上定义了级联的外键约束(CASCADING foreign key constraints)。这是因为多主模型下执行外键约束的级联操作可能会出现未检测到的冲突,从而导致组内成员间数据不一致。因此,我们推荐在使用多主模型时,在每个节点上都设置group_replication_enforce_update_everywhere_checks=ON
以避免出现未检测到的冲突。在单主模型下没有这种问题,因为没有并发写操作,从而不可能会出现未被探测到的冲突。 - 大事务可能会错误:如果一个事务非常大,导致GTID的内容非常多,以至于无法在 5 秒内通过网络传输完成,这时组成员间的通信将失败。要避免该问题,可以尽可能地限制事务的大小。例如,将
LOAD DATA INFILE
的文件切割为多个小块。 - 多主模型可能出现死锁:在多主模型下,
SELECT ... FOR UPDATE
语句可能会导致死锁。这是因为组内成员之间不会共享锁资源(译注:share nothing),因此这样的语句可能达不到预期的结果。
参考链接:
MySQL组复制详解(一)——基本原理 - 掘金 (juejin.cn)
使用MySQL组复制的限制和局限性 - 骏马金龙 - 博客园 (cnblogs.com)