死磕数据库系列(二十五):MySQL 高可用之组复制(MGR)详解

2023-03-16 00:00:00 视图 事务 节点 复制 成员


前面给大家介绍过:MySQL 高可用方案选型解析,其中就提到了组复制(MGR),其实,组复制是一个 MySQL 服务器插件,可以创建具有弹性、高可用性和容错的复制拓扑。今天我将详细的为大家介绍 MySQL 数据库高可用技术之一的:组复制(MGR)相关知识,希望大家能够从中收获多多!如有帮助,在看转发支持一波!!!

MySQL 高可用的背景

数据库的主从复制是一个很实用的功能,但如何保证它的高可用却是一件难事。实现MySQL主从复制高可用的工具,常见的有:

  • MMM:淘汰了,在一致性和高并发稳定性等方面有些问题。
  • MHA:有些人还在用,但也有些问题,也是趋于淘汰的MySQL主从高可用方案。
  • Galera:引领时代的主从复制高可用技术。
  • MariaDB Galera Cluster:MariaDB对Galera的实现。
  • PXC:Percona XtraDB Cluster,是Percona对Galera的自我实现,用的人似乎很多。
  • GR:Group Replication,MySQL官方提供的组复制技术(MySQL 5.7.17引入的技术),基于Paxos算法。

但是,上面每个高可用实现方法,都有这样那样的缺点,甚至mmm还是通过perl脚本来自动化模拟高可用环境的。到了Galera,这是一个引领潮流的高可用技术,它主要针对具有事务特性的Innodb存储引擎,Percona和MariaDB都分别实现了自己的Galera技术:MariaDB Galera Cluster和Percona XtraDB Cluster。MySQL没有推出自己的Galera,但却在2016年的MySQL 5.7.17版本中推出了Group Replication,即组复制技术。

网上对组复制和 Galera 的对比很多,特别是 2016 年组复制出生后大火的" Galera 将死"的言论,但实际上,仍然有很多人在用着 pxc,毕竟它已经扬帆航行多年,而 GR 才出没多久,前几个版本也一直在修修补补。那么,GR or Galera, that's a question更多关于MySQL学习的文章,请参阅:死磕数据库系列之 MySQL ,本系列持续更新中。

废话说了一大堆,还是进入正文吧:MySQL组复制技术的简介。

什么是 MGR?

MGR(MySQL Group Replication)是 MySQL 官方在 MySQL 5.7.17版本中以插件形式推出的主从复制高可用技术,它基于原生的主从复制,将各节点归入到一个组中,通过组内节点的通信协商(组通信协议基于Paxos算法),实现数据的强一致性、故障探测、冲突检测、节点加组、节点离组等等功能。

例如,具有3个节点的组:这3个节点互相通信,每当有事件发生,都会向其他节点传播该事件,然后协商,如果大多数节点都同意这次的事件,那么该事件将通过,否则该事件将失败或回滚。

这些节点可以是单主模型的(single-primary),也可以是多主模型的(multi-primary)。单主模型只有一个主节点可以接受写操作,主节点故障时可以自动选举主节点。多主模型下,所有节点都可以接受写操作,所以没有master-slave的概念。

组复制插件架构图

MySQL组复制是一个MySQL插件,它基于常规的MySQL复制,利用了基于行格式的二进制日志和GTID等特性。下图是MySQL组复制的整体框架图。以下是对该图中各组件的大致介绍,涉及到的术语先浏览一遍,后面会详细解释。

  • 从上图的顶端开始,有一系列的API控制组复制插件如何和MySQL Server进行交互(图中灰色方框)。
  • 中间有一些接口可以使信息从MySQL Server流向组复制插件,反之亦然。这些接口将MySQL Server核心部分和插件隔离开来。在Server到插件的方向上,传递一些通知信息,例如server正在启动,server正在恢复,server已准备好接收连接,server将要提交事务等等。另一方向,即插件到server的方向上,插件会通知server对事务进行提交,终止正在进行的事务,将事务放进relay-log中排队等等。
  • 从API往下是一些响应组件,当通知信息路由到它们身上时就响应。capture组件负责跟踪正在执行的事务的上下文信息。applier组件负责在本节点上执行远程传输来的事务。recovery组件负责管理分布式恢复过程,还负责在节点加入组时选择donor,编排追赶过程以及对选择donor失败时的反应。
  • 继续向下,replication协议模块包含了特定的复制协议逻辑。它负责探测冲突,在组中接收和传播事务。
  • 后两层是组内通信系统(GCS)的API(个绿色方框),以及一个基于Paxos组通信引擎的实现(implementation)(第二个绿色方框)。
    • GCS API是组内通信API,它是一个上层API,它抽象了构建一个复制状态机所需的属性,它从插件的更上层解耦了消息传递层。
    • 组通信引擎负责处理组内成员的通信。

