算法高级(5)-分布式系统选举算法及脑裂

一、选举算法定义

分布式中有这么一个疑难问题,客户端向一个分布式集群的服务端发出一系列更新数据的消息,由于分布式集群中的各个服务端节点是互为同步数据的,所以运行完客户端这系列消息指令后各服务端节点的数据应该是一致的,但由于网络或其他原因,各个服务端节点接收到消息的序列可能不一致,最后导致各节点的数据不一致。要确保数据一致,需要选举算法的支撑,这就引申出了今天我们要讨论的题目,关于选举算法的原理解释及实现,选举包括对机器的选举,也包括对消息的选举。

在分布式系统中,通常称主节点为Master(主人),其他从节点为Slave(奴隶),因为涉及到种族歧视,目前很多程序已经改为了Leader(首领)和Follower(追随者)。

一句话总结:选举算法是为了解决集群中谁说了算这个问题的。

二、选举算法分类

很多分布式算法需要有一个进程作为协作者。下面介绍一些常用的选举算法。

欺负算法

当任何一个进程发现协作者不再响应请求时,它就发起一次选举。算法实现如下:

  1. P向所有编号比它大的进程发送一个ELECTION消息
  2. 如果无人响应,P获胜并成为协作者
  3. 如果有编号比它大的进程响应,则有响应者接管选举工作。P的工作完成。
  4. 将选举获胜的消息发送给所有进程,通知这些进程自己是新的协作者。
  5. 当一个以前崩溃了的进程现在恢复过来时,它将主持一次选举。如果该进程恰好是当前正在运行的进程中进程号最大的进程,它将赢得此次选举,接管协作者的工作。这样,最大的进程总是取胜,故称为“欺负算法”。

环算法

  1. 当任何一个进程注意到协作者不工作时,它就构造一个带有它自己的进程号的ELECTION消息,并将该消息发送给它的后继者。
  2. 如果后继者崩溃了,发送者沿着此环跳过它的后继者发送给下一个进程,或者再下一个,直到找到一个正在运行的进程。
  3. 在每一步中,发送者都将自己的进程号加到该消息列表中,以使自己成为协作者的候选人之一。
  4. 最终,消息返回到发起此次选举的进程。当发起者进程接收到一个包含它自己进程号的消息时,它就识别出这个事件。
  5. 此时,消息类型变成COORDINATIOR消息,并再一次绕环运行,向所有进程通知谁是协作者(成员列表中进程号最大的那个)以及新环中的成员都有谁。
  6. 这个消息在循环一周后被删除,随后每个进程都恢复原来的工作。
  7. 如果有多个ELECTION消息,那么循环多周后被删除。

下面具体的算法其实都是基于上面两种原理来实现的。

1.最简单的选举算法

如果你需要开发一个分布式集群系统,一般来说你都需要去实现一个选举算法,来选举出Master节点。为了解决Master节点的单点问题,一般我们也会选举出一个Master-HA节点(高可用)。

如果不采用后文的算法,我们也可以实现一个简单的选举策略。

这类型简单的选举算法可以依赖很多计算机硬件因素作为选举因子,比如IP地址、CPU核数、内存大小、自定义序列号等等,比如采用自定义序列号,我们假设每台服务器利用组播方式获取局域网内所有集群分析相关的服务器的自定义序列号,以自定义序列号作为优先级,如果接收到的自定义序列号比本地自定义序列号大,则退出竞争,最终选择一台自定义序列号最大的服务器作为Leader服务器,其他服务器则作为普通服务器。这种简单的选举算法没有考虑到选举过程中的异常情况,选举产生后不会再对选举结果有异议,这样可能会出现序列号较小的机器被选定为Master节点(有机器临时脱离集群),实现伪代码如清单1所示。

state:=candidate; 
send(my_id):receive(nid);
while nid!=my_id
do if nid>my_id
then state:=no_leader;
send(nid):receive(nid);
od; 
if state=candidate then state:=leader;

2.拜占庭将军问题

