这是我分布式课程作业报告。希望如果同学们刷到了不要照抄。老师会发现!!!试了一下word导入功能,除了图片不清晰之外还是挺香的。

Paxos算法是莱斯利·兰伯特(Leslie Lamport)于1990年提出的一种基于消息传递且具有高度容错特性的一致性算法,是目前公认的解决分布式一致性问题最有效的算法之一。

在常见的分布式系统中,总会发生诸如机器宕机或网络异常等情况。Paxos算法需要解决的问题就是如何在一个可能发生上述异常的分布式系统中,快速且正确地在集群内部对某个数据的值达成一致,并且保证不论发生以上任何异常,都不会破坏整个系统的一致性。

1 Paxos的诞生

1982年,Lamport发表了论文The Byzantine Generals Problem,提出了一种计算机容错理论。对于拜占庭将军问题,从理论上来说,试图在异步系统和不可靠的通道上来达到一致性状态是不可能的,因此在对一致性的研究过程中,都往往假设信道是可靠的。而事实上,大多数系统都是部署在同一个局域网中的,消息被篡改的情况非常罕见。并且由于硬件和网络原因而造成的消息不完整问题,只需一套简单的校验算法即可避免,因此在实际实践中,可以假设不存在拜占庭问题,也即假设所有消息都是完整的,没有被篡改的。那么,在这种情况下需要什么样的算法来保证一致性呢?

Lamport在1990年提出了一个理论上的一致性解决方案。Lamport 设想出了一个场景来描述这种一致性算法需要解决的问题,及其具体的解决过程:在古希腊有一个叫做Paxos的小岛,岛上采用议会制的形式来选择通过法令,议会中的议员通过信使进行消息的传递。但是要明确一点,大家都是兼职人员,他们随时有可能会离开议会厅,并且信使可能会重复的传递消息,也可能一去不复返。因此,议会协议要保证在这种情况下法令仍然能够正确的产生,并且不会出现冲突。Paxos算法名称的由来也是取自论文中提到的 Paxos 小岛。

2 Paxos算法

Paxos 是一种提高分布式系统容错性的一致性算法,也就是论文 The Part-Time Parliament 中提到的“synod”算法。The Part-Time Parliament这篇以故事形式展开的论文,对于绝大部分人来说太过于晦涩。因此现在的Paxos讲解一般都是围绕Lamport的另一篇关于Paxos的论文Paxos Made Simple展开的,从一个一致性算法所必须满足的条件开始描述Paxos作为一种一致性算法的合理性。

2.1 Paxos的目标

保证最终有一个提案会被选定,当提案被选定后,进程最终也能获取到被选定的提案。

2.2 算法推导

假设有一组可以提出提案的进程集合,那么对于一个一致性算法来说需要保证以下几点:

  • 在这些被提出的提案中,只有一个会被选定。
  • 如果没有提案被提出,那么就不会有被选定的提案。
  • 当一个提案被选定后,进程应该可以获取被选定的提案信息。

对于一个分布式算法,有两个最重要的属性:

  • 安全性(Safety):那些需要保证永远都不会发生的事情
  • 活性(Liveness):那些最终一定会发生的事情

因此将上边一致性算法要求按照安全性(Safety)进行表述的话内容如下:

  • 只有被提出的提案才能被选定。
  • 只能有一个提案会被选定。
  • 如果某个进程认为某个提案被选定了,那么这个提案必须是真的被选定的那个。

对于接下来的Paxos 算法的分析推导,我们先不去精确地定义其活性需求,只从安全性角度分析。

在该一致性算法中,有三种参与角色,分别使用Proposer、Acceptor 和 Learner来表示。

Paxos算法推导详解 | Basic Paxos_数据库

在具体的实现中,一个进程可能充当不止一种角色,在这里我们并不关心进程如何映射到各种角色。

要选定一个唯一提案的最简单方式就是只允许一个Accpetor存在,Proposer只能发送提案给该Accpetor,Acceptor会选择它接收到的第一个提案作为被选定的提案。尽管实现起来非常简单,但是存在一个问题,一旦这个 Accpetor出现问题,那么整个系统就无法工作了。因此应该寻找一种更好的解决方式,例如可以使用多个 Accpetor 来避免Accpetor 的单点问题。