单主模型和多主模型

MySQL 组复制是 MySQL 5.7.17 开始引入的新功能,为主从复制实现高可用功能。它支持单主模型和多主模型两种工作方式(默认是单主模型)。

单主模型:从复制组中众多个MySQL节点中自动选举一个master节点,只有master节点可以写,其他节点自动设置为read only。当master节点故障时,会自动选举一个新的master节点,选举成功后,它将设置为可写,其他slave将指向这个新的master。

多主模型:复制组中的任何一个节点都可以写,因此没有master和slave的概念,只要突然故障的节点数量不太多,这个多主模型就能继续可用。

如何创建组?

当个节点启动组复制功能之前(不是启动mysql实例,而是启动组复制插件的功能),一般会将这个节点设置为组的引导节点,这不是必须的,但除非特殊调试环境,没人会吃撑了用第二、第三个节点去引导组。

所谓引导组,就是创建组。组创建之后,其他节点才能加入到组中。

将某节点设置为引导节点的方式是在该节点上设置以下变量为ON:

set @@global.group_replication_bootstrap_group=on;

开启引导功能后,一般会立即开启该节点的组复制功能来创建组,然后立即关闭组引导功能。所以,在个节点上,这3个语句常放在一起执行:

set @@global.group_replication_bootstrap_group=on;
start group_replication;
set @@global.group_replication_bootstrap_group=off;

当个节点成功加入到组中后,这个节点会被标记为ONLINE,只有标记为ONLINE的节点,才是组中有效节点,可以向外提供服务、可以进行组内通信和投票。

如果配置的是单主模型(single-primary mode)的组复制,个加入组的节点会自动选为primary节点。如果配置为多主模型(multi-primary mode)的组复制,则没有master节点的概念。更多关于MySQL学习的文章,请参阅:死磕数据库系列之 MySQL ,本系列持续更新中。

新节点如何加入组?

新节点要加组,在配置好配置文件后,只需执行以下3个过程等待成功返回就可以了。

change master to 
        master_user='XXXX',
        master_password='YYYY'
        for channle 'group_replication_recovery';

install plugin group_replication soname 'group_replication.so';
start group_replication;

虽然操作很少,但这里涉及到一个很重要的过程:恢复过程。

如果一个新的节点要加入组,它首先要将自己的数据和组内的数据保持同步,同步的过程实际上是从组中获取某个节点的Binlog,然后应用到自己身上,填补自己缺失的那部分数据,这是通过异步复制完成的。而新节点和组中数据保持同步的过程称为恢复过程(或者称为分布式恢复过程),它由组复制插件架构图中的"Recovery"组件来完成。

当新节点启动了它自己的组复制功能时,它将根据自己配置文件中 group_replication_group_seeds 选项的值来选一个节点作为同步时的数据供应节点,这个节点称为**"donor"**(中文意思就是"供应者")。例如:

loose-group_replication_group_seeds="192.168.100.21:20001,192.168.100.22:20002"

上面的配置中,两个节点都可以成为donor,它们称为"种子节点(seed)"。新节点从前向后逐一选择,当和个donor交互失败,就会选择第二个donor,直到选完后一个donor,如果还失败,将超时等待一段时间后再从头开始选择。建议每个节点的配置文件中,都将组中所有节点写入种子节点列表中。

当选择好一个donor后,新节点会和这个donor建立一个数据恢复的通道group_replication_recovery。Recovery组件会从donor上复制所有新节点缺失的数据对应的binlog,然后应用在自身。当新节点数据和组中已经保持一致,则新节点成功加入到组,它将标记为ONLINE。

实际上,这只是恢复的一个阶段,还有第二阶段,有了上面的基础,下面的解释就容易理解了。

在新节点准备开始加入组的时候,Recovery组件有两个功能:(1).选择donor,并从donor处复制缺失的数据;(2).监控组中新产生的事务并放进自己的事务缓存队列中。