拜占庭帝国国土辽阔,为了防御目的,每支军队都分隔很远,将军之间只能依靠信差传信。在战争的时候,拜占庭军队内所有司令和将军必需达成一致的共识,决定是否有赢的机会才去攻打敌人的阵营。但是,在军队内有可能存有叛徒和敌军的间谍,左右将军们的决定又扰乱整体军队的秩序。因此表决的结果并不一定能代表大多数人的意见。这时候,在已知有成员谋反的情况下,其余忠诚的将军在不受叛徒的影响下如何达成一致的协议,拜占庭问题就此形成。

拜占庭将军问题实则是一个协议问题。一个可靠的分布式系统必须容忍一个或多个部分的失效,失效的部分可能会送出相互矛盾的信息给系统的其他部分。纽约的一家银行可以在东京、巴黎、苏黎世设置异地备份,当某些点受到攻击甚至破坏以后,可以保证账目仍然不错,得以复原和恢复。从技术的角度讲,这是一个很困难的问题,因为被攻击的系统不但可能不作为,而且可能进行破坏。对于这类故障的问题被抽象地表达为拜占庭将军问题。

解决拜占庭将军问题的算法必须保证

  • A.所有忠诚的将军必须基于相同的行动计划做出决策;
  • B.少数叛徒不能使忠诚的将军做出错误的计划。

拜占庭问题的解决可能性

(1)叛徒数大于或等于1/3,拜占庭问题不可解

  • 如果有三位将军,一人是叛徒。当司令发进攻命令时,将军3可能告诉将军2,他收到的是“撤退”的命令。这时将军2收到一个“进攻”的命令,一个“撤退”的命令,而无所适从。
  • 如果司令是叛徒,他告诉将军2“进攻”,将军3“撤退”。当将军3告诉将军2,他收到“撤退”命令时,将军2由于收到了司令“进攻”的命令,而无法与将军3保持一致。
  • 正由于上述原因,在三模冗余系统中,如果允许一机有拜占庭故障,即叛徒数等于1/3,因而,拜占庭问题不可解。也就是说,三模冗余对付不了拜占庭故障。三模冗余只能容故障-冻结(fail-frost)那类的故障。就是说元件故障后,它就冻结在某一个状态不动了。对付这类故障,用三模冗余比较有效。

(2)用口头信息,如果叛徒数少于1/3,拜占庭问题可解

  • 这里是在四模冗余基础上解决。在四模中有一个叛徒,叛徒数是少于1/3的。
  • 拜占庭问题可解是指所有忠诚的将军遵循同一命令。若司令是忠诚的,则所有忠诚将军遵循其命令。我们可以给出一个多项式复杂性的算法来解这一问题。算法的中心思想很简单,就是司令把命令发给每一位将军,各将军又将收到的司令的命令转告给其他将军,递归下去,最后用多数表决。例如,司令送一个命令v给所有将军。若将军3是叛徒,当他转告给将军2时命令可能变成x。但将军2收到{v, v, x},多数表决以后仍为v,忠诚的将军可达成一致。如果司令是叛徒,他发给将军们的命令可能互不相同,为x, y, z。当副官们互相转告司令发来的信息时,他们会发现,他们收到的都是{x,y,z},因而也取得了一致。

(3)用书写信息,如果至少有2/3的将军是忠诚的,拜占庭问题可解

  • 所谓书写信息,是指带签名的信息,即可认证的信息。它是在口头信息的基础上,增加两个条件:
    • ①忠诚司令的签名不能伪造,内容修改可被检测。
    • ②任何人都可以识别司令的签名,叛徒可以伪造叛徒司令的签名。
  • 一种已经给出的算法是接收者收到信息后,签上自己的名字后再发给别人。由于书写信息的保密性,可以证明,用书写信息,如果至少有2/3的将军是忠诚的,拜占庭问题可解。

例如,如果司令是叛徒,他发送“进攻”命令给将军1,并带有他的签名0,发送“撤退”命令给将军2,也带签名0。将军们转送时也带了签名。于是将军1收到{“进攻”:0,“撤退”:0,2},说明司令发给自己的命令是“进攻”,而发给将军2的命令是“撤退”,司令对我们发出了不同的命令。对将军2同解。