Paxos算法推导详解 | Basic Paxos_paxos_02Paxos算法推导详解 | Basic Paxos_paxos_03

但是当我们有多个Acceptor的时候,假设一个极端的情况下,只有一个proposer提出提案。但是这些accepter都不接受提案,这就会出现一种情况,没有提案被选定。为了避免这种情况我们需要设定一个规则:

在没有失败和消息丢失的情况下,即使只有一个提案被提出,仍然可以选出一个提案。

P1:一个Acceptor必须批准它收到的第一个提案。

但是会引出另外一个问题:如下图左,如果有多个提案被不同的Proposer同时提出,这可能会导致虽然每个Acceptor都批准了它收到的第一个提案,但是这些提案是平票,在这种场景下,依旧无法选定一个提案的。

Paxos算法推导详解 | Basic Paxos_数据库_04Paxos算法推导详解 | Basic Paxos_数据库_05

换个情况,上图右,即使只有两个提案被提出,accepter数量多。但是如果此时一个Acceptor出错,依旧会导致无法选定提案。

导致了出现上面不一致的问题是因为,我们前边说的是“一个提案只要被接受,则该提案就被选定了”因此,我们需要再增加一个条件:要选定一个提案必须获得半数以上的Acceptor批准,这个需求暗示着一个Acceptor必须能够批准不止一个提案。

现在一个acceptor可以接受多个提案了,所以原来的提案表示方法已经不能满足需求了,我们需要对其进行改进。原来的提案 = Value ,现在将其变为一个[id,value]的组合体。提案表示方法个会改变之后,现在变成了一个关于提案 Value 的约定。就是虽然允许多个提案被选定,但是他们必须具有相同的Value值。

因此对于提案的选定,我们需要给他加一个规则:

P2:如果 [M0,V0]被选定了,那么所有比编号M0更高的,且被选定的提案,其Value值必须也是V0

提案被accepter接受才能够被选定,因此我们可以把P2约束改写成对Acceptor接受的提案的约束:

P2a:如果[M0,V0]被选定了,那么所有比编号M0更高的,且被Acceptor批准的提案,其Value值必须也是V0

我们可以知道P2a的范围比P2更广。

此时我们仍然需要 P1 来保证提案会被选定,P1与P2a必须同时存在。

因为通信是异步的,比如下图这个情况。在Acceptor 1没有收到任何提案的情况下,其他4个Acceptor已经批准了来自Proposer 2的提案,而此时,Proposer 1产生了一个具有其他Value值的、编号更高的提案,并发送给了Acceptor 1。根据P1,就需要Acceptor 1批准该提案,但是这与P2a矛盾,因此如果要同时满足P1和P2a,需要对P2a进行如下强化

P2b:如果[M0,V0]被选定后,那么之后任何Proposer产生的编号更高的提案,其Value值都为V0

因为一个提案必须在被Proposer提出后才能被Acceptor批准,因此P2b包含了P2a,进而包含了P2。于是,接下去的重点就是论证P2b成立即可。

保证p2b成立,问题就变为了:如何确保在某个value值为V的提案被选定后,Proposer提出的编号更高的提案的value都是V呢?

我们将上一个规则进行细化:

P2c:对于任意的 Mn和 Vn,如果提案[Mn ,Vn]被提出,那么肯定存在一个由半数以上的Acceptor组成的集合S,满足以下两个条件中的任意一个:

  • S中没有批准过编号小于Mn的提案。
  • S批准过的提案中,如果编号小于Mn,那编号最大提案Value值是Vn。

从上面的内容中,我们可以看到,从P1到P2c的过程其实是对一系列条件的逐步加强,如果需要证明这些条件可以保证一致性。

2.2.1 提出一个提案

接下来我们看看如何在P2c的基础上生成提案。对于Proposer来说,想要不违反规定生成提案,最简单的方法就是询问一下当前有什么提案。这就引出了如下的提案生成算法。

Proposer 选择提案编号Mn,然后向Acceptor 集合发送请求,要求Acceptor做出如下回应:

  • 承诺不再批准任何编号小于Mn的提案。
  • 已经批准过提案,就向 Proposer 反馈当前批准的,编号小于Mn,但为最大编号的那个提案的值。

