一、简介
Paxos算法是Leslie Lamport在1990提出一个基于消息通信/传递的一致性算法,用于解决在分布式环境中(存在机器宕机、网络异常等问题),如何快速且正确的在集群内部对某个数据的值达成一致。算法特点:高度容错性、且不会破坏系统的一致性等
二、Paxos算法历史
Paxos的历史很有意思,历经波折,辗转很久:
- 最早在1982年,Lamport在论文The byzantine generals problem,提出了一种计算机容错理论,在这篇论文中,Lamport设想了一个场景如下:
拜占庭帝国存在很多军队,在战争的时候,不同的军队的将军必须制定一个统一的作战计划,作出进攻还是撤退的决定,从而达成一致的共识,获取胜利。各个将军地理上是分割的,只能依赖军队的通讯云来互相通讯。
但是,在军队内有可能存有叛徒和敌军的间谍(可以任意篡改消息,欺骗将军), 因此在进行共识时,结果并不一定代表大多数人的意见。拜占庭问题就是说在可能有成员谋反的情况下,如何达成一致的协议
理论上,在不可靠的信道和异步系统上达成一致状态是不可能的,因此我们研究一致性的算法,假设信道是可靠的,不可以篡改的,即非拜占庭问题
- 1990年,Lamport提出了一个解决一致性问题的方案,同时给出了数学证明。通过描绘了一种场景用于描述:
古希腊有一个叫做Paxos的岛屿,小岛上所有法令(Decress)都由议会的议员(Legislator)表决通过,然后由议员记录在各自手中的律薄(Ledger)上。但是由于这里商业繁荣,没有人愿意做专职的议员,都是由小岛居民兼职的。由于平时工作很忙,所以兼职议员们会经常进出国会大厅,甚至中途去出海捕鱼,半年后再回到国会大厅。
当时《The Part-Time Parliament》提交给ACM TOCS评审会,不过遗憾的是当时没有人理解这篇论文以及故事,且希望用严谨的数据来证明该算法的正确性,否则就拒绝接受。Lamport不想改,So 没有成功发表
-
1998年,延迟了将近9年之后, 《The Part-Time Parliament》公开发表,之前被人审视挖掘出来{伯乐,微软的Butler Lampson以及麻省理工学院的Nancy Lynch公布了基于《The Part-Time Parliament》用数学的形式化定义证明的论文}
-
2001年,Lamport进行了妥协,放弃了故事的描述方式,用更为通俗易懂的语言来描述Paxos肃算法,因此才有个这篇论文,Paxos Made Simple
三、Paxos算法
The Problem
假设存在一组可以提出提案的进程集合,需要基于多数派基于多数派共识解决分布式系统各节点就某个值达成一致的问题
假设
- 不同进程节点可出现异常,比如宕机、通信消息丢失/乱序
- 非拜占庭场景,消费不能被篡改
safety-安全性约束
- 一个值只有被propose,才能被chosen
- 只能有一个值被chosen,达成一致
- 一个值只有被chosen后,才能被learn
liveness-活性约束
- 最终一定会有一个值被chosen
- 一个值一旦被chosen,最终一定会被learn
角色
一共有三种角色,Proposer,Acceptor,Learner
Choosing a value
- 最简单的方式选定一个提案,只允许有一个Acceptor,Proposer只能发提案给该Acceptor,Acceptor来接受第一个第一个提案来作为选定的提案这个解决很简单,但是存在单点问题,唯一的Acceptor挂了,系统就无法工作
- 引用多个Acceptor,需要如何达成决议?这里就是多数派的概念,比如5个有3个达成一致就满足多数派
理论推导
在没有失败或者消息丢失情况下,只有一个Proposer进行提案,也可以达成选出,这暗示了第一个要求:
P1: 一个Acceptor必须批准他收到第一个提案
但是这个要求有其他问题,比如多个Proposer提出多个提案,每个Acceptor都批准了收到第一个提案,但是没有一个提案满足多数派。即使只有两个提案提出,比如存在5个Acceptor, 存在一个Accepor异常,如果两个提案分别被2个Acceptor批准,也不能达成共识
因此,在P1的基础上,再加上需要满足多数派,意味着每一个Acceptor必须能批准不止一个提案。这里提案,包括全局编号和value,【编号,value】
基于上面所述,我们需要保证被选定的提案具有同样的value,因此这引出了进一步的要求:
P2: 如果一个提案,编号为M0, value值为V0, 即[M0, V0]被选定,那有更高编号,且被选定,value值也是V0。即存在一个提案[M1, V1]被选定,且M1>M0, 那么V0=V1
之所以有P2,就是为了满足最开始safety-安全性-只能有一个值被chosen,达成一致。
进一步,提案一定是被Acceptor批准,可以通过以下要求满足P2,
P2a: 如果一个提案,编号为M0, value值为V0, 即[M0, V0]被选定,那有更高编号,且被Acceptor批准,value值也是V0。即存在一个提案[M1, V1]被选定,且M1>M0, 那么V0=V1
基于上述,我们需要保证P1,但消息通信是异步的,一个提案可能在某个Acceptor还没收到,已经满足多数派,提案被选定了。
比如:存在一个Proposer1,提出了提案[M0, V0], 有5个Acceptor,其中第一个Acceptor1因为网络原因还没有收到该提案,该提案已经被其他4个Acceptor批准了。这时候如果有另外一个Proposer2,提出了[M1, V1], 且M1>M0, V0!=V1,这个提案发给Acceptor1后,按照P1,就会批准该提案,这和P2a产生矛盾。
因此为了同时满足P1和P2a, 强化P2a得到如下要求:
P2b: 如果一个提案,编号为M0, value值为V0, 即[M0, V0]被选定,那有之后任务Proposer产生编号更高的提案,其value都是V0, 即提出一个提案[M1, V1],且M1>M0, 那么V0=V1
推导关系如下:P2b->P2a->P2
P2b这里比较好证明,用数学归档法即可。这里不再详述。
P2c:对于任意的的一个提案[Mn, Vn],如果该提案被提出,那么肯定存在一个半数以上的Acceptor组成的集合S,满足以下两个条件的任何一个:
(1)集合S中不存在任何批准过编号小于Mn的提案的Acceptor-(含义就是集合S中的Acceptor都没有批准过小于Mn的提案,[Mn, Vn]是新的一轮)
(2)集合S中所有Acceptor批准过小于Mn的提案,其中编号最大的提案对应的value一定是Vn
P2c->P2b
基于上述推导,P2c->P2b->P2a->P2
Proposer生成提案
基于上面描述,每当Proposer要生成提案的时候,要通过某种学习的方式,获取已经通过的提案信息。Proposer在产生一个编号为Mn的提案时,假设编号为Mn,需要知道将要或者已经被多数派Acceptor批准的最大提案(满足编号都小于Mn);同时要求所有的Acceptor不会批准小于Mn的提案。因此,Proposer生成提案算法如下:
- Prepare请求:
Proposer选择一个提案编号Mn,然后向某个Acceptor集合发送请求,要求Acceptor做出如下回应:
(1)Acceptor做成承诺,保证不再批准任何编号小于Mn的提案
(2)如果Acceptor已经批准过一些提案,那么就像Proposer反馈消息,告诉Proposer,当前已经批准的编号小于Mn的最大编号的那个提案的Value
- Accept请求:
如果Proposer收到了多数派的Acceptor的响应结果,那么就可以构造自己的提案-[Mn, Vn],其中Vn的生成方式有两种:
(1)如果多数派的Acceptor都没有批准过任何提案,即Acceptor->Proposer的响应不包含任何提案,那么Vn可以由Proposer自己决定,因为没有任何Value可以学习到
(2)如果多数派的Acceptor批准过一些提案,Proposer基于上述Acceptor的响应消息,找到编号最大的那个提案的Value, 当作Vn,这样就学习到
Proposer确定好自己的提案后,就会发送Acceptor集合,这称之为Accept请求
Acceptor批准提案
基于上一节内容,Acceptor会收到请求,即Prepare请求和Accept请求,
Prepare请求: Acceptor可以任意一个时刻响应Prepare请求;
Accept请求:Acceptor在不违背现有承诺前提下,可以任意响应Accept请求
总结Acceptor约束如下:
P1a 一个Acceptor收到一个编号为Mn的提案,只要还没有响应过任何大于Mn的Prepare请求,那么就可以接受编号为Mn的提案
P1a->P1
算法优化
如何过滤或者忽略Prepare请求?
假设一个Acceptor收到一个编号为Mn的Prepare请求,此时该Acceptor已经对于编号大于Mn的Prepare请求做了响应,因此该Acceptor肯定不会再批准编号Mn的提案,因此可以忽略编号为Mn的Preaprea请求
算法流程
基于上述描述,类似两阶段提交的算法流程如下:
阶段一:
- Proposer生成一个提案-编号Mn, 然后向Acceptor集合发送Prepare请求 (编号为Mn)
- 如果一个Acceptor收到编号Mn的Prepare请求,且编号Mn大于该Acceptor已经响应过的所有Prepare请求的编号,就可以已经批准过的最大编号的提案(包括value,用于学习到)回复给Proposer,同时该Acceptor承诺不再批准任何编号小于Mn的提案
阶段二:
- 如果Proposer收到多数派Acceptor的回复(对应编号Mn的prepare请求),那么发送一个提案[Mn, Vn]给Acceptor,其中Vn是收到所有Acceptor回复的中编号最大的提案对应的值;如果Acceptor回复没有任何提案,Proposer可以自行决定任意值
- Acceptor收到[Mn,Vn]的Accept请求,只要该Acceptor尚未对编号大于Mn的Prepare请求做成响应,就可以通过这个提案
选主保证liveness
前面有liveness-活性约束,提到最终一定会有一个值被chosen.
考虑如下极端场景:
有两个Proposer轮流在提案,但是最终都没法选定。
存在Proposer P1 P2,时序如下:
- P1提出一个编号为M1的提案,完成阶段一的流程,
- 此时P2提出一个编号为M2的提案(且M2 > M1),完成阶段一的流程,基于阶段一的描述,此时Acceptor承诺不再批准任何编号小于M2的提案
- 因此P1进入阶段二的流程,会因为上述约束,Accept请求会失败掉
- 此时P1提出一个编号为M3的提案(且M3 > M2),完成阶段一的流程,阻止了P2的阶段二
- …
以此类推,互相组织和循环,导致没有一个值被chosen,违反了liveness
为了保Paxos的liveness以及可持续性,可以选择一个主Proposer,并规定只有主Proproser,可以提出提案。