MySQL学习笔记(1)——高可用组复制

MySQL学习笔记(1)——高可用组复制

积土成山,风雨兴焉。积水成渊,蛟龙生焉。

一、概念

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

例如具有3个节点的组:

img

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

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

二、组复制理论

2.1 组复制插件架构图

MySQL组复制是一个MySQL插件,它基于常规的MySQL复制,利用了基于行格式的二进制日志和GTID等特性。下图是MySQL组复制的整体框架图。

img

以下是对该图中各组件的大致介绍,涉及到的术语先浏览一遍,后面会详细解释。

  • 从上图的最顶端开始,有一系列的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,它抽象了构建一个复制状态机所需的属性,它从插件的更上层解耦了消息传递层。
    • 组通信引擎负责处理组内成员的通信。

2.2 组创建

当第一个节点启动组复制功能之前(不是启动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节点的概念。

2.3 新节点入组

新节点要加组,在配置好配置文件后,只需执行以下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).监控组中新产生的事务并放进自己的事务缓存队列中。

例如,在填补缺失的数据时,客户端向组中的可写节点插入了一条数据,这个数据不会被新节点的异步复制抓走,新节点会监控到这个事务。如下图:

img

所以将恢复过程概括一下:新节点的recovery组件通过异步复制通道从donor处获取p1之前的所有数据,并监控组中新生成的事务放入自己的事务缓存队列中。当新节点将p1之前的数据恢复完成后,将执行缓存队列中的事务,直到缓存中的事务数量减为0后,才表示新节点已经完全赶上了组中的数据,这是它才可以标识为ONLINE,并真正成为组中的一员。

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

2.4 执行单个事务

假设组中已经有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 */                                                   |
+------------+------------------------------------------------------------------------+

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

2.5 冲突检测

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

如果有多个客户端同时发起了事务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。

注意:

  1. 每个事务除了gtid,还有一个分布式的xid。这个id也必须全局唯一,但同一个事务在不同节点上的体现值不一样。例如,在s1节点上,事务的xid=200,应用到s2节点上,这个事务的xid会改为适应自己节点上的id,可能值为xid=222。
  2. 另外,这个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组件写入数据。如果验证不通过,意味着出现了事务冲突,这时将直接通告全组并回滚。注意,只要检测到了冲突,所有节点都会回滚,组复制不会为冲突事件进行投票决策。这里所投的票是非冲突方面的决策。

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

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

关于事务冲突问题,主要是多主模型下的问题,单主模型下只有单个主节点可写,不会出现事务冲突问题。单主模型下,如果两个事务修改的是同一行,第二个事务将会因为独占锁冲突而被阻塞。

2.6 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语句不会进行冲突检测。这会出现两个可怕的陷阱:

  1. 陷阱一:

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

  2. 陷阱二:

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

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

2.7 试图更新

在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,其它节点知道它已经在线了,信息广播的时候也会广播给这个新节点,这个新节点也会占有一个法定票数。

2.8 成员入组、离组

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

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

2.8.1 成员非自愿离组

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

  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";

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

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

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

2.8.2 成员自愿离组

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

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

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

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

非自愿离组触发的视图更改不会更改组的大小,离组节点过多,会无法达到大多数的要求而阻塞组;自愿离组触发的视图更改会更改组的大小,无论多少个节点自愿离组,剩下的节点总是组的全部,当然能满足组的大多数要求,因此绝不会阻塞组。

2.9 压缩组内信息

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

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

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

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

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

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

下图描述了压缩和解压发生的时间点:

img

一般情况下,无需去修改压缩的阈值,除非出现了性能严重不足的情况。

三、配置单主模型的组复制

MySQL的组复制可以配置为单主模型多主模型两种工作模式,它们都能保证MySQL的高可用。以下是两种工作模式的特性简介:

  • 单主模型:从复制组中众多个MySQL节点中自动选举一个master节点,只有master节点可以写,其他节点自动设置为read only。当master节点故障时,会自动选举一个新的master节点,选举成功后,它将设置为可写,其他slave将指向这个新的master。
  • 多主模型:复制组中的任何一个节点都可以写,因此没有master和slave的概念,只要突然故障的节点数量不太多,这个多主模型就能继续可用。