3.Paxos算法

算法起源

  • Paxos算法是LesileLamport于1990年提出的一种基于消息传递且具有高度容错特性的一致性算法,是目前公认的解决分布式一致性问题最有效的算法之一。
  • 在常见的分布式系统中,总会发生诸如机器宕机或网络异常等情况。Paxos算法需要解决的问题就是如何在一个可能发生上述异常的分布式系统中,快速且正确地在集群内部对某个数据的值达成一致,并且保证不论发生以上任何异常,都不会破坏整个系统的一致性。
  • 为了更加清晰概念,当client1、client2、client3分别发出消息指令A、B、C时,Server1~4由于网络问题,接收到的消息序列就可能各不相同,这样就可能由于消息序列的不同导致Server1~4上的数据不一致。对于这么一个问题,在分布式环境中很难通过像单机里处理同步问题那么简单,而Paxos算法就是一种处理类似于以上数据不一致问题的方案。
  • Paxos算法是要在一堆消息中通过选举,使得消息的接收者或者执行者能达成一致,按照一致的消息顺序来执行。其实,以最简单的想法来看,为了达到所有人执行相同序列的指令,完全可以通过串行来做,比如在分布式环境前加上一个FIFO队列来接收所有指令,然后所有服务节点按照队列里的顺序来执行。这个方法当然可以解决一致性问题,但它不符合分布式特性,如果这个队列出现异常这么办?而Paxos的高明之处就在于允许各个client互不影响地向服务端发指令,大伙按照选举的方式达成一致,这种方式具有分布式特性,容错性更好。
  • Paxos规定了四种角色(Proposer,Acceptor,Learner,以及Client)和两个阶段(Promise和Accept)。

实现原理

Paxos算法的主要交互过程在Proposer和Acceptor之间。Proposer与Acceptor之间的交互主要有4类消息通信。这4类消息对应于paxos算法的两个阶段4个过程:

阶段1:

  • a) proposer向网络内超过半数的acceptor发送prepare消息;
  • b) acceptor正常情况下回复promise消息。

阶段2:

  • a) 在有足够多acceptor回复promise消息时,proposer发送accept消息;
  • b) 正常情况下acceptor回复accepted消息。

Paxos算法的最大优点在于它的限制比较少,它允许各个角色在各个阶段的失败和重复执行,这也是分布式环境下常有的事情,只要大伙按照规矩办事即可,算法的本身保障了在错误发生时仍然得到一致的结果。

4.Raft分布式选举算法

参考我的另外一篇博文算法高级(6)-Raft算法

三、Zookeeper的ZAB协议

1.基本概念

ZooKeeper并没有完全采用Paxos算法,而是使用了一种称为ZooKeeper Atomic Broadcast(ZAB,ZooKeeper原子消息广播协议)的协议作为其数据一致性的核心算法。

ZAB协议是为分布式协调服务ZooKeeper专门设计的一种支持崩溃恢复的原子广播协议。ZAB协议最初并没有要求其具有很好的扩展性,最初只是为雅虎公司内部那些高吞吐量、低延迟、健壮、简单的分布式系统场景设计的。在ZooKeeper的官方文档中也指出,ZAB协议并不像Paxos算法那样,是一种通用的分布式一致性算法,它是一种特别为ZooKeeper设计的崩溃可恢复的原子消息广播算法。

ZooKeeper使用一个单一的主进程来接收并处理客户端的所有事务请求,并采用ZAB的原子广播协议,将服务器数据的状态变更以事务Proposal的形式广播到所有的副本进程上去。ZAB协议的这个主备模型架构保证了同一时刻集群中只能够有一个主进程来广播服务器的状态变更,因此能够很好地处理客户端大量的并发请求。另一方面,考虑到在分布式环境中,顺序执行的一些状态变更其前后会存在一定的依赖关系,有些状态变更必须依赖于比它早生成的那些状态变更,例如变更C需要依赖变更A和变更B。这样的依赖关系也对ZAB协议提出了一个要求,即ZAB协议需要保证如果一个状态变更已经被处理了,那么所有其依赖的状态变更都应该已经被提前处理掉了。最后,考虑到主进程在任何时候都有可能出现奔溃退出或重启现象,因此,ZAB协议还需要做到在当前主进程出现上述异常情况的时候,依旧能够工作。