例如,在填补缺失的数据时,客户端向组中的可写节点插入了一条数据,这个数据不会被新节点的异步复制抓走,新节点会监控到这个事务。如下图:所以将恢复过程概括一下:新节点的recovery组件通过异步复制通道从donor处获取p1之前的所有数据,并监控组中新生成的事务放入自己的事务缓存队列中。当新节点将p1之前的数据恢复完成后,将执行缓存队列中的事务,直到缓存中的事务数量减为0后,才表示新节点已经完全赶上了组中的数据,这是它才可以标识为ONLINE,并真正成为组中的一员。

这里有个问题需要考虑:事务数量相同,为什么新节点能赶上组?个原因是事务不会源源不断地生成,它总有停顿的时候;第二个原因归功于基于行格式的binlog(row-based binlog),除了发起事务的那个节点,只要事务被大多数节点同意了,所有节点都根据binlog行中设置的值直接修改数据,而不会完整地执行事务逻辑。换句话说,事务发起节点修改数据的速度没有其他节点快,尽管它们是同时提交的。更多关于MySQL学习的文章,请参阅:死磕数据库系列之 MySQL ,本系列持续更新中。

如何执行单个事务?

假设组中已经有5个节点(s1、s2、s3、s4、s5)了,这些节点目前全都是ONLINE状态,这个状态表示能正确向外提供服务、能正确进行组内通信、能正确投票。假设s1是单主模型的主节点。

当在节点s1上执行了以下事务A1:

start transaction;
insert into t values(3);
commit;

s1称为事务的发起节点。s1首先会执行这个事务到commit,在真正commit之前有一个before_commit的过程,这个过程会将binlog传播给组内其它节点。当组内其它节点的receiver线程(其实就是io_thread,但组复制中不这样称呼)收到binlog后,将对这个事务进行决策,如果组内大多数节点(因为组内有5个节点,所以至少3个节点才满足大多数的要求)对这个事务达成一致,则这个事务会真正提交(决定后、提交前会先将buffer binlog写到disk binlog),s2-s5会将收到的binlog也写入到自己的disk binlog中,并通过applier线程(sql_thread)对这个事务进行应用。如果大多数节点未达成一致,则这个事务会回滚,日志不会写入到日志中。

需要注意的是,决策通过后,事务发起节点的commit操作和其它节点对binlog的应用没有强烈的先后关系,各节点收到"决策通过"的消息后,是独立完成事务的。所以,有些节点可能会有延迟。当延迟在允许的范围内,不会有什么问题,但如果某个节点严重落后于其他节点(拖后腿),组复制会放慢整个组的处理速度,也可能会做一些限流,关于这个问题的处理,叫做flow control。

以下为binlog中两个事务的对应的日志内容,每个事务的binlog是在成功决定提交之后才写入的:

+------------+------------------------------------------------------------------------+
| Gtid       | SET @@SESSION.GTID_NEXT= 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:4000007'|
| Query      BEGIN                                                                  |
| Table_map  | table_id: 111 (mut_gr.t1)                                              |
| Write_rows | table_id: 111 flags: STMT_END_F                                        |
| Xid        | COMMIT /* xid=152 */                                                   |
| Gtid       | SET @@SESSION.GTID_NEXT= 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:3000010'|
Query      | BEGIN                                                                  |
| Table_map  | table_id: 111 (mut_gr.t1)                                              |
| Write_rows | table_id: 111 flags: STMT_END_F                                        |
| Xid        | COMMIT /* xid=153 */                                                   |
+------------+------------------------------------------------------------------------+

问题是,组内所有节点数据和状态都是同步的,为什么还要对事务进行决策?在主节点上判断事务能否执行、能否提交不就可以了吗?对于单主模型下执行正常的事务来说确实如此。但是多主模型下多个节点可执行并发事务,而且组复制内部也有些特殊的事件(例如成员加组、离组),这些都需要得到大多数节点的同意才能通过,所以必须先进行决策。后文会逐一解释这些情况。

如何执行并发事务:冲突检测?

多个事务分两种:线性的事务和并发的事务。线性的多个事务和执行单个事务没有什么区别,无非是一个一个事务从上到下执行下去。并发事务就要多方面考虑。

如果有多个客户端同时发起了事务t1、t2、t3、t4,这4个事务会因为GTID特性而具有全局性并有序。例如:

'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:146'      # 第1个节点的gtid序列
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:1000033'  # 第2个节点的gtid序列
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:2000010'  # 第3个节点的gtid序列
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:3000009'  # 第4个节点的gtid序列
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:4000007'  # 第5个节点的gtid序列