虽然多主模型的特性很诱人,但缺点是要配置和维护这种模式,必须要深入理解组复制的理论,更重要的是,多主模型限制较多,其一致性、安全性还需要多做测试。

而使用单主模型的组复制就简单的太多了,唯一需要知道的就是它会自动选举master节点这个特性,因为它的维护一切都是自动进行的,甚至对于管理人员来说,完全可以不用去了解组复制的理论。

虽然单主模型比多主模型的性能要差,但它没有数据不一致的危险,加上限制少,配置简单,基本上没有额外的学习成本,所以多数情况下都是配置单主模型的组复制,即使是PXC和MariaDB也如此。

3.1 单主模型组复制的理论基础

如下图,master节点为s1,其余节点为slave节点。

img

组复制一切正常时,所有的写操作都路由到s1节点上,所有的读操作都路由到s2、s3、s4或s5上。当s1节点故障后,组复制自动选举新的master节点。假如选举s2为新master成功后,s3、s4和s5将指向s2,写操作将路由到s2节点上。

至于如何改变客户端的路由目标,这不是组复制应该考虑的事情,而是客户端应用程序应该考虑的事情。实际上,更好的方式是使用中间件来做数据库的路由,比如MySQL Router、ProxySQL、amoeba、cobar、mycat。

3.1.1 如何加入新节点

上面一直说,单主模型是自动选举主节点的,那么如何选举?

首先,在第一个MySQL节点s1启动时,一般会将其设置为组的引导节点,所谓引导就是在启动组复制功能时去创建一个复制组。当然,这并非强制要求,也可以设置第二个启动节点作为组的引导节点。因为组内没有其他节点,所以这第一个节点会直接选为master节点。

然后,如果有第二个节点要加入组时,新节点需要征得组的同意,因为目前只有一个节点,所以只需s1节点同意即可。新节点在加入组时,首先会联系s1,与s1建立异步复制的通道,并从s1节点处获取s2上目前缺失的数据,等到s1和s2节点上的数据同步后,s2节点就会真正成为组中的新成员。当然,实际过程要比这里复杂一些。

如果还有新节点(比如s3节点)继续加入组,s3将从s1或s2中选一个,并与之建立异步复制的通道,然后获取缺失的数据,同步结束后,如果s1和s2都同意s3加入,那么s3将会组中的新成员。其余节点加入组也依次类推。

有两点需要注意:

  1. 新节点加入组时,如何选择联系对象?

    上面说加入第二个节点s2时会联系s1,加入s3时会联系s1、s2中的任意一个。实际上,新节点加入组时联系的对象,称为donor,意为数据供应者。新节点会和选中的donor建立异步复制通道,并从donor处获取缺失的数据。

    在配置组复制时,需要指定种子节点列表。当新节点加入组时,只会联系种子节点,也即是说,只有种子节点列表中的节点才有机会成为donor,没有在种子节点列表中的节点不会被新节点选中。但建议,将组中所有节点都加入到种子列表中

    当联系第一个donor失败后,会向后联系第二个donor,再失败将联系第三个donor,如果所有种子节点都联系失败,在等待一段时间后再次从头开始联系第一个donor。依此类推,直到加组失败报错。

  2. 新节点加入组时,需要征得哪些节点的同意?

    实际上,新节点加组涉及到组的决策:是否允许它加组。在组复制中,所有的决策都需要组中大多数节点达成一致,也即是达到法定票数。所谓大多数节点,指的是N/2+1(N是组中目前节点总数),例如目前组中有5个节点,则需要3个节点才能达到大多数的要求。有两点需要注意:

    1. 新节点加入组时,如何选择联系对象?

      上面说加入第二个节点s2时会联系s1,加入s3时会联系s1、s2中的任意一个。实际上,新节点加入组时联系的对象,称为donor,意为数据供应者。新节点会和选中的donor建立异步复制通道,并从donor处获取缺失的数据。

      在配置组复制时,需要指定种子节点列表。当新节点加入组时,只会联系种子节点,也即是说,只有种子节点列表中的节点才有机会成为donor,没有在种子节点列表中的节点不会被新节点选中。但建议,将组中所有节点都加入到种子列表中

      当联系第一个donor失败后,会向后联系第二个donor,再失败将联系第三个donor,如果所有种子节点都联系失败,在等待一段时间后再次从头开始联系第一个donor。依此类推,直到加组失败报错。

    2. 新节点加入组时,需要征得哪些节点的同意?

      实际上,新节点加组涉及到组的决策:是否允许它加组。在组复制中,所有的决策都需要组中大多数节点达成一致,也即是达到法定票数。所谓大多数节点,指的是N/2+1(N是组中目前节点总数),例如目前组中有5个节点,则需要3个节点才能达到大多数的要求。