下面这段日志所示是ZooKeeper集群启动时选举过程所打印的日志,从里面可以看出初始阶段是LOOKING状态,该节点在极短时间内就被选举为Leader节点。

zookeeper.out:
2016-06-14 16:28:57,336 [myid:3] - INFO [main:QuorumPeerMain@127] - Starting quorum peer
2016-06-14 16:28:57,592 [myid:3] - INFO [QuorumPeer[myid=3]/0:0:0:0:0:0:0:0:2181:QuorumPeer@774] - LOOKING
2016-06-14 16:28:57,593 [myid:3] - INFO [QuorumPeer[myid=3]/0:0:0:0:0:0:0:0:2181:FastLeaderElection@818] - New election. My id =  3, proposed zxid=0xc00000002
2016-06-14 16:28:57,599 [myid:3] - INFO [WorkerSender[myid=3]:QuorumPeer$QuorumServer@149] - Resolved hostname: 10.17.138.225 to address: /10.17.138.225
2016-06-14 16:28:57,599 [myid:3] - INFO [WorkerReceiver[myid=3]:FastLeaderElection@600] - Notification: 1 (message format version), 3 (n.leader), 0xc00000002 (n.zxid)
, 0x1 (n.round), LOOKING (n.state), 3 (n.sid), 0xc (n.peerEpoch) LOOKING (my state)
2016-06-14 16:28:57,602 [myid:3] - INFO [WorkerReceiver[myid=3]:FastLeaderElection@600] - Notification: 1 (message format version), 1 (n.leader), 0xc00000002 (n.zxid)
, 0x1 (n.round), LOOKING (n.state), 1 (n.sid), 0xc (n.peerEpoch) LOOKING (my state)
2016-06-14 16:28:57,605 [myid:3] - INFO [WorkerReceiver[myid=3]:FastLeaderElection@600] - Notification: 1 (message format version), 3 (n.leader), 0xc00000002 (n.zxid)
, 0x1 (n.round), LOOKING (n.state), 1 (n.sid), 0xc (n.peerEpoch) LOOKING (my state)
2016-06-14 16:28:57,806 [myid:3] - INFO [QuorumPeer[myid=3]/0:0:0:0:0:0:0:0:2181:QuorumPeer@856] - LEADING
2016-06-14 16:28:57,808 [myid:3] - INFO [QuorumPeer[myid=3]/0:0:0:0:0:0:0:0:2181:Leader@59] - TCP NoDelay set to: true

2.ZAB协议实现原理

ZAB协议的核心是定义了对于那些会改变ZooKeeper服务器数据状态的事务请求的处理方式,即所有事务请求必须由一个全局唯一的服务器来协调处理,这样的服务器被称为Leader服务器,而余下的服务器则称为Follower服务器,ZooKeeper后来又引入了Observer服务器,主要是为了解决集群过大时众多Follower服务器的投票耗时时间较长问题,这里不做过多讨论。Leader服务器负责将一个客户端事务请求转换成一个事务Proposal(提议),并将该Proposal分发给集群中所有的Follower服务器。之后Leader服务器需要等待所有Follower服务器的反馈信息,一旦超过半数的Follower服务器进行了正确的反馈后,那么Leader就会再次向所有的Follower服务器分发Commit消息,要求其将前一个Proposal进行提交。

3.支持模式

ZAB协议包括两种基本的模式,分别是崩溃恢复和消息广播。