只要同一个节点执行的事务,它的前缀是一样的,只是后缀会不断增长。例如,3个s1节点的事务,它们gtid的后缀可能会是146、147、148。3个s2节点的事务,它们的gtid后缀可能会是1000033、1000034、1000035。

注意

每个事务除了gtid,还有一个分布式的xid。这个id也必须全局,但同一个事务在不同节点上的体现值不一样。例如,在s1节点上,事务的xid=200,应用到s2节点上,这个事务的xid会改为适应自己节点上的id,可能值为xid=222。

另外,这个xid并不能保证顺序。例如s1上执行开启事务t1执行DML,但不提交,然后在s2节点上开启事务t2执行DML并提交,后提交s1上的事务,那么在s1上,t1的xid虽然值更小,但在binlog中却排在t2后面。

回到正题,并发事务t1、t2、t3、t4如何执行:

(1).如果是单主模型,那么这4个事务都会路由到主节点上,如果这4个事务修改的数据互不冲突、互不影响,那么它们都会成功应用到所有节点上。如果这4个事务之间有冲突,那么先提交的事务将获胜(事实上,这时的冲突事务会被阻塞)。

(2).如果是多主模型,如果这4个事务路由到同一个节点上执行,这和单主模型的情况一样。如果路由到不同节点,且并发执行,如果无冲突,则一切都OK,如果并发事务由冲突,那么先提交的事务获胜。此时,后提交的冲突事务,实际上是在修改过期的数据。

问题是如何进行事务的冲突检测?使用组复制有一大堆的限制:必须使用InnoDB引擎、必须开启gtid模式、表中必须有主键等等。如果创建了MyISAM表,或者创建没有主键的表,没关系,它会复制走,因为它们是DDL语句,MySQL中没有DDL事务,但是不能再向这样的表中插入数据,这是强制的,如果插入数据将报错。

ERROR 3098 (HY000): The table does not comply with the requirements by an external plugin.

其实这3个要求都是为了冲突检测做准备:

  • 使用InnoDB是为了保证所有数据写入都是事务性的,只有事务型的操作才能回滚,例如并发事务中检测到了冲突时需要回滚。
  • 必须开启GTID模型是为了保证让事务具有的事务ID,在组内传播出去后不会重复执行。GTID对组复制的影响是方方面面的,有了GTID,无论新节点还是曾经退出的旧节点,当它们再次加入到组中时都能保证不会与组中现有数据冲突。
  • 表中必须有主键是为了冲突检测。这个下面做详细解释。

当在某节点发起事务后,这个节点的replication协议模块(该模块的位置见文章开头的架构图)会收集该事务将要修改行的写集(write-set,术语),写集是根据每行的主键进行hash计算的,是否记得在组复制的配置文件中有一行:

transaction_write_set_extraction=XXHASH64

这就是指定以"XXHASH64"算法将主键hash为写集。然后该协议模块将binlog和写集一起传播出去,并被其它节点的replication协议模块收到。当其它节点收到后,会对写集进行验证,这个过程称为certify,它由certifier线程完成。如果验证通过,则为此事务投上自己的一票,多数节点通过后交给applier组件写入数据。如果验证不通过,意味着出现了事务冲突,这时将直接通告全组并回滚。注意,只要检测到了冲突,所有节点都会回滚,组复制不会为冲突事件进行投票决策。这里所投的票是非冲突方面的决策。

如果多个节点上出现了并发事务,因为写集是根据表中每行的主键值来计算的,所以不同事务如果修改同一行数据,它的写集就会相等,这表示并发事务出现了冲突。这时会将消息报告出去,并进行投票,投票的结果是先提交者获胜,失败的事务将回滚。

如果两个事务经常出现冲突,建议将这两个事务路由到同一节点上执行。

关于事务冲突问题,主要是多主模型下的问题,单主模型下只有单个主节点可写,不会出现事务冲突问题。单主模型下,如果两个事务修改的是同一行,第二个事务将会因为独占锁冲突而被阻塞。更多关于MySQL学习的文章,请参阅:死磕数据库系列之 MySQL ,本系列持续更新中。

小心DDL语句

在MySQL中,没有DDL事务,所有的DDL语句都无法保证原子性,无法回滚。但DDL语句毕竟会对表中数据产生影响,它是一个事件,必须被复制走。

先看一下DDL语句和DML语句在binlog中的记录格式的区别:(show binlog events in 'BINLOG_FILE';,为了排版,删掉了几个字段)

