在MGR的代码的四层架构中,Xcom层实现了一个Paxos的协议。这个实现被称为Leaderless(或者是Multiple Leaders)的Paxos。在这个协议中不需要选举Leader,每个加入集群的节点都是Leader,都可以接受客户端的数据广播请求,主动发起数据的广播过程(Pepare->Accept->Learn)。XCOM的这个设计是MGR能够实现多写的基础。


XCOM(Paxos)在MGR中的作用
在《由浅入深理解Paxos协议(1)》中,我说过Paxos承担的就是一个原子广播器的角色,用来对数据做全局排序。MGR中Xcom就是用来对用户发起的写事务做全局排序的。

MySQL Group Replication的Paxos实现_java

经过排序后,所有的事务在所有的节点上按同样的顺序执行。这样所有的节点上的数据相同。当然实际实现时为了提高效率,MGR采用的基于Primary Key的冲突检测机制和并发策略,更新的数据没有冲突的事务可以并发的执行。



Leaderless Paxos的原理Xcom是基于《Mencius: Building Efficient Replicated State Machines for WANs》这个论文实现的,有兴趣的可以读一下。
Paxos Group
首先,Mencius中每个节点都会主动和其他节点建立连接,组成一个Paxos组,在这个组中主动发起连接的节点永远是Leader(Proposer).  其他的节点只有Acceptor和Leaner的角色。所以一个三个节点的集群中,实际上有3个Paxos组。每个节点分别领导一个Paxos组。当一个节点收到客户端的数据排序请求时,就使用自己领导的Paxos组执行一个Paxos过程。三个Paxos组之间互不干涉。


全局排序组内的数据顺序按照数据请求的先后顺序就可以了,那么组间的数据是如何排序呢。Mencius中组间的数据顺序采用了一个预先确定的顺序。

全局的数据顺序由两个参数决定:

  • 组内顺序号

  • 组号(即Leader节点在集群中的序号)

所以组间的排序是事先确定的。<1,1>在<1,2>前,<1,2>在<1,3>,<1,3>在<2,1>前...。


Xcom必须要按照顺序将的完成paxos过程的数据发送给应用。由于顺序是事先确定的,即顺序靠后的数据先完成paxos过程,也不能提前发送给应用。比如<1,3>可能比<1,2>先完成paxos过程,但是必须要等到<1,2>完成,并发送给应用后,才能将<1,3>的数据发送给应用。


Noop

那如果其中的一个节点很久没有数据,岂不是会导致后面其他Paxos组已经完成paxos过程的数据也一直没有办法发送给应用吗?是的,为了避免这个问题Mencius中加入了Noop操作。当一个节点发现比自己的顺序号靠后的数据已经完成了Paxos过程时(并且自己的这个位置上没有paxos操作),就会广播一个Noop,告诉其他节点自己的这个顺序号可以跳过。

MySQL Group Replication的Paxos实现_java_02

注意: Noop操作是直接通过一个消息发送出去的,不需要Paxos的过程。因为除了Leader其他节点不能在这个Paxos组内发送任何数据。


故障处理

一个节点没有数据时可以用Noop跳过自己的顺序位。那如果这个节点故障了怎么办呢?Paxos本身是允许多个节点同时发起proposal的. 所以当Leader故障时,其他节点可以发起一个noop proposal,执行一个完整的paxos过程来跳过Leader的顺序位。

MySQL Group Replication的Paxos实现_java_03

故障检测或长或短总是需要时间的,所以尽管Mencius用的是Paxos的多数派协议,但是可以看到,任何一个节点的抖动、故障都会影响到集群的可用性。这一点在实现时可以进行优化,但无法彻底避免。


XCOM的实现

xcom采用了co-routine的方式来实现。co-routine就是在用户态用一个线程模拟多线程的工作方式,主要用来减少内核态context switch带来的开销。xcom的实现中用的task就相当于一个线程的主函数。

Xcom中定义了如下的一些Tasks:

tcp_server

是socket监听线程,负责接收TCP连接请求。

tcp_reaper_server

负责清理长时间不使用的TCP连接。


local_server

接收和处理应用端发送过来的消息。如果是paxos请求则放入proposal 队列。

每一个客户端连接都有一个独立的local_server task.


proposer_task

从proposal队列取出一批proposal,开启一个2阶段(仅包括accept和learn)或者完整的paxos过程。为了提高效率,将一批propoal用一个paxos过程完成。


proposer_task可以有多个,并发的执行多个paxos过程。默认情况下,xcom有10个proposers。


acceptor_leaner_task

处理对端xcom节点(proposer)发送过来的paxos消息(prepare, accept, learn等消息),并发送Reply消息给proposer. 每一个从其他Xcom发起的连接都有一个单独的acceptor_leaner_task。


reply_handler_task

处理远端acceptor/leaner返回的消息(ack_prepare, ack_accept),继续paxos的过程。为每一个远端节点建一个单独的reply_handler_task


local_sender_task

本质上是一个本地的acceptor_leaner_task+reply_handler_task。Leader本身也是一个acceptor leaner. sender_task用于处理leader发给自己的acceptor/leaner的消息,以及acceptor/learner回复的消息。sender_task避免了使用socket通讯,可以提升效率。


sender_task

发送给其他节点的paxos消息会放入outgoing队列。sender_task负责将outgoing队列中的消息通过tcp连接发送出去。Leader到每一个节点的连接都有一个单独的sender_task和outgoing队列。



sweeper_task

Leader上用来发送noop(skip_op)消息,去跳过某个位置。


executor_task

按顺序将完成paxos过程的数据发送给应用。

当某个位置所属的节点可能发生故障时,executor_task就会在该位置上执行一个noop paxos过程,来跳过该位置。节点是否发生故障通过may_be_dead()来判断。判决依据是该节点4s内没有任何消息。也就是说一但某个节点故障下线了,会导致整个集群至少4秒不可用。为了避免多个节点同时执行noop paxos过程,规定只有当前paxos组内第一个alive的节点执行。


另外noop必须要执行一个完整的paxos过程,而故障节点的每个位置都要执行一个完整的paxos过程,效率很低。为了避免这个问题,MGR会自动的将故障的节点从集群中移除出去,详见《MySQL Group Replication的错误响应机制


alive_task

当没有数据流量时,发送alive消息给其他节点。alive_task做两件事情:

  • 在节点空闲了0.5秒后,alive_task()会发送i_am_alive_op消息给其他节点。空闲是指0.5秒内没有发送任何消息出去。

  • 当某个节点4秒内没有任何通信时(代码中称作may_be_dead),发送are_you_alive_op消息给这个节点,去探查该节点是否还活着。


detector_task

每1秒做一轮检查。检查节点是否还活着。判断的标准是:


detected_time > task_now() - 5.0

即5秒内没有和这个节点通信。如果此节点的detected time在5秒之内,则为活着,否则认为该节点可能已经死去。


当detector_task()发现任何节点的状态发生了变化:

  • 从活着变成可能死了‘

  • 从可能死了变成活着

都会通过local view回调函数xcom_receive_local_view来处理状态变化。状态的变化由GCS模块处理。


数据在Leader和远端节点间的处理过程


MySQL Group Replication的Paxos实现_java_04



数据在Leader和本地Acceptor/Learner之间的处理过程


MySQL Group Replication的Paxos实现_java_05