关于若干选举算法的解释与实现

743 查看

已经出版的《大话Java性能优化》请大家多多支持,《深入学习JVM&G1 GC》、《动手学习Apache ZooKeeper》2016年下半年出版。

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

选举算法

最简单的选举算法

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

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

清单1简单选举算法实现伪代码

拜占庭问题

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

拜占庭将军问题实则是一个协议问题。一个可信的计算机系统必须容忍一个或多个部件的失效,失效的部件可能送出相互矛盾的信息给系统的其他部件。这正是目前网络安全要面对的情况,如银行交易安全、存款安全等。美国911恐怖袭击发生之后,大家普遍认识到银行的异地备份非常重要。纽约的一家银行可以在东京、巴黎、苏黎世设置异地备份,当某些点受到攻击甚至破坏以后,可以保证账目仍然不错,得以复原和恢复。从技术的角度讲,这是一个很困难的问题,因为被攻击的系统不但可能不作为,而且可能进行破坏。国家的安全就更不必说了,对付这类故障的问题被抽象地表达为拜占庭将军问题。

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

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同解。

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:

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

阶段2:

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

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

ZooKeeper ZAB协议

基本概念

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

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

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

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

清单4ZooKeeper集群选举日志输出