+-----------+-------------------------------------------------------------------+
| Gtid      | SET @@SESSION.GTID_NEXT= 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:2' |
| Query     create database gr_test                                           |
| Gtid      | SET @@SESSION.GTID_NEXT= 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:3' |
| Query     use `gr_test`create table t4(id int primary key)                |
| Gtid      | SET @@SESSION.GTID_NEXT= 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:4' |
| Query     BEGIN                                                             |
| Table_map | table_id: 117 (gr_test.t4)                                        |
| Write_rows| table_id: 117 flags: STMT_END_F                                   |
| Xid       | COMMIT /* xid=63 */                                               |
+-----------+-------------------------------------------------------------------+

上面的结果中,前4行是2个DDL语句代表的事件,后5行是一个DML语句代表的事件,它是一个分布式事务。不难看出,DDL语句从头到尾就只记录了它的GTID和语句部分。

换句话说,DDL语句不会进行冲突检测。这会出现两个可怕的陷阱:

陷阱一

如果多个节点并发执行多个DDL语句,后执行的DDL如果能正确执行,其实是在对前面的DDL进行覆盖。如果不能正确执行,将报错,例如个DDL语句的作用是删除一个字段,第二个DDL语句是修改这个字段的数据类型,这时第二个DDL语句将失去操作目标而报错。

陷阱二

如果多个节点并发执行DDL和DML语句,将可能出错。例如个DDL语句是truncate语句,它将删除表中所有数据,第二个DML语句则是update语句,显然第二个DML语句在truncate后已经失去了更新目标。

所以,不要执行并发的DDL+DDL语句,也不要执行并发的DDL+DML语句。

binlog中的特殊事件:视图更新

在binlog中,除了DDL语句、DCL语句(grant,revoke)语句、DML语句生成的事件,还有一种因组复制而存在的特殊事件:视图更新事件(view change)。

这个视图是什么视图?在组复制插件中,有一个内置的服务称为"成员管理服务"(group membership service)。这个服务负责维护组内的成员关系,并向外展示当前组内成员列表。这个成员关系,或者当前成员列表就是所谓的成员视图。

成员管理服务动态维护成员成员视图,每当有成员加组、离组时,都会自动触发成员视图更新的行为,这意味着让成员管理服务去重新配置当前的组成员。例如,一个新成员加组成功时,这个组的大小加1,离组时组大小减1。

加组、离组的过程稍许复杂,在解释它们之前,先解释下视图id和视图更新事件。

在成员加组、离组时会触发视图更改,它不是DDL、DCL、DML,但却也会当作一个事件写入binlog,这样才能在组内传播,让其它节点对视图更改进行投票。这个事件称为"视图更改事件"。

例如,以下是binlog中某次视图更改对应的事件。

View_change   | view_id=15294216022242634:2

在上面的示例中,有一个view_id,它是视图标识符,用来惟一地标识一个视图。它的格式由两部分组成:部分是创建组时随机生成的,在组停止(关闭)之前一直保持不变;第二部分是一个单调递增的整数,每次视图发生更改都递增一次。例如,以下是4个节点加入组时的binlog事件。

View_change   | view_id=15294216022242634:1     # 个节点创建组
View_change   | view_id=15294216022242634:2     # 第二个节点加入组
View_change   | view_id=15294216022242634:3     # 第三个节点加入组
View_change   | view_id=15294216022242634:4     # 第四个节点加入组

需要注意的是,加组失败时也会记录到binlog中,视图更改事件就像DDL/DCL事件一样,是非事务型的,不具有原子性和回滚性,只要发生了就会记录到binlog中,即使加组失败或者离组失败。

使用这种混合视图id的原因是,可以明确地标记当成员加入或离开时发生的组成员配置更改,也能标记所有成员离开组后没有任何信息保留在视图中。实际上,单纯使用单调递增的整数作为标识符会导致在组重启后重用视图ID,但显然这会破坏恢复过程所依赖的二进制日志标记的性。总而言之,部分标识这个组是从什么时候启动的,第二部分标识组在什么时间点发生了更改。

在组通信层(见本文开头的架构图),视图更改以及它们关联的视图id是加组之前和之后的区分边界。通过视图id,可以知道某个事务是视图阶段的。例如,一个新节点加入到组,对应的view_id为id1,那么id1之前的事务需要全部复制到新节点上,id1之后的事务是加组之后的事务,不需要复制给新节点(前文已经解释过,在赶上组中数据之前,这部分事务会放进新节点的事务缓存队列)。