3.1.2 如何选举新的master

当主节点s1故障后,组复制的故障探测机制就能发现这个问题并报告给组中其他成员,组中各成员根据收集到的其他成员信息,会比较各成员的权重值(由变量group_replication_member_weigth控制),权重值最高的优先成为新的Master。如果有多个节点具有相同的最高权重值,会按字典顺序比较它们的server_uuid值,最小的(升序排序,最小值在最前面)优先成为新的master。

但需要注意,变量group_replication_member_weigth是从MySQL 5.7.20开始提供的,在MySQL 5.7.17到5.7.19之间没有该变量。此时将根据它们的server_uuid值进行排序选举。具体的规则可自行测试。

3.1.3 最多允许多少个节点故障

MySQL组复制使用Paxos分布式算法来提供节点间的分布式协调。正因如此,它要求组中大多数节点在线才能达到法定票数,从而对一个决策做出一致的决定。

大多数指的是N/2+1(N是组中目前节点总数),例如目前组中有5个节点,则需要3个节点才能达到大多数的要求。所以,允许出现故障的节点数量如下图:

img

3.1.4 单主模型组复制的要求

基本要求

  1. InnoDB存储引擎:数据必须存储在事务型的InnoDB存储引擎中。事务以乐观形式执行,然后在提交前会检测冲突问题。如果有冲突,为了维护组中一致性,有些事务必须回滚。这意味着需要事务型的存储引擎。此外,InnoDB 存储引擎提供了一些额外的功能,它们结合组复制时能更好地管理和处理冲突。
  2. Primary Keys:每张需要被组复制的表都必须显式定义一个主键。主键在判断事务是否冲突扮演极其重要的角色:通过主键来准确识别每个事务中修改了表中的哪些行。(实际上是将主机hash成写集,然后由certifier来并发事务之间的检测冲突性)
  3. 使用IPv4 地址:MySQL组复制使用的组通信引擎组件只支持 IPv4。因此,必须使用IPv4的网络。
  4. 良好的网络性能:组复制设计的初衷是部署在集群环境中,集群中的节点相互之间都非常近,因此除了网络延迟,网络带宽也会影响组复制。

配置要求

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

  1. 必须开启二进制日志:设置--log-bin[=log_file_name]。MySQL组复制会复制二进制日志的内容,因此必须开启二进制日志。
  2. Slave Updates Logged:设置--log-slave-updates。节点需要记录applier已应用的日志。组中的每个节点都需要记录它们所接收到并应用的所有事务,这是必须的,因为恢复过程是依赖于组中参与者的二进制日志来进行的。因此,组中每个成员都必须保留每个事务的副本,即使某事务不是在该节点上开始的。
  3. Row Format的二进制日志:设置--binlog-format=row。组复制依赖于基于行格式的二进制日志,以便在组中传播所发生的更改能保持一致性。而且,在探测组中不同节点间发生的并发事务是否冲突时,需要从行格式的日志中提取一些内容来做比较。
  4. 开启GTID复制:设置
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值