分布式中有这么一个疑难问题,客户端向一个分布式集群的服务端发出一系列更新数据的消息,由于分布式集群中的各个服务端节点是互为同步数据的,所以运行完客户端这系列消息指令后各服务端节点的数据应该是一致的,但由于网络或其他原因,各个服务端节点接收到消息的序列可能不一致,最后导致各节点的数据不一致。要确保数据一致,需要选举算法的支撑,这就引申出了今天我们要讨论的题目,关于选举算法的原理解释及实现,选举包括对机器的选举,也包括对消息的选举。
选举算法
最简单的选举算法
如果你需要开发一个分布式集群系统,一般来说你都需要去实现一个选举算法,选举出Master节点,其他节点是Slave节点,为了解决Master节点的单点问题,一般我们也会选举出一个Master-HA节点。
这类选举算法的实现可以采用本文后面介绍的Paxos算法,或者使用ZooKeeper组件来帮助进行分布式协调管理,当然也有很多应用程序采用自己设计的简单的选举算法。这类型简单的选举算法可以依赖很多计算机硬件因素作为选举因子,比如IP地址、CPU核数、内存大小、自定义序列号等等,比如采用自定义序列号,我们假设每台服务器利用组播方式获取局域网内所有集群分析相关的服务器的自定义序列号,以自定义序列号作为优先级,如果接收到的自定义序列号比本地自定义序列号大,则退出竞争,最终选择一台自定义序列号最大的服务器作为Leader服务器,其他服务器则作为普通服务器。这种简单的选举算法没有考虑到选举过程中的异常情况,选举产生后不会再对选举结果有异议,这样可能会出现序列号较小的机器被选定为Master节点(有机器临时脱离集群),实现伪代码如清单1所示。
清单1简单选举算法实现伪代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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
;
|
拜占庭问题
原始问题起源于东罗马帝国(拜占庭帝国)。拜占庭帝国国土辽阔,为了防御目的,每支军队都分隔很远,将军之间只能依靠信差传信。在战争的时候,拜占庭军队内所有司令和将军必需达成一致的共识,决定是否有赢的机会才去攻打敌人的阵营。但是,在军队内有可能存有叛徒和敌军的间谍,左右将军们的决定又扰乱整体军队的秩序。因此表决的结果并不一定能代表大多数人的意见。这时候,在已知有成员谋反的情况下,其余忠诚的将军在不受叛徒的影响下如何达成一致的协议,拜占庭问题就此形成。
拜占庭将军问题实则是一个协议问题。一个可信的计算机系统必须容忍一个或多个部件的失效,失效的部件可能送出相互矛盾的信息给系统的其他部件。这正是目前网络安全要面对的情况,如银行交易安全、存款安全等。美国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:
- a) proposer向网络内超过半数的acceptor发送prepare消息;
- b) acceptor正常情况下回复promise消息。
阶段2:
- a) 在有足够多acceptor回复promise消息时,proposer发送accept消息;
- 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集群选举日志输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
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
=&
nbsp
;
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协议实现原理
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服务器完成了状态。在清单4所示选举的基础上,我们把Leader节点的进程手动关闭(kill -9 pid),随即进入崩溃恢复模式,重新选举Leader的过程日志输出如清单5所示。
清单5ZooKeeper重新集群选举日志输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
|
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协议的每一个分布式进程,会循环地执行这三个阶段,我们将这样一个循环称为一个主进程周期。
- 阶段一:发现
阶段一主要就是Leader选举过程,用于在多个分布式进程中选举出主进程,准Leader和Follower的工作流程分别如下。
1.Follower将自己最后接受的事务Proposal的epoch值发送给准Leader;
2.当接收到来自过半Follower的消息后,准Leader会生成消息给这些过半的Follower。关于这个epoch值e’,准Leader会从所有接收到的CEPOCH消息中选取出最大的epoch值,然后对其进行加1操作,即为e’。
3.当Follower接收到来自准Leader的NEWEPOCH消息后,如果其检测到当前的CEPOCH值小于e’,那么就会将CEPOCH赋值为e’,同时向这个准Leader反馈ACK消息。在这个反馈消息中,包含了当前该Follower的epoch CEPOCH(F p),以及该Follower的历史事务Proposal集合:hf。
当Leader接收到来自过半Follower的确认消息ACK之后,Leader就会从这过半服务器中选取出一个Follower,并使用其作为初始化事务集合Ie’。
ZooKeeper选举算法执行流程图如图4所示。
图4. ZooKeeper选举算法流程图
- 阶段二:同步
在完成发现流程之后,就进入了同步阶段。在这一阶段中,Leader和Follower的工作流程如下:
1.Leader会将e’和Ie’以NEWLEADER(e’,Ie’)消息的形式发送给所有Quorum中的Follower。
2.当Follower接收到来自Leader的NEWLEADER(e’,Ie’)消息后,如果Follower发现CEPOCH(F p)不等于e’,就直接进入下一轮循环,因为此时Follower发现自己还在上一轮,或者更上轮,无法参与本轮的同步。
如果等于e’,那么Follower就会执行事务应用操作。
最后,Follower会反馈给Leader,表明自己已经接受并处理了所有Ie’中的事务Proposal。
3.当Leader接收到来自过半Follower针对NEWLEADER(e’,Ie’)的反馈消息后,就会向所有的Follower发送commit消息。至此Leader完成阶段二。
4.当Follower收到来自Leader的Commit消息后,就会依次处理并提交所有的Ie’中未处理的事务。至此Follower完成阶段二。
新增的节点会从Leader节点同步最新的镜像,日志输出如清单8所示。
清单6新增节点的同步信息日志输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
2016
-
06
-
14
19
:
45
:
41
,
173
[
myid
:
3
]
-
INFO
[
QuorumPeer
[
myid
=
3
]
/
0
:
0
:
0
:
0
:
0
:
0
:
0
:
0
:
2181
:
Follower
@
63
]
-
FOLLOWING
-
LEADER
ELECTION
TOOK
-
16
2016
-
06
-
14
19
:
45
:
41
,
175
[
myid
:
3
]
-
INFO
[
QuorumPeer
[
myid
=
3
]
/
0
:
0
:
0
:
0
:
0
:
0
:
0
:
0
:
2181
:
QuorumPeer
$
QuorumServer
@
149
]
-
Resolved
hostname
:
10.17.138.225
to
address
:
/
10.17.13
8.225
2016
-
06
-
14
19
:
45
:
41
,
179
[
myid
:
3
]
-
INFO
[
QuorumPeer
[
myid
=
3
]
/
0
:
0
:
0
:
0
:
0
:
0
:
0
:
0
:
2181
:
Learner
@
329
]
-
Getting
a
snapshot
from
leader
2016
-
06
-
14
19
:
45
:
41
,
182
[
myid
:
3
]
-
INFO
[
QuorumPeer
[
myid
=
3
]
/
0
:
0
:
0
:
0
:
0
:
0
:
0
:
0
:
2181
:
FileTxnSnapLog
@
240
]
-
Snapshotting
:
0xe00000007
to
/
home
/
hemeng
/
zookeeper
-
3.4.7
/
data
/
zookeeper
/
version
-
2
/
snapshot
.
e00000007
2016
-
06
-
14
19
:
45
:
44
,
344
[
myid
:
3
]
-
INFO
[
NIOServerCxn
.
Factory
:
0.0.0.0
/
0.0.0.0
:
2181
:
NIOServerCnxnFactory
@
192
]
-
Accepted
socket
connection
from
/
127.0.0.1
:
56072
2016
-
06
-
14
19
:
45
:
44
,
346
[
myid
:
3
]
-
INFO
[
NIOServerCxn
.
Factory
:
0.0.0.0
/
0.0.0.0
:
2181
:
NIOServerCnxn
@
827
]
-
Processing
srvr
command
from
/
127.0.0.1
:
56072
2016
-
06
-
14
19
:
45
:
44
,
347
[
myid
:
3
]
-
INFO
[
Thread
-
1
:
NIOServerCnxn
@
1008
]
-
Closed
socket
connection
for
client
/
127.0.0.1
:
56072
(
no
session
established
for
client
)
|
清单7新增节点时Leader节点的日志输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
2016
-
06
-
14
19
:
44
:
58
,
835
[
myid
:
2
]
-
INFO
[
/
10.17.138.225
:
3888
:
QuorumCnxManager
$
Listener
@
541
]
-
Received
connection
request
/
10.17.138.227
:
48764
2016
-
06
-
14
19
:
44
:
58
,
837
[
myid
:
2
]
-
INFO
[
WorkerReceiver
[
myid
=
2
]
:
FastLeaderElection
@
600
]
-
Notification
:
1
(
message
format
version
)
,
3
(
n
.
leader
)
,
0xe00000000
(
n
.
zxid
)
,
0x1
(
n
.
round
)
,
LOOKING
(
n
.
state
)
,
3
(
n
.
sid
)
,
0xe
(
n
.
peerEpoch
)
LEADING
(
my
state
)
2016
-
06
-
14
19
:
44
:
58
,
850
[
myid
:
2
]
-
INFO
[
LearnerHandler
-
/
10.17.138.227
:
36025
:
LearnerHandler
@
329
]
-
Follower
sid
:
3
:
info
:
org
.
apache
.
zookeeper
.
server
.
quorum
.
QuorumPeer
$
QuorumServer
@
1de18380
2016
-
06
-
14
19
:
44
:
58
,
851
[
myid
:
2
]
-
INFO
[
LearnerHandler
-
/
10.17.138.227
:
36025
:
LearnerHandler
@
384
]
-
Synchronizing
with
Follower
sid
:
3
maxCommittedLog
=
0xe00000007
minCommittedLog
=
0xe00000001
peerLastZxid
=
0xe00000000
2016
-
06
-
14
19
:
44
:
58
,
852
[
myid
:
2
]
-
WARN
[
LearnerHandler
-
/
10.17.138.227
:
36025
:
LearnerHandler
@
451
]
-
Unhandled
proposal
scenario
2016
-
06
-
14
19
:
44
:
58
,
852
[
myid
:
2
]
-
INFO
[
LearnerHandler
-
/
10.17.138.227
:
36025
:
LearnerHandler
@
458
]
-
Sending
SNAP
2016
-
06
-
14
19
:
44
:
58
,
852
[
myid
:
2
]
-
INFO
[
LearnerHandler
-
/
10.17.138.227
:
36025
:
LearnerHandler
@
482
]
-
Sending
snapshot
last
zxid
of
peer
is
0xe00000000
zxid
of
leader
is
0xe00000007sent
zxid
of
db
as
0xe00000007
2016
-
06
-
14
19
:
44
:
58
,
894
[
myid
:
2
]
-
INFO
[
LearnerHandler
-
/
10.17.138.227
:
36025
:
LearnerHandler
@
518
]
-
Received
NEWLEADER
-
ACK
message
from
3
|
- 阶段三:广播
完成同步阶段之后,ZAB协议就可以正式开始接收客户端新的事务请求,并进行消息广播流程。
1.Leader接收到客户端新的事务请求后,会生成对应的事务Proposal,并根据ZXID的顺序向所有Follower发送提案<e’,<v,z>>,其中epoch(z)=e’。
2.Follower根据消息接收到的先后次序来处理这些来自Leader的事务Proposal,并将他们追加到hf中去,之后再反馈给Leader。
3.当Leader接收到来自过半Follower针对事务Proposal<e’,<v,z>>的ACK消息后,就会发送Commit<e’,<v,z>>消息给所有的Follower,要求它们进行事务的提交。
4.当Follower接收到来自Leader的Commit<e’,<v,z>>消息后,就会开始提交事务Proposal<e’,<v,z>>。需要注意的是,此时该Follower必定已经提交了事务Proposal<v,z’>。
如清单6所示,新增一个节点,运行日志输出。
清单8新增一个Znode节点Leader节点日志输出
1
2
3
4
5
6
7
8
9
10
11
12
13
|
2016
-
06
-
14
19
:
40
:
43
,
882
[
myid
:
2
]
-
INFO
[
LearnerHandler
-
/
10.17.138.234
:
38144
:
ZooKeeperServer
@
678
]
-
submitRequest
(
)
2016
-
06
-
14
19
:
40
:
43
,
882
[
myid
:
2
]
-
INFO
[
ProcessThread
(
sid
:
2
cport
:
-
1
)
::
PrepRequestProcessor
@
533
]
-
pRequest
(
)
2016
-
06
-
14
19
:
40
:
43
,
933
[
myid
:
2
]
-
INFO
[
CommitProcessor
:
2
:
FinalRequestProcessor
@
87
]
-
processRequest
(
)
2016
-
06
-
14
19
:
40
:
43
,
933
[
myid
:
2
]
-
INFO
[
CommitProcessor
:
2
:
ZooKeeperServer
@
1022
]
-
processTxn
(
)
2016
-
06
-
14
19
:
40
:
43
,
934
[
myid
:
2
]
-
INFO
[
CommitProcessor
:
2
:
DataTree
@
457
]
-
createNode
(
)
2016
-
06
-
14
19
:
40
:
43
,
934
[
myid
:
2
]
-
INFO
[
CommitProcessor
:
2
:
WatchManager
@
96
]
-
triggerWatch
(
)
2016
-
06
-
14
19
:
40
:
43
,
934
[
myid
:
2
]
-
INFO
[
CommitProcessor
:
2
:
WatchManager
@
96
]
-
triggerWatch
(
)
|
清单9新增一个Znode节点Follower节点日志输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
2016
-
06
-
14
18
:
46
:
04
,
488
[
myid
:
1
]
-
INFO
[
NIOServerCxn
.
Factory
:
0.0.0.0
/
0.0.0.0
:
2181
:
ZooKeeperServer
@
678
]
-
submitRequest
(
)
2016
-
06
-
14
18
:
46
:
04
,
489
[
myid
:
1
]
-
INFO
[
CommitProcessor
:
1
:
FinalRequestProcessor
@
87
]
-
processRequest
(
)
2016
-
06
-
14
18
:
46
:
04
,
489
[
myid
:
1
]
-
INFO
[
CommitProcessor
:
1
:
FinalRequestProcessor
@
163
]
-
request
.
type
=
11
2016
-
06
-
14
18
:
46
:
14
,
154
[
myid
:
1
]
-
WARN
[
NIOServerCxn
.
Factory
:
0.0.0.0
/
0.0.0.0
:
2181
:
NIOServerCnxn
@
357
]
-
caught
end
of
stream
exception
EndOfStreamException
:
Unable
to
read
additional
data
from
client
sessionid
0x1554e0e2c160001
,
likely
client
has
closed
socket
at
org
.
apache
.
zookeeper
.
server
.
NIOServerCnxn
.
doIO
(
NIOServerCnxn
.
java
:
228
)
at
org
.
apache
.
zookeeper
.
server
.
NIOServerCnxnFactory
.
run
(
NIOServerCnxnFactory
.
java
:
203
)
atjava
.
lang
.
Thread
.
run
(
Thread
.
java
:
745
)
2016
-
06
-
14
18
:
46
:
14
,
156
[
myid
:
1
]
-
INFO
[
NIOServerCxn
.
Factory
:
0.0.0.0
/
0.0.0.0
:
2181
:
NIOServerCnxn
@
1008
]
-
Closed
socket
connection
for
client
/
0
:
0
:
0
:
0
:
0
:
0
:
0
:
1
:
42678
which
had
sessionid
0x1554e0e2c160001
|
实现代码
具体关于Leader节点的选举程序代码分析,请见本人的另一篇文章《Apache ZooKeeper服务启动源码解释》。
- 重新选举Leader节点
如清单5所示,当手动关闭Leader节点后,原有的Follower节点会通过QuorumPeer对应的线程发现Leader节点出现异常,并开始重新选举,线程代码如清单10所示。
清单10QuorumPeer线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
case
FOLLOWING
:
try
{
LOG
.
info
(
"FOLLOWING"
)
;
setFollower
(
makeFollower
(
logFactory
)
)
;
follower
.
followLeader
(
)
;
}
catch
(
Exception
e
)
{
LOG
.
warn
(
"Unexpected exception"
,
e
)
;
}
finally
{
follower
.
shutdown
(
)
;
setFollower
(
null
)
;
setPeerState
(
ServerState
.
LOOKING
)
;
}
break
;
|
所有的Follower节点都会进入到Follower类进行主节点的检查,如清单11所示。
清单11Follower类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
void
followLeader
(
)
throws
InterruptedException
{
try
{
connectToLeader
(
addr
)
;
longnewEpochZxid
=
registerWithLeader
(
Leader
.
FOLLOWERINFO
)
;
//check to see if the leader zxid is lower than ours
//this should never happen but is just a safety check
longnewEpoch
=
ZxidUtils
.
getEpochFromZxid
(
newEpochZxid
)
;
if
(
newEpoch
<
self
.
getAcceptedEpoch
(
)
)
{
LOG
.
error
(
"Proposed leader epoch "
+
ZxidUtils
.
zxidToString
(
newEpochZxid
)
+
" is less than our accepted epoch "
+
ZxidUtils
.
zxidToString
(
self
.
getAcceptedEpoch
(
)
)
)
;
throw
new
IOException
(
"Error: Epoch of leader is lower"
)
;
}
syncWithLeader
(
newEpochZxid
)
;
QuorumPacketqp
=
new
QuorumPacket
(
)
;
while
(
self
.
isRunning
(
)
)
{
readPacket
(
qp
)
;
processPacket
(
qp
)
;
}
}
catch
(
Exception
e
)
{
LOG
.
warn
(
"Exception when following the leader"
,
e
)
;
try
{
sock
.
close
(
)
;
}
catch
(
IOException
e1
)
{
e1
.
printStackTrace
(
)
;
}
// clear pending revalidations
pendingRevalidations
.
clear
(
)
;
}
|
RecvWorker线程会继续抛出Leader连接不上的错误。
经过一系列的SHUTDOWN操作后,退出了之前集群正常时的线程,重新开始新的选举,有进入了LOOKING状态,首先通过QuorumPeer类的loadDataBase方法获取最新的镜像,然后在FastLeaderElection类内部,传入自己的ZXID和MYID,按照选举机制对比ZXID和MYID的方式,选举出Leader节点,这个过程和初始选举方式是一样的。
- 集群稳定后新加入节点
集群稳定后ZooKeeper在收到新加节点请求后,不会再次选举Leader节点,会直接给该新增节点赋予FOLLOWER角色。然后通过清单11的代码找到Leader节点的IP地址,然后通过获取到的最新的EpochZxid,即最新的事务ID,调用方法syncWithLeader查找最新的投票通过的镜像(Snap),如清单12所示。
清单12Learner类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
protected
void
syncWithLeader
(
long
newLeaderZxid
)
throws
IOException
,
InterruptedException
{
caseLeader
.
UPTODATE
:
if
(
!
snapshotTaken
)
{
// true for the pre v1.0 case
zk
.
takeSnapshot
(
)
;
self
.
setCurrentEpoch
(
newEpoch
)
;
}
self
.
cnxnFactory
.
setZooKeeperServer
(
zk
)
;
breakouterLoop
;
|
清单13保存最新Snap
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public
void
save
(
DataTreedataTree
,
ConcurrentHashMap
<
Long
,
Integer
>
sessionsWithTimeouts
)
throwsIOException
{
longlastZxid
=
dataTree
.
lastProcessedZxid
;
File
snapshotFile
=
new
File
(
snapDir
,
Util
.
makeSnapshotName
(
lastZxid
)
)
;
LOG
.
info
(
"Snapshotting: 0x{} to {}"
,
Long
.
toHexString
(
lastZxid
)
,
snapshotFile
)
;
snapLog
.
serialize
(
dataTree
,
sessionsWithTimeouts
,
snapshotFile
)
;
}
|
- 新提交一个事务
当新提交一个事务,例如清单6所示是新增一个ZNODE,这时候会按照ZooKeeperServer->PrepRequestProcessor->FinalRequestProcessor->ZooKeeperServer->DataTree这个方式新增这个节点,最终由ZooKeeperServer类的submitRequest方法提交Proposal并完成。在这个提交Proposal的过程中,FOLLOWER节点也需要进行投票,如清单14所示。
清单14处理投票
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
protected
void
processPacket
(
QuorumPacketqp
)
throws
IOException
{
switch
(
qp
.
getType
(
)
)
{
caseLeader
.
PING
:
ping
(
qp
)
;
break
;
caseLeader
.
PROPOSAL
:
TxnHeaderhdr
=
new
TxnHeader
(
)
;
Record
txn
=
SerializeUtils
.
deserializeTxn
(
qp
.
getData
(
)
,
hdr
)
;
if
(
hdr
.
getZxid
(
)
!=
lastQueued
+
1
)
{
LOG
.
warn
(
"Got zxid 0x"
+
Long
.
toHexString
(
hdr
.
getZxid
(
)
)
+
" expected 0x"
+
Long
.
toHexString
(
lastQueued
+
1
)
)
;
}
lastQueued
=
hdr
.
getZxid
(
)
;
fzk
.
logRequest
(
hdr
,
txn
)
;
break
;
|
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算法则是用于构建一个分布式的一致性状态机系统。
结束语
通常在分布式系统中,构成一个集群的每一台机器都有自己的角色,最典型的集群模式就是Master/Slave模式(主备模式)。在这种模式中,我们把能够处理所有写操作的机器称为Master机器,把所有通过异步复制方式获取最新数据,并提供读服务的机器称为Slave机器。在Paxos算法内部,引入了Proposer、Acceptor和Learner三种角色。而在ZooKeeper中,这些概念也做了改变,它没有沿用传统的Master/Slave概念,而是引入了Leader、Follower和Observer三种角色。本文通过对Paxos和ZooKeeper ZAB协议进行讲解,让读者有一些基本的分布式选举算法方面的认识。
参考资源 (resources)
- 参考ZooKeeper文档首页,了解IBM开发者论坛已经收录的ZooKeeper文章。
- 参考 ZooKeeper原理 文章,了解ZooKeeper的原理性中文文章。
- 参考书籍《ZooKeeper Essential》作者作为ZooKeeper社区的主要推动者,对于ZooKeeper技术有深入的介绍。
- 参考书籍《从Paxos到ZooKeeper》,作者的书是国内第一本ZooKeeper书籍。