其实,binlog中的view_change事件还充当另一个角色:组内其余节点感知新节点已经成功加组或成功离组。例如新成员加组的情况,当view_id写入binlog,表示这个新节点已经标记为ONLINE,其它节点知道它已经在线了,信息广播的时候也会广播给这个新节点,这个新节点也会占有一个法定票数。

成员加组、离组的细节

新成员加组的情况,其实前文已经解释过了,这里做个小回顾:触发视图更改事件,同时找一个donor,从这个donor上通过recovery通道异步复制它所缺失的数据,并监控新生成的事务放进事务缓存队列中,当事务缓存队列中的事务数量为0,将视图更改事件写入到binlog中,并将该节点标记为ONLINE,此时它才算是成功加入到组中。

成员离组的情况分为两种:自愿离组和非自愿离组。这两种离组方式造成的影响不同。

成员非自愿离组

除了自愿离组的情况(自愿离组见下一小节),所有离组的情况都是非自愿离组。比如节点宕机,断网等等。

  • 1.节点非自愿离组时,故障探测机制会检测到这个问题,于是向组中报告这个问题。然后会触发组视图成员自动配置,需要大多数节点同意新视图。
  • 2.非自愿离组时,组的大小不会改变,无论多少个节点的组,节点非自愿退出后,组大小还是5,只不过这些离组的节点被标记为非ONLINE。但注意,组的视图配置会改变,因为离组的节点状态需要标记为非ONLINE。
  • 3.非自愿离组时,会丢失法定票数。所以,当非自愿离组节点数量过多时,导致组中剩余节点数量达不到大多数的要求,组就会被阻塞。
  • 4.举个例子,5节点的组,非自愿退出1个节点A后,这个组的大小还是5,但是节点A在新的视图中被标记为unreachable或其他状态。当继续非自愿退出2个节点后,组中只剩下2个ONLINE节点,这时达不到大多数的要求,组就会被阻塞。

阻塞后的组只能手动干预,例如重启整个组。也可以对当前已阻塞的组强制更改组成员。例如,5个节点的组,当前只剩下2个节点为ONLINE,那么可以强制更新这个组,让这个组只包含这两个成员,也就是说强制更改组的大小。

SET GLOBAL group_replication_force_members="192.168.100.21:10000,192.168.100.22:10001";

这种方法必须只能作为后的手段,一个操作不当,就会导致脑裂。为什么会导致脑裂?

因为非自愿离开的成员可能并非下线了,而是出现了网络分区或其它原因将这个节点给隔离了。这样一来,这个节点会自认为自己是组中的成员,它不知道还有另一个甚至多个同名的组存在。虽然被隔离的节点因为不满足大多数的要求而被阻塞,但如果将这些隔离的组之一、之二等强制更改组大小,那么它们都会解除阻塞,允许写入新数据,从而出现数据不一致、脑裂等各种恶劣事件。

所以,当多个节点非自愿离组导致组被阻塞后,安全的方法是重启整个复制组。或者将所有被隔离的节点都完全下线,然后强制更改剩下的组让其解除阻塞,后再将下线的节点重新加入到这个组中。

成员自愿离组

只有一种情况是节点自愿离组的情况:执行stop group_replication;语句。

自愿离组时,待离组成员会触发视图更改事件,通知组内其它成员:老孙我现在要去寻仙拜师了,猴儿们别挂念你孙爷爷。然后组内的其它节点就当这个成员从未出现过一样,它的离开除了对组复制性能造成一点影响之外,没有其它任何影响。

节点自愿离组时,不会丢失法定票数。所以无论多少个节点自愿离组,都不会出现"达不到大多数"的要求而阻塞组。

举个例子,5个节点的组,陆陆续续地依次自愿退出了4个节点,无论哪个节点退出,都会触发视图更改事件更改组的大小,这样一来,永远也不会出现组被阻塞的问题。

非自愿离组触发的视图更改不会更改组的大小,离组节点过多,会无法达到大多数的要求而阻塞组;自愿离组触发的视图更改会更改组的大小,无论多少个节点自愿离组,剩下的节点总是组的全部,当然能满足组的大多数要求,因此绝不会阻塞组。更多关于MySQL学习的文章,请参阅:死磕数据库系列之 MySQL ,本系列持续更新中。

压缩组内的信息

使用组复制需要有一个低延迟、高带宽的网络环境,因为业务越繁忙,组内节点数量越多,组内要传递的消息就越多。如果突然遇到一个非常大的事务(例如load data infile中的数据非常多),可能会让组复制非常慢。