当整个服务框架在启动的过程中,或是当Leader服务器出现网络中断、崩溃退出与重启等异同步之后,ZAB协议就会退出恢复模式。其中,所谓的状态同步是指数据同步,用来保证集群中存在过半的恶机器能够和Leader服务器的数据状态保持一致。通常情况下,ZAB协议会进入恢复模式并选举产生新的Leader服务器。当选举产生了新的Leader服务器,同时集群中已经有过半的机器与该Leader服务器完成了状态。在上文所示选举的基础上,我们把Leader节点的进程手动关闭(kill -9 pid),随即进入崩溃恢复模式,重新选举Leader的过程日志输出如下所示。

2016-06-14 17:33:27,723 [myid:2] - WARN  [RecvWorker:3:QuorumCnxManager$RecvWorker@810] - Connection broken for id 3, my id = 2, error =
java.io.EOFException 
atjava.io.DataInputStream.readInt(DataInputStream.java:392) 
at org.apache.zookeeper.server.quorum.QuorumCnxManager$RecvWorker.run(QuorumCnxManager.java:795) 
2016-06-14 17:33:27,723 [myid:2] - WARN  [RecvWorker:3:QuorumCnxManager$RecvWorker@810] - Connection broken for id 3, my id = 2, error = 
java.io.EOFException 
atjava.io.DataInputStream.readInt(DataInputStream.java:392) 
at org.apache.zookeeper.server.quorum.QuorumCnxManager$RecvWorker.run(QuorumCnxManager.java:795) 
2016-06-14 17:33:27,728 [myid:2] - INFO  [QuorumPeer[myid=2]/0:0:0:0:0:0:0:0:2181:Follower@166] - shutdown called 
java.lang.Exception: shutdown Follower 
at org.apache.zookeeper.server.quorum.Follower.shutdown(Follower.java:166) 
at org.apache.zookeeper.server.quorum.QuorumPeer.run(QuorumPeer.java:850) 
2016-06-14 17:33:27,728 [myid:2] - WARN  [RecvWorker:3:QuorumCnxManager$RecvWorker@813] - Interrupting SendWorker 
2016-06-14 17:33:27,729 [myid:2] - INFO  [QuorumPeer[myid=2]/0:0:0:0:0:0:0:0:2181:FollowerZooKeeperServer@140] - Shutting down
2016-06-14 17:33:27,730 [myid:2] - INFO  [QuorumPeer[myid=2]/0:0:0:0:0:0:0:0:2181:ZooKeeperServer@467] - shutting down 
2016-06-14 17:33:27,730 [myid:2] - WARN  [SendWorker:3:QuorumCnxManager$SendWorker@727] - Interrupted while waiting for message on queue
java.lang.InterruptedException
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.reportInterruptAfterWait(AbstractQueuedSynchronizer.java:2017)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2095)
at java.util.concurrent.ArrayBlockingQueue.poll(ArrayBlockingQueue.java:389)
at org.apache.zookeeper.server.quorum.QuorumCnxManager.pollSendQueue(QuorumCnxManager.java:879)
at org.apache.zookeeper.server.quorum.QuorumCnxManager.access$500(QuorumCnxManager.java:65)
at org.apache.zookeeper.server.quorum.QuorumCnxManager$SendWorker.run(QuorumCnxManager.java:715)
2016-06-14 17:33:27,730 [myid:2] - INFO  [QuorumPeer[myid=2]/0:0:0:0:0:0:0:0:2181:FollowerRequestProcessor@107] - Shutting down
2016-06-14 17:33:27,731 [myid:2] - WARN  [SendWorker:3:QuorumCnxManager$SendWorker@736] - Send worker leaving thread
2016-06-14 17:33:27,732 [myid:2] - INFO  [FollowerRequestProcessor:2:FollowerRequestProcessor@97] - FollowerRequestProcessor exited loop!
2016-06-14 17:33:27,732 [myid:2] - INFO  [QuorumPeer[myid=2]/0:0:0:0:0:0:0:0:2181:CommitProcessor@184] - Shutting down
2016-06-14 17:33:27,733 [myid:2] - INFO  [QuorumPeer[myid=2]/0:0:0:0:0:0:0:0:2181:FinalRequestProcessor@417] - shutdown of request processor complete 
2016-06-14 17:33:27,733 [myid:2] - INFO  [CommitProcessor:2:CommitProcessor@153] - CommitProcessor exited loop! 
2016-06-14 17:33:27,733 [myid:2] - INFO  [QuorumPeer[myid=2]/0:0:0:0:0:0:0:0:2181:SyncRequestProcessor@209] - Shutting down 
2016-06-14 17:33:27,734 [myid:2] - INFO  [SyncThread:2:SyncRequestProcessor@187] - SyncRequestProcessor exited!
2016-06-14 17:33:27,734 [myid:2] - INFO  [QuorumPeer[myid=2]/0:0:0:0:0:0:0:0:2181:QuorumPeer@774] - LOOKING 
2016-06-14 17:33:27,739 [myid:2] - INFO  [QuorumPeer[myid=2]/0:0:0:0:0:0:0:0:2181:FileSnap@83] - Reading snapshot /home/hemeng/zookeeper-3.4.7/data/zookeepe 
r/version-2/snapshot.c00000002[QuorumPeer[myid=2]/0:0:0:0:0:0:0:0:2181:QuorumPeer@856] – LEADING 
2016-06-14 17:33:27,957 [myid:2] - INFO  [QuorumPeer[myid=2]/0:0:0:0:0:0:0:0:2181:Leader@361] - LEADING - LEADER ELECTION TOOK - 222

当集群中已经有过半的Follower服务器完成了和Leader服务器的状态同步,那么整个服务框架就可以进入消息广播模式了。当一台同样遵守ZAB协议的服务器启动后加入到集群中时,如果此时集群中已经存在一个Leader服务器在负责进行消息广播,那么新加入的服务器就会自觉地进入数据恢复模式:找到Leader所在的服务器,并与其进行数据同步,然后一起参与到消息广播流程中去。ZooKeeper设计成只允许唯一的一个Leader服务器来进行事务请求的处理。Leader服务器在接收到客户端的事务请求后,会生成对应的事务提案并发起一轮广播协议;而如果集群中的其他机器接收到客户端的事务请求,那么这些非Leader服务器会首先将这个事务请求转发给Leader服务器。

4.选举三阶段

整个ZAB协议主要包括消息广播和崩溃恢复这两个过程,进一步可以细分为三个阶段,分别是发现、同步和广播阶段。组成ZAB协议的每一个分布式进程,会循环地执行这三个阶段,我们将这样一个循环称为一个主进程周期。

  • 发现:要求zookeeper集群必须选举出一个 Leader 进程,同时 Leader 会维护一个 Follower 可用客户端列表。将来客户端可以和这些 Follower节点进行通信。

  • 同步:Leader 要负责将本身的数据与 Follower 完成同步,做到多副本存储。这样也是提现了CAP中的高可用和分区容错。Follower将队列中未处理完的请求消费完成后,写入本地事务日志中。

  • 广播:Leader 可以接受客户端新的事务Proposal请求,将新的Proposal请求广播给所有的 Follower。

5. 服务器启动时期的Leader选举核心算法

若进行Leader选举,则至少需要两台机器,这里选取3台机器组成的服务器集群为例。在集群初始化阶段,当有一台服务器Server1启动时,其单独无法进行和完成Leader选举,当第二台服务器Server2启动时,此时两台机器可以相互通信,每台机器都试图找到Leader,于是进入Leader选举过程。选举过程如下

(1) 每个Server发出一个投票。由于是初始情况,Server1和Server2都会将自己作为Leader服务器来进行投票,每次投票会包含所推举的服务器的myid和ZXID,使用(myid, ZXID)来表示,此时Server1的投票为(1, 0),Server2的投票为(2, 0),然后各自将这个投票发给集群中其他机器。

(2) 接受来自各个服务器的投票。集群的每个服务器收到投票后,首先判断该投票的有效性,如检查是否是本轮投票、是否来自LOOKING状态的服务器。

(3) 处理投票。针对每一个投票,服务器都需要将别人的投票和自己的投票进行PK,PK规则如下

  • 优先检查ZXID。ZXID比较大的服务器优先作为Leader。
  • 如果ZXID相同,那么就比较myid。myid较大的服务器作为Leader服务器。

对于Server1而言,它的投票是(1, 0),接收Server2的投票为(2, 0),首先会比较两者的ZXID,均为0,再比较myid,此时Server2的myid最大,于是更新自己的投票为(2, 0),然后重新投票,对于Server2而言,其无须更新自己的投票,只是再次向集群中所有机器发出上一次投票信息即可。

(4) 统计投票。每次投票后,服务器都会统计投票信息,判断是否已经有过半机器接受到相同的投票信息,对于Server1、Server2而言,都统计出集群中已经有两台机器接受了(2, 0)的投票信息,此时便认为已经选出了Leader。

(5) 改变服务器状态。一旦确定了Leader,每个服务器就会更新自己的状态,如果是Follower,那么就变更为FOLLOWING,如果是Leader,就变更为LEADING。

6. 服务器运行时期的Leader选举核心算法

在Zookeeper运行期间,Leader与非Leader服务器各司其职,即便当有非Leader服务器宕机或新加入,此时也不会影响Leader,但是一旦Leader服务器挂了,那么整个集群将暂停对外服务,进入新一轮Leader选举,其过程和启动时期的Leader选举过程基本一致。假设正在运行的有Server1、Server2、Server3三台服务器,当前Leader是Server2,若某一时刻Leader挂了,此时便开始Leader选举。选举过程如下:

(1) 变更状态。Leader挂后,余下的非Observer服务器都会讲自己的服务器状态变更为LOOKING,然后开始进入Leader选举过程。

(2) 每个Server会发出一个投票。在运行期间,每个服务器上的ZXID可能不同,此时假定Server1的ZXID为123,Server3的ZXID为122;在第一轮投票中,Server1和Server3都会投自己,产生投票(1, 123),(3, 122),然后各自将投票发送给集群中所有机器。

(3) 接收来自各个服务器的投票。与启动时过程相同。

(4) 处理投票。与启动时过程相同,此时,Server1将会成为Leader。

(5) 统计投票。与启动时过程相同。

(6) 改变服务器的状态。与启动时过程相同。

7.ZAB与Paxos的区别

ZAB协议并不是Paxos算法的一个典型实现,在讲解ZAB和Paxos之间的区别之间,我们首先来看下两者的联系。

两者都存在一个类似于Leader进程的角色,由其负责协调多个Follower进程运行。
Leader进程都会等待超过半数的Follower做出正确的反馈后,才会将一个提案进行提交。
在ZAB协议中,每个Proposal中都包含了一个epoch值,用来代表当前的Leader周期,在Paxos算法中,同样存在这样的一个标识,只是名字变成了Ballot。
在Paxos算法中,一个新选举产生的主进程会进行两个阶段的工作。第一阶段被称为读阶段,在这个阶段中,这个新的主进程会通过和所有其他进程进行通信的方式来收集上一个主进程提出的提案,并将它们提交。第二阶段被称为写阶段,在这个阶段,当前主进程开始提出它自己的提案。在Paxos算法设计的基础上,ZAB协议额外添加了一个同步阶段。在同步阶断之前,ZAB协议也存在一个和Paxos算法中的读阶段非常类似的过程,称为发现阶段。在同步阶段中,新的Leader会确保存在过半的Follower已经提交了之前Leader周期中的所有事务Proposal。这一同步阶段的引入,能够有效地保证Leader在新的周期中提出事务Proposal之前,所有的进程都已经完成了对之前所有事务Proposal的提交。一旦完成同步阶段后,那么ZAB就会执行和Paxos算法类似的写阶段。

总的来讲,Paxos算法和ZAB协议的本质区别在于,两者的设计目标不一样。ZAB协议主要用于构建一个高可用的分布式数据主备系统,例如ZooKeeper,而Paxos算法则是用于构建一个分布式的一致性状态机系统。

四、Zookeeper的Quorum机制-谈谈怎样解决脑裂(split-brain)

Split-Brain问题说的是1个集群如果发生了网络故障,很可能出现1个集群分成了两部分,而这两个部分都不知道对方是否存活,不知道到底是网络问题还是直接机器down了,所以这两部分都要选举1个Leader,而一旦两部分都选出了Leader, 并且网络又恢复了,那么就会出现两个Brain的情况,整个集群的行为不一致了。

在使用zookeeper的过程中,我们经常会看到这样一些说法:

  • zookeeper cluster的节点数目必须是奇数。
  • zookeeper 集群中必须超过半数节点(Majority)可用,整个集群才能对外可用。

所谓整个集群是否可用,隐含的一个意思就是整个集群还能够选举出一个”Leader”。ZooKeeper默认设置的是采用Majority Qunroms的方式来支持Leader选举。以这种方式来防止Split-Brain问题出现,即只有集群中超过半数节点投票才能选举出Leader。这样的方式可以确保leader的唯一性,要么选出唯一的一个leader,要么选举失败。

在ZooKeeper中Quorums有2个作用:

  • 集群中最少的节点数用来选举Leader保证集群可用
  • 通知客户端数据已经安全保存前集群中最少数量的节点数已经保存了该数据。一旦这些节点保存了该数据,客户端将被通知已经安全保存了,可以继续其他任务。而集群中剩余的节点将会最终也保存了该数据

理解了Quorums就不难理解为什么集群中的节点数一般配置为奇数。节点数配置成奇数的集群的容忍度更高。

举例如下:

  • 比如3个节点的集群,Quorums = 2, 也就是说集群可以容忍1个节点失效,这时候还能选举出1个lead,集群还可用;
  • 比如4个节点的集群,它的Quorums = 3,Quorums要超过3,相当于集群的容忍度还是1,如果2个节点失效,那么整个集群还是无效的;

所以4个节点的集群的容忍度 = 3个节点的集群的容忍度,但是4个节点的集群多了1个节点,相当于浪费了资源。

更极端的例子是100个节点的集群,如果网络问题导致分为两个部分,50个节点和50个节点,这样整个集群还是不可用的,因为按照Quorums的方式必须51个节点才能保证选出1个Leader。这时候可以采用Weight加权的方式,有些节点的权值高,有些节点的权值低,最后计算权值,只要权值过半,也能选出1个Leader。

五、总结

  1. 通常在分布式系统中,构成一个集群的每一台机器都有自己的角色,最典型的集群模式就是Master/Slave模式(主备模式)。在这种模式中,我们把能够处理所有写操作的机器称为Master机器,把所有通过异步复制方式获取最新数据,并提供读服务的机器称为Slave机器。在Paxos算法内部,引入了Proposer、Acceptor和Learner三种角色。而在ZooKeeper中,这些概念也做了改变,它没有沿用传统的Master/Slave概念,而是引入了Leader、Follower和Observer三种角色。本文通过对Paxos和ZooKeeper ZAB协议进行讲解,让读者有一些基本的分布式选举算法方面的认识。
  2. 引入半数选举机制,必须获得半数以上票数选举生效,可以有效避免这种情况下的脑裂问题。

我的微信公众号:架构真经(id:gentoo666),分享Java干货,高并发编程,热门技术教程,微服务及分布式技术,架构设计,区块链技术,人工智能,大数据,Java面试题,以及前沿热门资讯等。每日更新哦!

参考资料:

  1. https://www.jianshu.com/p/f0c717501cee
  2. https://www.jianshu.com/p/c2ced54736aa
  3. https://blog.csdn.net/xiaqunfeng123/article/details/51712983
  4. https://blog.csdn.net/michaelzhou224/article/details/52304777
  5. https://www.jianshu.com/p/2bceacd60b8a
  6. https://blog.csdn.net/varyall/article/details/80151205
  7. https://blog.csdn.net/ai_xiangjuan/article/details/78156505

你可能感兴趣的:(算法高级)