paxos算法可以说是大名鼎鼎,一方面是因为它是一个很优秀的分布式一致性算法,另一方面也是由于它的晦涩难懂。今天看到了一个博客(Paxos算法原理与推导)深入浅出的讲了一下这个算法,感觉收获很大,于是借此平台写一下自己对该算法的理解,同时也希望能够通过自己写的过程加深对这个算法的理解。
paxos算法的目标
一个算法自然要有一个确定性的目标。作为分布式一致性算法,paxos的目标是:
- 能够在集群中对于某个数据的值达成一致
- 不论发生网络异常还是机器宕机,都不会破坏第一条中的一致性
角色
在paxos算法中,主要有三种角色:proposer、acceptor和learner。同一个进程可以扮演三种角色中的任一/二/三种。
proposer负责生成提案给acceptor,acceptor负责接受提案,learner负责学习接受的提案。在这个定义中,提案被定义为对一个值具体是什么的描述。
当一个提案被半数以上的acceptor接受,我们称为该提案被选定。
问题描述
- 只有被提出的value才能被选定
- 只有一个value被选定,并且
- 如果某个进程认为value被选定了,那么这个value必须是真正被选定的哪个
约束
为了能够选出来一个一致的值,我们需要对上面三个角色的行为做一定的约束。
首先,为了能够产生选定的提案,需要下面的约束:
P1: 一个Acceptor必须接受它接收到的第一个提案
但是,这个约束会产生一个问题:假设有三个Proposer,三个Acceptor,每个acceptor接受了其中一个的值,那么没有值可以被选定。所以,我们需要允许Acceptor接受多个提案。为了后面描述,我们规定每个提案必须有个代表顺序的编号N,这样提案可以抽象为<N, V>。
但是,这带来一个问题,由于允许Acceptor接受多个提案,会有多个提案被选定。因此,我们引入以下约束来避免该问题:
P2: 如果某个值为v的提案被选定,那么每个编号更高的被选定提案的值也必须是v
在具体实现的时候,P2可以用一个更强的更具体的约束代替:
P2a: 如果某个值为v的提案被选定,那么每个编号更高的被acceptor接受提案的值也必须是v
但是,考虑以下情况:如果一个值v1被选定了,且acceptor 2,3,4,5都接受了该值。但acceptor 1挂了,后面起来后收到了v2的提案,这时候它不知道v1已经被选定了,因此又会接受v2,违反了P2a。所以P2a是在存在acceptor故障的情况下无法实现的,因此还需要更加强的约束:
P2b:如果某个值为v的提案被选定,那么每个proposer之后编号更高的提案的值也必须是v
那么P2b如何实现呢,可以使用P2c:
P2c:对于任一N和V,如果[N, V]被提出,必须存在一个超过半数的acceptor的集合S,满足下列条件之一:
1. S中的acceptor没有接受过编号为N以下的提案
2. S接受过的提案中,编号最大的提案的值为V
可以看出,如果满足P2c,那么如果[N, V]被提出,那么一定不会有超过半数的acceptor集合接受了值不为V的提案,也就是满足了P2b。
但是,其实还有关键的一点:在上面的情况中,如果有超过半数的acceptor集合接受了值不为V的提案,那么proposer的提案就需要跟随已经被接受的提案。那么跟随哪个呢,这个需要proposer对acceptor已经接受提案的值来学习得出。
为了满足P2c,我们提出下面的提案生成算法:
- proposer提出一个新的提案编号N,然后向半数以上的acceptor发出请求,内容有两点:1)请acceptor不再接受编号小于N的提案;2)如果acceptor之前接受过提案,那么把其中编号最大的响应给proposer。
- 如果proposer收到了半数以上acceptor的响应,那么就生成一个提案[N, V],其中V是响应中带值的提案中编号最大的那个V。如果响应中都不带值,那么V可以自选;生成后将提案发给半数以上的acceptor。如果没有收到,那么获取新的N,回到步骤1.
对于acceptor,根据提案生成算法,需要加强为如下约束:
P1a:一个acceptor只要没有响应过任何编号大于N的请求,那么它就可以接受这个编号为N的提案
这样就不违背proposer提出的要求。
在收到请求的时候,需要进行如下步骤以满足约束:
- 如果提案编号N小于当前响应提案的最大编号ResN,不响应或响应错误
- 如果不满足1,则返回当前接受过的最大编号的提案(如果有的话),并且承诺不再接受编号小于N的提案。
- 如果收到的是提案,只要acceptor没有对编号大于N的请求做过响应,那么就接受该提案
综上,可以得到paxos算法。算法分为两阶段:
- 阶段1: (a) proposer生成一个编号N的请求,发个半数以上的acceptor。
(b) acceptor收到一个编号为N的请求后,如果N<ResN,直接拒绝该请求;否则返回目前接受的编号最大的提案(如果有的话),同时承诺不再接受编号小于N的提案。 - 阶段2
(a) 如果proposer收到了半数以上acceptor的请求回复,那么按照约束生成提案[N, V]
(b) 如果acceptor收到了提案[N, V],如果没有响应过编号大于N的请求,那么就接受之。