所以,如果业务中经常有大事务,或者网络带宽资源不足,可以考虑开启组内信息压缩功能。

例如,以下设置开启了压缩功能,压缩的阈值为2M左右。

STOP GROUP_REPLICATION;
SET GLOBAL group_replication_compression_threshold= 2097152;
START GROUP_REPLICATION;

当一个事务的消息超过2M时,就会将这个消息进行压缩。默认情况下已经开启了压缩功能,其阈值为1000000字节(大致1MB)。如果要关闭压缩功能,将阈值设置为0即可。

当组内其它节点收到了压缩的消息后,会进行解压,然后读取其中内容。

下图描述了压缩和解压发生的时间点:一般情况下,无需去修改压缩的阈值,除非出现了性能严重不足的情况。

使用组复制的要求和局限性

在真正开始使用组复制之前,件事不是去学会如何搭建组复制环境、不是学一大堆的理论,而是了解并记住它的要求和局限性,否则一不小心就会酿成大祸。

组复制的要求

要使用组复制,每个MySQL节点必须满足以下条件:

基本要求

InnoDB存储引擎:数据必须存储在事务型的InnoDB存储引擎中。事务以乐观形式执行,然后在提交前会检测冲突问题。如果有冲突,为了维护组中一致性,有些事务必须回滚。这意味着需要事务型的存储引擎。此外,InnoDB 存储引擎提供了一些额外的功能,它们结合组复制时能更好地管理和处理冲突。

Primary Keys:每张需要被组复制的表都必须显式定义一个主键。主键在判断事务是否冲突扮演极其重要的角色:通过主键来准确识别每个事务中修改了表中的哪些行。(实际上是将主机hash成写集,然后由certifier来并发事务之间的检测冲突性)

使用IPv4 地址:MySQL组复制使用的组通信引擎组件只支持 IPv4。因此,必须使用IPv4的网络。

良好的网络性能:组复制设计的初衷是部署在集群环境中,集群中的节点相互之间都非常近,因此除了网络延迟,网络带宽也会影响组复制。

配置上的要求

组中的每个成员都必须配置以下选项:

1.必须开启二进制日志:设置--log-bin[=log_file_name]。MySQL组复制会复制二进制日志的内容,因此必须开启二进制日志。

2.Slave Updates Logged:设置--log-slave-updates。节点需要记录applier已应用的日志。组中的每个节点都需要记录它们所接收到并应用的所有事务,这是必须的,因为恢复过程是依赖于组中参与者的二进制日志来进行的。因此,组中每个成员都必须保留每个事务的副本,即使某事务不是在该节点上开始的。

3.Row Format的二进制日志:设置--binlog-format=row。组复制依赖于基于行格式的二进制日志,以便在组中传播所发生的更改能保持一致性。而且,在探测组中不同节点间发生的并发事务是否冲突时,需要从行格式的日志中提取一些内容来做比较。

4.开启GTID复制:设置--gtid-mode=ON。组复制使用GTID(全局事务ID)来跟踪每个节点上已经提交了哪些事务。也因此可以推断出某节点上要执行的事务是否和已执行的事务(每个节点上都有副本)冲突。换句话说,GTID是整个组复制判断事务是否冲突的基础。

5.Replication Information Repositories:设置--master-info-repository=TABLE--relay-log-info-repository=TABLE。applier需要将 master 和 relay log 的元数据信息写入到系统表 mysql.slave_master_infomysql.slave_relay_log_info 中。这保证了组复制插件具有一致性恢复的能力和复制的元数据事务管理能力。

6.Transaction Write Set Extraction:设置--transaction-write-setextraction=XXHASH64,以便将行写入到二进制日志中时,节点也收集写集。写集基于每行的主键,是标识被更改行的标签的简化形式,该标签后续会用于探测事务冲突性。

7.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,却没有设置其他两项的新成员,将会报错并阻止它的加入。更多关于MySQL学习的文章,请参阅:死磕数据库系列之 MySQL ,本系列持续更新中。

组复制的限制(局限性)

下面是使用组复制已知的限制:

  • 1.Replication Event Checksums:由于对复制事件校验的设计缺陷,目前组复制不能使用它们。因此,需要设置--binlog-checksum=NONE
  • 2.Gap Locks:在验证阶段中(certification process),不会考虑 Gap Locks,因此在 InnoDB 的外部无法获取任何关于Gap 锁的信息。