如果Proposer收到了半数以上的Acceptor的响应,那么它就可以产生[Mn ,Vn]提案。如果这些Acceptor批准过其他提案,这里的Vn是所有响应中编号最大的提案的Value值。如果这些Acceptor都没有批准过任何提案,即响应中不包含任何的提案,那么此时Mn对应Value值的可以由Proposer自行决定。

在确定提案之后,Proposer就会将该提案再次发送给某个Acceptor集合,要他们批准提案。我们称此请求为Accept请求。

一个Acceptor可能会收到来自Proposer的两种请求,分别是Prepare请求和Accept请求,对这两类请求做出响应的条件分别如下。

  • Prepare请求:Acceptor可以在任何时候响应一个Prepare请求。
  • Accept请求:在不违背Accept现有承诺的前提下,可以任意响应Accept请求。

因为Acceptor要接受两种请求可能导致混乱,所以我们对Acceptor更新约束规则:

P1a:从上面这个约束条件中,我们可以看出,P1a 包含了 P1。回顾一下P1:一个acceptor必须接受他收到的第一个提案。

2.2.2 获取一个提案

Paxos算法推导详解 | Basic Paxos_paxos_06

方案1最简单的做法就是一旦Acceptor批准了一个提案,就将该提案发送给所有的Learner。这种做法比较方便快速,但是需要让每个Acceptor与所有的Learner逐个进行一次通信,通信的次数至少为二者个数的乘积。

Paxos算法推导详解 | Basic Paxos_数据库_07

方案2:我们可以让所有的Acceptor将结果统一发送给一个特定的 Learner(称为“主Learner”),再由他通知其他的Learner。

较方案一而言,方案二虽然需要多一个步骤才能将提案通知到所有的 Learner,但其通信次数却大大减少了,只是 Acceptor 和Learner 的个数总和。但同时,该方案引入了一个新的不稳定因素:主 Learner随时可能出现故障。

Paxos算法推导详解 | Basic Paxos_数据库_08

方案3:Acceptor 可以将批准的提案发送给一个特定的 Learner集合,该集合中的每个 Learner 都可以在一个提案被选定后通知所有其他的Learner。

方案三主要是对方案二主 Learner单点问题改进,这个 Learner集合中的 Learner个数越多,可靠性就越好,但同时网络通信的复杂度也就越高。

2.3算法描述

将Paxos分为三大阶段如下:

  • Prepare阶段:Proposer向Acceptor发出prepare请求,Acceptor针对收到的Prepare请求进行promise承诺。
  • Accept阶段:Proposer收到多数Acceptor响应之后,向Acceptor发出accept请求,Acceptor在不违背之前promise的情况下处理提案。
  • Learn阶段:多数Acceptor批准提案之后,标志着本次决策成功,将形成的提案发送给所有Learner。

补充:因为前两阶段进行决策,Learn阶段不参与决策,因此我个人认为还可以划分为:

  • 决策阶段:
  • Prepare阶段:生成提案
  • Accept阶段:批准提案
  • 获取决策阶段:
  • Learn阶段:获取提案

3 Basic Paxos的衍生

3.1 Basic Paxos活锁

前文论述只是就算法的安全性角度进行了考虑,现在就活性进行讨论。

下图的情况:Proposer 1提出了一个编号为1的提案,并完成了prepare过程。但是与此同时,Proposer 2提出了一个编号为2的提案,同样也完成了prepare流程。Acceptor已经承诺不再批准编号小于2的提案了,所以提案1作废。因此当Proposer 1准备提交提案1 的accept请求的时候,发现自己提案作废了,于是Proposer 1又提出了一个编号为 3的提案,而这又导致提案2的Accept请求被忽略。一直持续下去,提案的选定过程将陷入死循环,又称活锁。

Paxos算法推导详解 | Basic Paxos_数据库_09

3.2 Paxos衍生

为了保证 Paxos 算法流程的可持续性,避免陷入上述提到的“死循环”,可以选择一个主Proposer,我们称之为Leader,并规定只有Leader才能提出议案。如果系统中有足够多的组件能够正常工作,那么通过选择一个Leader,整套Paxos算法流程就能够保持活性,就是Multi-Paxos。但是Multi-Paxos允许有多个自认为Leader的节点存在,这样又会退化为Basic Paxos

Zookeeper使用的ZAB,还有之后的Raft算法都是Multi-Paxos的变体。它们三者与basic Paxos的区别是选出具有更长生命周期的Leader进行决策。

3.3 Chubby

Google Chubby是一个分布式锁服务,GFS和Big Table等大型系统都用它来解决分布式协作、元数据存储和 Master 选举等一系列与分布式锁服务相关的问题。Chubby的底层一致性实现就是以嗯。Muti-Paxos算法为基础的。

Chubby 提供了粗粒度的分布式锁服务,开发人员不需要使用复杂的同步协议,而是直接调用 Chubby 的锁服务接口即可实现分布式系统中多个进程之间粗粒度的同步控制,从而保证分布式数据的一致性。

Chubby最为典型的应用是集群中服务器的 Master 选举。例如在Google文件系统(Google File System,GFS)中使用Chubby锁服务来实现对GFS Master服务器的选举。而在BigTable中,Chubby同样被用于Master选举,并且借助Chubby,Master 能够非常方便地感知到其所控制的那些服务器。

Paxos算法推导详解 | Basic Paxos_数据库_10

一个典型的Chubby集群,我们称之为 Chubby cell,一般是用5台服务器组成的。这些副本服务器采用Muti-Paxos协议,通过投票的方式来选举产生一个获得过半投票的服务器作为Master。一旦某台服务器成为了 Master,Chubby 就会保证在一段时期内不会再有其他服务器成为Master,这段时期被称为Master租期(Master lease)。在运行过程中,Master服务器会通过不断续租的方式来延长 Master 租期,而如果 Master 服务器出现故障,那么余下的服务器就会进行新一轮的 Master 选举,最终产生新的 Master 服务器,开始新的Master租期。这可以理解为Basic Paxos在每次决策的时候都需要进行一次选举,但是在Muti-Paxos中可以进行一次选举,选出一个长老之后一直由他主持议会,直到他生病或者任期结束才进行下一次选举。

集群中的每个服务器都维护着一份服务端数据库的副本,但在实际运行过程中,只有Master 服务器才能对数据库进行写操作,而其他服务器都是使用 Paxos 协议从 Master服务器上同步数据库数据的更新。

Paxos算法推导详解 | Basic Paxos_paxos_11

Chubby的客户端与Master 服务器的通信过程:Chubby客户端通过向记录有Chubby服务端机器列表的DNS来请求获取所有的Chubby服务器名单。依据名单逐个发起请求询问服务器是否是Master。如果被询问的服务器是Master就会继续处理服务器的请求;如果被询问的服务器不是Master的服务器,则会将当前Master所在的服务器是哪个回复给给客户端,这样做还能帮助服务器快速获取Master服务器位置。

一旦客户端定寻找到到Master服务器之后,只要该 Master服务器正常运行,那么客户端就会将所有的请求都发送到该Master服务器上。

  • 针对写请求,Chubby Master会采用一致性协议将其广播给集群中所有的副本服务器,并且在过半的服务器接受了该写请求之后,再响应给客户端正确的应答。
  • 而对于读请求,则不需要在集群内部进行广播处理,直接由Master服务器单独处理即可。

在Chubby运行过程中,服务器难免会发生故障。如果当前的Master服务器崩溃了,那么集群中的其他服务器会在Master租期到期后,重新开启新一轮的Master选举。通常,进行一次 Master 选举大概需要花费几秒钟的时间。而如果是集群中任意一台非 Master服务器崩溃,那么整个集群是不会停止工作的,这个崩溃的服务器会在恢复之后自动加入到 Chubby 集群中去。新加入的服务器首先需要同步 Chubby 最新的数据库数据,完成数据同步之后,新的服务器就可以加入到正常的运作流程中与其他服务器副本一起协同工作。

如果集群中的一个服务器发生崩溃并在几小时后仍无法恢复正常,那么就需要加入新的机器,并同时更新DNS列表。Chubby服务器的更换方式非常简单,只需要启动Chubby服务端程序,用新机器的 IP 地址替换老机器的 IP地址,然后更新 DNS 上的机器列表即可。在Chubby运行过程中,Master服务器会周期性地轮询DNS列表。因此某一个服务器地址发生变动的时候当前Master可以很快获取到其变化。然后Master就会将集群数据库中的地址列表做相应的变更,集群内部的其他副本服务器通过复制方式就可以获取到最新的服务器地址列表了。