目录
一、同步复制
二、Galera复制架构
1. wsrep api
2. 全局事务ID(global transaction id,GTID)
3. Galera复制插件
4. 组通信插件
三、Galera复制工作原理
四、状态转移
1. 状态快照传输
2. 增量状态转移
3. 写集缓存(gcache)
五、流控
1. 流控原理
2. 理解节点状态
3. 节点状态与流控
六、单节点故障与恢复
七、仲裁
1. 加权法定票数(Weighted Quorum)
2. 裂脑(Split-Brain)
3. 法定票数计算
4. 加权仲裁示例
参考:
Galera Cluster是由Codership开发的MySQL多主集群,包含在MariaDB中,同时支持Percona xtradb、MySQL,是一个易于使用的高可用解决方案,在数据完整性、可扩展性及高性能方面都有可接受的表现。图1所示为一个三节点Galera 集群,三个MySQL实例是对等的,互为主从,这被称为多主(multi-master)架构。当客户端读写数据时,可连接任一MySQL实例。对于读操作,从每个节点读取到的数据都是相同的。对于写操作,当数据写入某一节点后,集群会将其同步到其它节点。这种架构不共享任何数据,是一种高冗余架构。
Galera集群具有以下特点:
Galera集群复制要求数据库系统支持事务,因此仅支持MySQL的Innodb存储引擎,并且多主模式下只能使用可重复读隔离级别。
不同于MySQL原生的主从异步复制,Galera采用的是多主同步复制,如图2所示。
异步复制中,主库将数据更新传播给从库后立即提交事务,而不论从库是否成功读取或重放数据变化。这种情况下,在主库事务提交后的短时间内,主从库数据并不一致。同步复制时,主库的单个更新事务需要在所有从库上同步更新。换句话说,当主库提交事务时,集群中所有节点的数据保持一致。
相对于异步复制,同步复制的优点主要体现在以下几方面:
当然,同步复制的缺点也显而易见,这主要源于其实现方式。同步复制协议通常使用两阶段提交或分布式锁协调不同节点的操作。假设集群有n个节点,每秒处理o个操作,每个操作中包含t个事务,则每秒将在网络中产生 n*o*t 条消息。这意味着随着节点数量的增加,事务冲突和死锁的概率将呈指数级增加。这也是MySQL缺省使用异步复制的主要原因。
为解决传统同步复制的问题,现已提出多种数据库同步复制的替代方法。除理论外,一些原型实现也显示出了很大的希望,如以下重要改进:
Galera集群就是基于这些方法构建的。可以看到Galera复制的原理与实现与MySQL组复制有很多相似之处。为了更好地理解Galera,在深入细节之前,先将它和MySQL组复制作一类比,如下表所示。
对比项 |
Galera |
MySQL Group Replication |
组通信系统(Group Communication System) |
专有组通信系统GComm,所有节点都必须有 ACK 消息 |
基于 Paxos,只要求大多数节点有 ACK 消息 |
二进制日志(Binlog) |
不需要二进制日志,将二进制行事件写入Gcache |
需要二进制日志 |
节点配置(Node Provisioning) |
自动全量同步(State Snapshot Transfer,SST)与增量同步(Incremental State Transfer,IST) |
没有自动全量同步,使用异步复制通道 |
全局事务ID(GTID) |
使用状态UUID和递增序列号 |
依赖GTID,集群上的写操作产生GTID事件 |
分区控制(Partition Handling) |
分区节点拒绝读写,自动恢复并重新加入集群 |
分区节点可读,接受写请求但将永久挂起,需要手工重新加入集群 |
流控(Flow Control) |
当一个节点慢到一个限制值,阻止所有节点写 |
每个节点都有所有成员的统计信息,独立决定该节点写的阈值。如果有节点慢到阈值,其它节点放慢写速度。 |
DDL支持 |
总序隔离(Total Order Isolation,TOI),DDL执行期间,所有写入都将被阻止 |
DDL 并不会阻塞写,仅建议在单主模式下使用(因为 DDL 并没有冲突检测) |
同步复制系统中的节点将通过单个事务更新副本,从而与所有其它节点同步。这意味着当事务提交时,所有节点都将具有相同的值。此过程通过组通信使用写集复制进行。
Galera集群的内部架构包含四个组件,如图3所示:
wsrep api是数据库的通用复制插件接口,定义了一组应用程序回调和复制插件调用函数。wsrep api将数据库中的数据改变视为一种状态变化,当客户端修改数据库内容时,其状态将更改。wsrep api将数据库状态更改表示为一系列事务。集群中的所有节点始终具有相同状态,它们通过以相同的顺序复制和应用状态更改来相互同步。从更技术角度看,Galera集群使用以下方式处理状态更改:
在MySQL社区中,GTID的概念并不新鲜,MySQL中的GTID由Master生成,是用于标记唯一事务并通过ID定位binlog位置的一种手段,从而有效解决了级联复制等场景中的各种问题。
对Galera Cluster而言,复制不基于binlog,而是通过Galera复制插件来保障。Galera的GTID同样也标记事务唯一性,wsrep api使用GTID识别状态更改。Galera的GTID格式如下:
45eec521-2f34-11e0-0800-2a36050b826b:94530586304
GTID由两部分组成:
Galera复制插件实现wsrep api,作为wsrep provider运行。Galera复制插件由以下组件构成:
组通信框架为各种gcomm系统提供了一个插件体系结构。Galera集群建立在专有的组通信系统层之上,实现虚拟同步。所谓虚拟同步,简单说是指一个事务在一个节点上执行成功后,保证它在其它节点也一定会被成功执行,但并不能保证实时同步。为了解决实时性问题,Galera集群实现了自己的运行时可配置的时态流控。
组通信框架还使用GTID提供来自多个源的消息总序(Total Order)。在传输层上,Galera集群是一个对称的无向图,所有节点都通过TCP相互连接。默认情况下,TCP用于消息复制和群集成员资格服务,但也可以使用udp多播在LAN中进行复制。
Galera复制是一种基于验证的复制,以这两篇论文为理论基础:Don’t be lazy, be consistent 和 Database State Machine Approach。基于验证的复制使用组通信和事务排序技术实现同步复制。它通过广播并发事务之间建立的全局总序来协调事务提交。简单说就是事务必须以相同的顺序应用于所有实例。事务在本节点乐观执行,然后在提交时运行一个验证过程以保证全局数据一致性。所谓乐观执行是指,事务在一个节点提交时,被认为与其它节点上的事务没有冲突,首先在本地执行,然后再发送到所有节点做冲突检测,无冲突时在所有节点提交,否则在所有节点回滚。Galera复制原理如图4所示:
当客户端发出commit命令时,在实际提交之前,对数据库所做的更改都将被收集到一个写集中,写集中包含事务信息和所更改行的主键。然后,数据库将此写集发送到所有其它节点。节点用写集中的主键与当前节点中未完成事务的所有写集(不仅包括当前节点其它事务产生的写集,还包括其它节点传送过来的写集)的主键相比较,确定节点是否可以提交事务。同时满足以下三个条件则验证失败(存在冲突):
验证失败后,节点将删除写集,集群将回滚原始事务。对于所有的节点都是如此,每个节点单独进行验证。因为所有节点都以相同的顺序接收事务,它们对事务的结果都会做出相同的决定,要么全成功,要么都失败。成功后自然就提交了,所有的节点又会重新达到数据一致的状态。节点之间不交换“是否冲突”的信息,各个节点独立异步处理事务。由此可见,Galera本身的数据也不是严格同步的,很明显在每个节点上的验证是异步的,这也就是前面提到的“虚拟同步”。
最后,启动事务的节点可以通知客户端应用程序是否提交了事务。
当一个新节点加入集群时,数据将从集群复制到这个节点,这是一个全自动的过程,Galera将此称为状态转移。前面介绍Galera架构时曾提到,wsrep api将集群中的数据改变视为状态改变,因此这里将数据同步称作状态转移也就不足为怪了。Galera集群中有两种状态转移方法:
当有新节点加入时,集群会选择出一个捐献者(Donor)节点为新节点提供数据,这点与MySQL组复制类似。
新节点加入集群时会启动状态快照传输(SST),将其数据与集群同步。Galera支持rsync、rsync_-wan、xtrabackup、mysqldump四种状态快照传输方法,由系统变量wsrep_sst_method指定,缺省为rsync。
rsync、rsync_-wan、xtrabackup三种方法是物理备份,将数据文件直接从捐献者服务器复制到新节点服务器,并在传输后初始化接收服务器,其中xtrabackup方式可实现捐赠者无阻塞数据同步。这些方法比mysqldump快很多。
mysqldump方法是逻辑备份,要求用户手动初始化接收服务器,并在传输之前准备好接受连接。这是一种阻塞方法,在传输期间,捐赠节点变为只读。mysqldump是状态快照传输最慢的方法,不建议在生产环境使用。
增量状态转移(IST)只向新节点发送它所缺失的事务。使用IST需要满足两个先决条件:
满足这些条件时,捐助节点单独传输缺失的事务,并按顺序重放它们,直到新节点赶上集群。例如,假设集群中有一个节点落后于集群。此节点携带的节点状态如下:
5a76ef62-30ec-11e1-0800-dba504cf2aab:197222
同时,集群上的捐助节点状态为:
5a76ef62-30ec-11e1-0800-dba504cf2aab:201913
集群上的捐助节点从加入节点接收状态转移请求。它检查自身写集缓存中的序列号197223。如果该序号在写集缓存中不可用,则会启动SST。否则捐助节点将从197223到201913的提交事务发送到新加入节点。增量状态传输的优点是可以显著加快节点合并到集群的速度。另外,这个过程对捐赠者来说是非阻塞的。
增量状态传输最重要的参数是捐助节点上的gcache.size,它控制分配多少系统内存用于缓存写集。可用空间越大,可以存储的写集越多。可以存储的写集越多,通过增量状态传输可以弥合的事务间隙就越大。另一方面,如果写集缓存远大于数据库大小,则增量状态传输开始时的效率低于发送状态快照。
Galera群集将写集存储在一个称为gcache的特殊缓存中。gcache使用三种类型的存储:
Galera集群使用一种分配算法,尝试按上述顺序存储写集。也就是说,它首先尝试使用永久内存存储,如果没有足够的空间用于写入集,它将尝试存储到永久环缓冲区文件。除非写入集大于可用磁盘空间,否则页面存储始终成功。
注意,如果gcache.recover参数设置为yes,则在启动时将尝试恢复gcache,以便该节点可以继续向其它节点提供IST服务。如果设置为no(缺省),gcache将在启动时失效,节点将只能为SST提供服务。
Galera集群内部使用一种称为流控的反馈机制来管理复制过程。流控允许节点根据需要暂停和恢复复制,这可以有效防止任一节点在应用事务时落后其它节点太多。
从Galera集群同步复制(虚拟同步)原理可知,事务的应用和提交在各个节点上异步发生。节点从集群接收但尚未应用和提交的事务将保留在接收队列中。由于不同节点之间执行事务的速度不一样,慢节点的接收队列会越积越长。当接收队列达到一定大小时,节点触发流控,作用就是协调各个节点,保证所有节点执行事务的速度大于队列增长速度。流控的实现原理很简单:整个Galera集群中,同时只有一个节点可以广播消息,每个节点都会获得广播消息的机会(获得机会后也可以不广播)。当慢节点的接收队列超过一定长度后,它会广播一个FC_PAUSE消息,所有节点收到消息后都会暂缓广播消息,直到该慢节点的接收队列长度减小到一定长度后再恢复复制。
流控相关参数如下:
一个节点在Galera集群中可能经历的节点状态有Open、Primary、Joiner、Joined、Synced、Donor。可以通过wsrep_local_state和wsrep_local_state_comment系统变量查看节点的当前状态。节点状态更改如图5所示:
Galera集群根据节点状态实现多种形式的流控以保证数据一致性。有四种主要流控类型:
当一个节点因为硬件、软件、网络等诸多原因与集群失去联系时,都被概括为节点故障。从集群的角度看,主组件看不到出问题的节点,它将会认为该节点失败。从故障节点本身的角度来看,假设它没有崩溃,那么唯一的迹象是它失去了与主组件的连接。可以通过轮询wsrep_local_state状态变量监控Galera群集节点的状态,值及其含义见上节流控中的描述。
集群检查从节点最后一次接收到数据包的时间确定该节点是否连接到集群,检查的频率由evs.inactive_check_period参数指定,缺省值为每隔0.5秒检查一次。在检查期间,如果群集发现自上次从节点接收网络数据包以来的时间大于evs.keepalive_period参数的值(缺省值为1秒),则它将开始发出心跳信号。如果集群在evs.suspect_timeout参数(缺省值为5秒)期间没有继续从节点接收到网络数据包,则该节点被声明为suspect,表示怀疑该节点已下线。一旦主组件的所有成员都将该节点视为可疑节点,它就被声明为inactive,即节点失败。如果在大于evs.inactive_timeout(缺省值为15秒)的时间内未从节点接收到消息,则无论意见是否一致,都会声明该节点失败。在所有成员同意其成员资格之前,失败节点将保持非操作状态。如果成员无法就节点的活跃性达成一致,说明网络对于集群操作来说太不稳定。
这些选项值之间的关系为:
evs.inactive_check_period <= evs.keepalive_period <= evs.suspect_timeout <= evs.inactive_timeout
需要注意,如果网络过于繁忙,以至于无法按时发送消息或心跳信号无响应,也可能被宣布为节点失败,这可以防止集群其余部分的操作被锁。如果不希望这样处理,可以增加超时参数。如果用CAP原则来衡量,Galera集群强调的是数据一致性(Consistency),这就导致了集群需要在可用性(Availability)和分区容忍性(Partition tolerance)之间进行权衡。也就是说,当使用的网络不稳定时,低evs.suspect_timeout和evs.inactive_timeout值可能会导致错误的节点故障检测结果,而这些参数的较高值可能会导致在实际节点故障的情况下更长的发现时间。
集群中的一个节点出现故障不会影响其它节点继续正常工作,单节点故障不会丢失任何数据。失败节点的恢复是自动的。当失败节点重新联机时,它会自动与其它节点同步数据,之后才允许它重新回到集群中。如果重新同步过程中状态快照传输(SST)失败,会导致接收节点不可用,因为接收节点在检测到状态传输故障时将中止。这种情况下若使用的是mysqldump方式的SST,需要手动还原。
除了单节点故障外,群集还可能由于网络故障而拆分为多个部分。每部分内的节点相互连接,但各部分之间的节点失去连接,这被称为网络分裂(network partitioning)。此情况下只有一部分可以继续修改数据库状态,以避免数据差异,这一部分即为主组件。正常情况下主组件就是整个集群。当发生网络分裂时,Galera集群调用一个仲裁算法选择一部分作为主组件,保证集群中只有一个主组件。
集群中的当前节点数量定义了当前集群的大小,群集大小决定达到仲裁所需的票数。Galera集群在节点不响应并且被怀疑不再是集群的一部分时进行仲裁投票。可以使用evs.suspect_timeout参数微调此无响应的超时时间,默认为5秒。
发生网络分裂时,断开连接的两侧都有活动节点。主组件要求获得仲裁的多数票,因此具有较多存活节点的部分将成为主组件,而另一部分将进入非主状态并开始尝试与主组件连接,如图6所示。
仲裁要求多数,这意味着不能在双节点群集中进行自动故障转移,因为一个节点的故障会导致另一节点自动进入非主状态。而具有偶数个节点的集群则有脑裂风险。如果在网络分裂导致节点的数量正好分成两半,则两个分区都不能成为主组件,并且都进入非主状态,如图7所示。要启用Galera集群自动故障切换,至少需要使用三个节点。
导致数据库节点彼此独立运行的集群故障称为“脑裂”。这种情况可能导致数据不一致,并且无法修复,例如当两个数据库节点独立更新同一表上的同一行时。与任何基于仲裁的系统一样,当仲裁算法无法选择主组件时,Galera集群会受到脑裂影响。
Galera设计为避免进入分裂脑状态,如果失败导致将集群分割为两个大小相等的部分,则两部分都不会成为主组件。在节点数为偶数的集群中,为把脑裂风险降到最低,可以人为分区将一部分始终划分为集群主组件,如:
4 node cluster -> 3 (Primary) + 1 (Non-primary)
6 node cluster -> 4 (Primary) + 2 (Non-primary)
6 node cluster -> 5 (Primary) + 1 (Non-primary)
以上分区示例中,任何中断或失败都很难导致节点完全分成两半。
Galera群集支持加权仲裁,其中每个节点可以被分配0到255范围内的权重参与计算。法定票数计算公式为:
其中:
这个公式的含义是:当且仅当当前节点权重总和大于最后一个主组件节点权重和减去正常离开集群节点权重和的一半时,才会被选为新的主组件。
消息传递时带有权重信息。缺省的节点权重为1,此时公式被转换为单纯的节点计数比较。通过设置pc.weight参数,可以在运行时更改节点权重,例如:
set global wsrep_provider_options="pc.weight=3";
在了解了加权仲裁的工作原理后,下面是一些部署模式的示例。
(1)三个节点的加权仲裁
三个节点配置仲裁权重如下:
node1: pc.weight = 2
node2: pc.weight = 1
node3: pc.weight = 0
此时如果node2和node3失效,node1会成为主组件,而如果node1失效,node2和node3都将成为非主组件。
(2)一主一从方案的加权仲裁
主、从节点配置仲裁权重如下:
node1: pc.weight = 1
node2: pc.weight = 0
如果主节点失效,node2将成为非主组件,如果node2失效,node1将继续作为主组件。
(3)一主多从方案的加权仲裁
为具有多个从节点的主从方案配置仲裁权重:
node1: pc.weight = 1
node2: pc.weight = 0
node3: pc.weight = 0
...
noden: pc.weight = 0
如果node1失效,所有剩余的节点都将作为非主组件,如果任何其它节点失效,则保留主组件。在网络分裂的情况下,node1始终作为主组件。(4)主站点和从站点方案的加权仲裁
为主站点和从站点配置仲裁权重:
Primary Site:
node1: pc.weight = 2
node2: pc.weight = 2
Secondary Site:
node3: pc.weight = 1
node4: pc.weight = 1
这种模式下,一些节点位于主站点,而其它节点位于从站点。如果从站点关闭或站点之间的网络连接丢失,则主站点上的节点仍然是主组件。此外,node1或node2崩溃不会让其它剩余节点成为非主组件。