注意:除非你的应用程序或业务需求依赖于REPEATABLE READ(MySQL默认该隔离级别),否则建议在组复制中使用READ COMMITTED隔离级别。在READ COMMITTED隔离级别中,InnoDB基本上不会使用Gap Locks,这将使得InnoDB自带的冲突探测能和组复制的冲突探测相互对齐从而保持一致。

  • 3.Table Locks and Named Locks:验证阶段(certification process)中不考虑表锁和命名锁。
  • 4.不支持 SERIALIZABLE 隔离级别:在多主模型下,默认不支持该隔离级别。如果在多主模型下设置了该隔离级别,将拒绝提交事务。
  • 5.不支持并发的 DDL 和 DML 操作:不支持在多主模型下不同节点上同时执行DDL和DML修改同一对象。在某节点上对某对象执行DDL语句时,如果在其他节点同时执行DML修改该对象,将有一定风险探测到冲突。(译注:是 DDL+DML 的并发,DDL+DDL 的并发也不允许。这是因为MySQL中没有DDL事务,不能保证DDL的原子性,当DDL和DML同时操作某一个对象,可能DDL修改后,DML将因为对象结构的改变而无法执行,继而回滚)
  • 6.不支持级联的外键约束:多主模型的组(所有节点都配置了group_replication_single_primary_mode=OFF)不支持多级外键依赖,特别是表上定义了级联的外键约束(CASCADING foreign key constraints)。这是因为多主模型下执行外键约束的级联操作可能会出现未检测到的冲突,从而导致组内成员间数据不一致。因此,我们推荐在使用多主模型时,在每个节点上都设置group_replication_enforce_update_everywhere_checks=ON以避免出现未检测到的冲突。在单主模型下没有这种问题,因为没有并发写操作,从而不可能会出现未被探测到的冲突。
  • 7.大事务可能会错误:如果一个事务非常大,导致GTID的内容非常多,以至于无法在 5 秒内通过网络传输完成,这时组成员间的通信将失败。要避免该问题,可以尽可能地限制事务的大小。例如,将LOAD DATA INFILE的文件切割为多个小块。
  • 8.多主模型可能出现死锁:在多主模型下,SELECT ... FOR UPDATE语句可能会导致死锁。这是因为组内成员之间不会共享锁资源(译注:share nothing),因此这样的语句可能达不到预期的结果。

如何排错?

在使用组复制过程中,限制多多,要求多多,难免的问题也多多,有些是比较严重的问题,有些是小问题。于是,如何进行排错?

我个人有几点总结:

  • 1.看报错信息。
  • 2.看错误日志。
  • 3.show binlog events仔细比对各节点的GTID。

组复制的流程控制

这部分浏览即可,一切都是自动的,人为可控度不高,几乎也不需要人为修改。

组复制只有在组内所有节点都收到了事务,且大多数成员对该事务所在顺序及其他一些相关内容都达成一致时才会进行事务的提交。

如果写入组的事务总数量都在组中任何成员的写入能力范围之内,则可良好运行下去。如果某节点或某些节点的写吞吐量较其他节点更差,则这些节点可能会落后。

当组中有一些成员落后了,可能会带来一些问题,典型的是读取这些落后节点上的数据都是比较旧的。根据节点为什么落后的原因,组内其他成员可能会保存更多或更少的上下文,以便满足落后成员潜在的数据传输请求。

好在复制协议中有一种机制,可以避免在应用事务时,快速成员和慢速成员之间拉开的距离太大。这就是所谓的流程控制(flow control)机制。该机制试图实现以下几个目标:

  • 保证成员足够接近,使得成员之间的缓冲和非同步只是一个小问题;
  • 快速适应不断变化的情况,例如不同的工作量、更多的写节点;
  • 给每个成员分配公平的可用写容量;
  • 不要因为避免浪费资源而过多地减少吞吐量。

可由两个工作队列来决定是否节流:(1)认证队列(certification);(2)二进制日志上的应用队列(applier)。当这两个队列中的任何一个超出队列大值(阈值)时,将会触发节流机制。只需配置:(1)是否要对ceritier或applier或两者做flow control;(2)每个队列的阈值是多少。

flow control依赖于两个基本的机制:

  • 监控组内每个成员收集到的有关于吞吐量和队列大小的一些统计数据,以便对每个成员可以承受的大写入压力做有根据的猜测;
  • 每当有成员的写入量试图超出可用容量公平份额,就对其节流限制。
出处:https://www.cnblogs.com/f-ck-need-u/
p/9197442.html




相关文章