MySQL组复制系列文章:html
这一篇对MySQL组复制作个详细的整理和解释,是MySQL组复制官方手册的整理版和总结。mysql
MySQL组复制是一个MySQL插件,它基于常规的MySQL复制,利用了基于行格式的二进制日志和GTID等特性。下图是MySQL组复制的总体框架图。算法
如下是对该图中各组件的大体介绍,涉及到的术语先浏览一遍,后面会详细解释。sql
MySQL组复制是MySQL 5.7.17开始引入的新功能,为主从复制实现高可用功能。它支持单主模型和多主模型两种工做方式(默认是单主模型)。bootstrap
当第一个节点启动组复制功能以前(不是启动mysql实例,而是启动组复制插件的功能),通常会将这个节点设置为组的引导节点,这不是必须的,但除非特殊调试环境,没人会吃撑了用第2、第三个节点去引导组。缓存
所谓引导组,就是建立组。组建立以后,其余节点才能加入到组中。安全
将某节点设置为引导节点的方式是在该节点上设置如下变量为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节点的概念。
新节点要加组,在配置好配置文件后,只需执行如下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行中设置的值直接修改数据,而不会完整地执行事务逻辑。换句话说,事务发起节点修改数据的速度没有其余节点快,尽管它们是同时提交的。
假设组中已经有5个节点(s一、s二、s三、s四、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 */ | +------------+------------------------------------------------------------------------+
问题是,组内全部节点数据和状态都是同步的,为何还要对事务进行决策?在主节点上判断事务可否执行、可否提交不就能够了吗?对于单主模型下执行正常的事务来讲确实如此。可是多主模型下多个节点可执行并发事务,并且组复制内部也有些特殊的事件(例如成员加组、离组),这些都须要获得大多数节点的赞成才能经过,因此必须先进行决策。后文会逐一解释这些状况。
多个事务分两种:线性的事务和并发的事务。线性的多个事务和执行单个事务没有什么区别,无非是一个一个事务从上到下执行下去。并发事务就要多方面考虑。
若是有多个客户端同时发起了事务t一、t二、t三、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的后缀可能会是14六、14七、148。3个s2节点的事务,它们的gtid后缀可能会是100003三、100003四、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后面。
回到正题,并发事务t一、t二、t三、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个要求都是为了冲突检测作准备:
当在某节点发起事务后,这个节点的replication协议模块(该模块的位置见文章开头的架构图)会收集该事务将要修改行的写集(write-set,术语),写集是根据每行的主键进行hash计算的,是否记得在组复制的配置文件中有一行:
transaction_write_set_extraction=XXHASH64
这就是指定以"XXHASH64"算法将主键hash为写集。而后该协议模块将binlog和写集一块儿传播出去,并被其它节点的replication协议模块收到。当其它节点收到后,会对写集进行验证,这个过程称为certify,它由certifier线程完成。若是验证经过,则为此事务投上本身的一票,多数节点经过后交给applier组件写入数据。若是验证不经过,意味着出现了事务冲突,这时将直接通告全组并回滚。注意,只要检测到了冲突,全部节点都会回滚,组复制不会为冲突事件进行投票决策。这里所投的票是非冲突方面的决策。
若是多个节点上出现了并发事务,由于写集是根据表中每行的主键值来计算的,因此不一样事务若是修改同一行数据,它的写集会相等,这表示并发事务出现了冲突。这时会将消息报告出去,并进行投票,投票的结果是先提交者获胜,失败的事务将回滚。
若是两个事务常常出现冲突,建议将这两个事务路由到同一节点上执行。
关于事务冲突问题,主要是多主模型下的问题,单主模型下只有单个主节点可写,不会出现事务冲突问题。单主模型下,若是两个事务修改的是同一行,第二个事务将会由于独占锁冲突而被阻塞。
在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中,除了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,此时它才算是成功加入到组中。
成员离组的状况分为两种:自愿离组和非自愿离组。这两种离组方式形成的影响不一样。
除了自愿离组的状况(自愿离组见下一小节),全部离组的状况都是非自愿离组。好比节点宕机,断网等等。
阻塞后的组只能手动干预,例如重启整个组。也能够对当前已阻塞的组强制更改组成员。例如,5个节点的组,当前只剩下2个节点为ONLINE,那么能够强制更新这个组,让这个组只包含这两个成员,也就是说强制更改组的大小。
SET GLOBAL group_replication_force_members="192.168.100.21:10000,192.168.100.22:10001";
这种方法必须只能做为最后的手段,一个操做不当,就会致使脑裂。为何会致使脑裂?
由于非自愿离开的成员可能并不是下线了,而是出现了网络分区或其它缘由将这个节点给隔离了。这样一来,这个节点会自认为本身是组中的惟一成员,它不知道还有另外一个甚至多个同名的组存在。虽然被隔离的节点由于不知足大多数的要求而被阻塞,但若是将这些隔离的组之1、之二等强制更改组大小,那么它们都会解除阻塞,容许写入新数据,从而出现数据不一致、脑裂等各类恶劣事件。
因此,当多个节点非自愿离组致使组被阻塞后,最安全的方法是重启整个复制组。或者将全部被隔离的节点都彻底下线,而后强制更改剩下的组让其解除阻塞,最后再将下线的节点从新加入到这个组中。
只有一种状况是节点自愿离组的状况:执行stop group_replication;
语句。
自愿离组时,待离组成员会触发视图更改事件,通知组内其它成员:老孙我如今要去寻仙拜师了,猴儿们别挂念你孙爷爷。而后组内的其它节点就当这个成员从未出现过同样,它的离开除了对组复制性能形成一点影响以外,没有其它任何影响。
节点自愿离组时,不会丢失法定票数。因此不管多少个节点自愿离组,都不会出现"达不到大多数"的要求而阻塞组。
举个例子,5个节点的组,陆陆续续地依次自愿退出了4个节点,不管哪一个节点退出,都会触发视图更改事件更改组的大小,这样一来,永远也不会出现组被阻塞的问题。
非自愿离组触发的视图更改不会更改组的大小,离组节点过多,会没法达到大多数的要求而阻塞组;自愿离组触发的视图更改会更改组的大小,不管多少个节点自愿离组,剩下的节点老是组的所有,固然能知足组的大多数要求,所以毫不会阻塞组。
使用组复制须要有一个低延迟、高带宽的网络环境,由于业务越繁忙,组内节点数量越多,组内要传递的消息就越多。若是忽然遇到一个很是大的事务(例如load data infile
中的数据很是多),可能会让组复制很是慢。
因此,若是业务中常常有大事务,或者网络带宽资源不足,能够考虑开启组内信息压缩功能。
例如,如下设置开启了压缩功能,压缩的阈值为2M左右。
STOP GROUP_REPLICATION; SET GLOBAL group_replication_compression_threshold= 2097152; START GROUP_REPLICATION;
当一个事务的消息超过2M时,就会将这个消息进行压缩。默认状况下已经开启了压缩功能,其阈值为1000000字节(大体1MB)。若是要关闭压缩功能,将阈值设置为0便可。
当组内其它节点收到了压缩的消息后,会进行解压,而后读取其中内容。
下图描述了压缩和解压发生的时间点:
通常状况下,无需去修改压缩的阈值,除非出现了性能严重不足的状况。
组内的不少信息均可以被监控,它们都记录在mysql的performance_schema架构中。
这部分没什么理论性的内容,详细内容见个人翻译:监控MySQL组复制。
在真正开始使用组复制以前,第一件事不是去学会如何搭建组复制环境、不是学一大堆的理论,而是了解并记住它的要求和局限性,不然一不当心就会酿成大祸。
详细内容见个人翻译:MySQL组复制的限制和局限性。
在使用组复制过程当中,限制多多,要求多多,不免的问题也多多,有些是比较严重的问题,有些是小问题。因而,如何进行排错?
我我的有几点总结:
show binlog events
仔细比对各节点的GTID。好像都是些废话啊。我在配置单主模型的组复制和配置多主模型的组复制中很是详细介绍了配置组复制的步骤,除此以外还给出了几个我遇到过的几个问题和解决方案。
这部分浏览便可,一切都是自动的,人为可控度不高,几乎也不须要人为修改。
组复制只有在组内全部节点都收到了事务,且大多数成员对该事务所在顺序及其余一些相关内容都达成一致时才会进行事务的提交。
若是写入组的事务总数量都在组中任何成员的写入能力范围以内,则可良好运行下去。若是某节点或某些节点的写吞吐量较其余节点更差,则这些节点可能会落后。
当组中有一些成员落后了,可能会带来一些问题,典型的是读取这些落后节点上的数据都是比较旧的。根据节点为何落后的缘由,组内其余成员可能会保存更多或更少的上下文,以便知足落后成员潜在的数据传输请求。
好在复制协议中有一种机制,能够避免在应用事务时,快速成员和慢速成员之间拉开的距离太大。这就是所谓的流程控制(flow control)机制。该机制试图实现如下几个目标:
可由两个工做队列来决定是否节流:(1)认证队列(certification);(2)二进制日志上的应用队列(applier)。当这两个队列中的任何一个超出队列最大值(阈值)时,将会触发节流机制。只需配置:(1)是否要对ceritier或applier或二者作flow control;(2)每一个队列的阈值是多少。
flow control依赖于两个基本的机制: