分布式中有这么一个疑难问题,客户端向一个分布式集群的服务端发出一系列更新数据的消息,由于分布式集群中的各个服务端节点是互为同步数据的,所以运行完客户端这系列消息指令后各服务端节点的数据应该是一致的,但由于网络或其他原因,各个服务端节点接收到消息的序列可能不一致,最后导致各节点的数据不一致。要确保数据一致,需要选举算法的支撑,这就引申出了今天我们要讨论的题目,关于选举算法的原理解释及实现,选举包括对机器的选举,也包括对消息的选举。
在分布式系统中,通常称主节点为Master(主人),其他从节点为Slave(奴隶),因为涉及到种族歧视,目前很多程序已经改为了Leader(首领)和Follower(追随者)。
一句话总结:选举算法是为了解决集群中谁说了算这个问题的。
很多分布式算法需要有一个进程作为协作者。下面介绍一些常用的选举算法。
当任何一个进程发现协作者不再响应请求时,它就发起一次选举。算法实现如下:
下面具体的算法其实都是基于上面两种原理来实现的。
如果你需要开发一个分布式集群系统,一般来说你都需要去实现一个选举算法,来选举出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;
拜占庭帝国国土辽阔,为了防御目的,每支军队都分隔很远,将军之间只能依靠信差传信。在战争的时候,拜占庭军队内所有司令和将军必需达成一致的共识,决定是否有赢的机会才去攻打敌人的阵营。但是,在军队内有可能存有叛徒和敌军的间谍,左右将军们的决定又扰乱整体军队的秩序。因此表决的结果并不一定能代表大多数人的意见。这时候,在已知有成员谋反的情况下,其余忠诚的将军在不受叛徒的影响下如何达成一致的协议,拜占庭问题就此形成。
拜占庭将军问题实则是一个协议问题。一个可靠的分布式系统必须容忍一个或多个部分的失效,失效的部分可能会送出相互矛盾的信息给系统的其他部分。纽约的一家银行可以在东京、巴黎、苏黎世设置异地备份,当某些点受到攻击甚至破坏以后,可以保证账目仍然不错,得以复原和恢复。从技术的角度讲,这是一个很困难的问题,因为被攻击的系统不但可能不作为,而且可能进行破坏。对于这类故障的问题被抽象地表达为拜占庭将军问题。
解决拜占庭将军问题的算法必须保证
拜占庭问题的解决可能性
(1)叛徒数大于或等于1/3,拜占庭问题不可解
(2)用口头信息,如果叛徒数少于1/3,拜占庭问题可解
(3)用书写信息,如果至少有2/3的将军是忠诚的,拜占庭问题可解
例如,如果司令是叛徒,他发送“进攻”命令给将军1,并带有他的签名0,发送“撤退”命令给将军2,也带签名0。将军们转送时也带了签名。于是将军1收到{“进攻”:0,“撤退”:0,2},说明司令发给自己的命令是“进攻”,而发给将军2的命令是“撤退”,司令对我们发出了不同的命令。对将军2同解。
算法起源
实现原理
Paxos算法的主要交互过程在Proposer和Acceptor之间。Proposer与Acceptor之间的交互主要有4类消息通信。这4类消息对应于paxos算法的两个阶段4个过程:
阶段1:
阶段2:
Paxos算法的最大优点在于它的限制比较少,它允许各个角色在各个阶段的失败和重复执行,这也是分布式环境下常有的事情,只要大伙按照规矩办事即可,算法的本身保障了在错误发生时仍然得到一致的结果。
参考我的另外一篇博文算法高级(6)-Raft算法
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
ZAB协议的核心是定义了对于那些会改变ZooKeeper服务器数据状态的事务请求的处理方式,即所有事务请求必须由一个全局唯一的服务器来协调处理,这样的服务器被称为Leader服务器,而余下的服务器则称为Follower服务器,ZooKeeper后来又引入了Observer服务器,主要是为了解决集群过大时众多Follower服务器的投票耗时时间较长问题,这里不做过多讨论。Leader服务器负责将一个客户端事务请求转换成一个事务Proposal(提议),并将该Proposal分发给集群中所有的Follower服务器。之后Leader服务器需要等待所有Follower服务器的反馈信息,一旦超过半数的Follower服务器进行了正确的反馈后,那么Leader就会再次向所有的Follower服务器分发Commit消息,要求其将前一个Proposal进行提交。
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服务器。
整个ZAB协议主要包括消息广播和崩溃恢复这两个过程,进一步可以细分为三个阶段,分别是发现、同步和广播阶段。组成ZAB协议的每一个分布式进程,会循环地执行这三个阶段,我们将这样一个循环称为一个主进程周期。
发现:要求zookeeper集群必须选举出一个 Leader 进程,同时 Leader 会维护一个 Follower 可用客户端列表。将来客户端可以和这些 Follower节点进行通信。
同步:Leader 要负责将本身的数据与 Follower 完成同步,做到多副本存储。这样也是提现了CAP中的高可用和分区容错。Follower将队列中未处理完的请求消费完成后,写入本地事务日志中。
广播:Leader 可以接受客户端新的事务Proposal请求,将新的Proposal请求广播给所有的 Follower。
若进行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规则如下
对于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。
在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) 改变服务器的状态。与启动时过程相同。
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算法则是用于构建一个分布式的一致性状态机系统。
Split-Brain问题说的是1个集群如果发生了网络故障,很可能出现1个集群分成了两部分,而这两个部分都不知道对方是否存活,不知道到底是网络问题还是直接机器down了,所以这两部分都要选举1个Leader,而一旦两部分都选出了Leader, 并且网络又恢复了,那么就会出现两个Brain的情况,整个集群的行为不一致了。
在使用zookeeper的过程中,我们经常会看到这样一些说法:
所谓整个集群是否可用,隐含的一个意思就是整个集群还能够选举出一个”Leader”。ZooKeeper默认设置的是采用Majority Qunroms的方式来支持Leader选举。以这种方式来防止Split-Brain问题出现,即只有集群中超过半数节点投票才能选举出Leader。这样的方式可以确保leader的唯一性,要么选出唯一的一个leader,要么选举失败。
在ZooKeeper中Quorums有2个作用:
理解了Quorums就不难理解为什么集群中的节点数一般配置为奇数。节点数配置成奇数的集群的容忍度更高。
举例如下:
所以4个节点的集群的容忍度 = 3个节点的集群的容忍度,但是4个节点的集群多了1个节点,相当于浪费了资源。
更极端的例子是100个节点的集群,如果网络问题导致分为两个部分,50个节点和50个节点,这样整个集群还是不可用的,因为按照Quorums的方式必须51个节点才能保证选出1个Leader。这时候可以采用Weight加权的方式,有些节点的权值高,有些节点的权值低,最后计算权值,只要权值过半,也能选出1个Leader。
我的微信公众号:架构真经(id:gentoo666),分享Java干货,高并发编程,热门技术教程,微服务及分布式技术,架构设计,区块链技术,人工智能,大数据,Java面试题,以及前沿热门资讯等。每日更新哦!
参考资料: