本篇文章以 John Ousterhout(斯坦福大学教授) 和 Diego Ongaro(斯坦福大学得到博士学位,Raft算法发明人) 在 Youtube 上的讲解视频及 ppt 为蓝本,深刻分析 Paxos 的内部机制,并以日志复制同步(Replicated Logs)为背景,详细介绍使用 Paxos 协议实现日志复制同步。php
Paxos 是在十九世纪80年代末由 Leslie Lamport 发明的,从那开始 Paxos 几乎就成为了分布式系统共识性的同义词。当大学教授分布式系统共识性的时候,几乎老是使用 Paxos 做为算法。Paxos 或许是在全部分布式系统算法中惟一重要的算法。web
下面会以日志复制同步为背景介绍 Paxos,并用日志拷贝建立副本状态机(replicated state machine),当说到状态机时,这里指的是一个接收一些输入和生成一些输出,并保留一些内部状态的程序或应用,能够将几乎全部的程序都当作一个状态机。这里的想法是让一个状态机高度可靠,能够经过在多个不一样的服务器上并行地运行多个状态机来达到此目的。若是每一个状态机都以相同的顺序接收到一样的命令集合,那么它们应该表现出一致的行为并输出相同的结果。因此在理想状态下,若是有些状态机宕掉了,其余的还能继续提供服务。算法
实现日志复制同步的目的是让状态机以相同的顺序来处理命令,首先将命令存入日志并保证全部的日志具备相同顺序的相同命令。安全
系统是这样运行的:服务器
若是一个客户端想要执行状态机里的一条命令,它先会将命令传给其中一个服务器,假设这里的命令是 X ,服务器会记录这条命令,并将它传递给其余的服务器,其余的服务器都会各自记录这条命令,一旦命令在全部的日志中都保存有副本,那么它就能够传递给状态机供执行,咱们有时会使用词语 “应用(apply)” 表明命令真实执行的状况,一旦其中一台状态机执行了命令,那么它的结果就会被返回到客户端,能够发如今状态机上只要日志是相同的,处理日志中命令的顺序也是一致的,那么全部状态机所表现的行为也是同样的,这也就是共识模块要保证的 — 日志的 副本是正确的。这也就是使用 Paxos 协议的目的。网络
一个共识模块最关键的特性是:对于一个系统来讲,只要有大多数的服务器是可用的,那么它就能够提供全部的服务。因此若是咱们有一个 5 台服务器的集群,那么它能够在仅有 3 台服务器可用的状况下,仍然能正常提供服务。因此咱们能够容忍 5 台其中的 2 台宕掉。一般状况下,集群的大小会是一个奇数,如 三、5 或 7 。app
Paxos 的失败模型是一个中止失败的模型,也就是说服务器会宕掉,或者它们会停掉和重启,不过一旦它们运行,它们的行为老是正确的,它们不会有很差的行为(即拜占庭将军问题)。咱们也会假设网络会丢失消息,或者会存在消息延迟,也就是说可能会存在到达顺序会和发送顺序不同的状况。当网络发生短暂隔断时,隔断也能够被修复,并让通讯能够再次容许通讯。当消息传递时只要系统在工做,就能保证正确。分布式
要分解这个问题有多种方式,首先以最简单让人容易想象的共识问题开始 — 基础 Paxos(Basic Paxos) 或 单度 Paxos。在这个问题下,咱们有一组服务器,其中有些服务器会提议(propose)特定的值。基础 Paxos 的目的是挑选这些值中惟一的一个,这个被选中的值称为(chosen)。全部系统作的就是一次只挑选一个惟一值,它不会挑选第二个值,也不会更改它的选择。这是咱们能够想象获得的最简单的共识性算法。当人们用到短语共识性算法的时候,一般就是指的这种最简单的模式。一般咱们谈论的 Paxos 也是这个简单版本的 Paxos 。一旦咱们有这种很是简单的方式来选择值,咱们能够建立日志,为多条日志记录建立多个实例,这就是 多 Paxos(Multi-Paxos) 。咱们会先解释 基础 Paxos ,而后介绍如何根据它来构建 多 Paxos3d
在介绍 基础 Paxos 以前,咱们先来了解一下需求。整体上讲有两个需求,针对于算法的安全性(Safety)和可用性(Liveness)。安全性(Safety)从整体上讲,指的是算法在任什么时候候都不能作很差的事情,在 基础 Paxos 的语境下,也就是说最多只能选择单个值,不能够选择第二个值取代第一个值。安全性的第二点是说,若是服务器认为一个值被选中,那么它必须真的被服务器继续选择了。可用性(Liveness)指的是咱们但愿系统最终能作对的事情,仅仅不作很差的事情是不够的。可用性有两个属性,第一个是最终必定会选择某个提议的值,第二点是服务器最终会知道值已经被选中。这个可用性的前提是大多数的服务器是活着的并能进行合理的通信。日志
基础 Paxos 有两个主要的组件, 提议者(Proposers) 和 接受者(Acceptors) , 提议者(Proposers) 是活动元素,它们会主动作一些事情,一般它们会接收来自客户端的请求,得到特定的选定值,而后它会传递这个值,并让集群里的其余服务器也达成一致选择这个值。 接受者(Acceptors) 是被动元素,它们简单地接收来自于 提议者(Proposers) 的请求并作出响应,能够把这种响应当成 “投票” , 提议者(Proposers) 会尝试得到 接受者(Acceptors) 所投的多数票, 接受者(Acceptors) 会存储多个状态,好比可能被选定或未被选定的值,以及响应的 “投票” 结果。最终它仍是会想要知道具体被选定的值是哪一个。不过正如咱们所见,开始的时候,只有 提议者(Proposers) 知道这个值,可是最终 接受者(Acceptors) 仍是须要知道这个值,这样才能将它传递给状态机。在 Lamport 对于这个问题定义的传统公式下,还定了另一个元素,称为 监听者(Listeners) 。这些元素想要知道被选定的值,在这个例子中, 监听者(Listeners) 是处于 接受者(Acceptors) 内的。不只如此,在咱们介绍的这个例子中,每一个服务器都包含一个 提议者(Proposers) 和 接受者(Acceptors) 。想要经过独立的 提议者(Proposers) 和 接受者(Acceptors) 来构建 Paxos 协议也是可能的,但对于这里的例子,咱们作以上的前提假设。
接下来会介绍一些咱们想要实现共识性所须要解决的问题。比方说这里有一个例子,但不幸的是它是不正确的。假设咱们只选择了一个 接受者(Acceptors) 并让这个 接受者(Acceptors) 选择全部的值,因此在这种状况下,每一个 提议者(Proposers) 都会给 接受者(Acceptors) 发送它的值, 接受者(Acceptors) 会挑选其中的一个,而后将其做为选定值。尽管这中实现方式很简单,可是没法解决 接受者(Acceptors) 可能会崩溃问题,若是 接受者(Acceptors) 在选择以后立刻就崩溃了,咱们就没法知道选定的值是什么,这样就必须进行重启。记住算法的目的是只要大多数节点是可用的,系统就必须能彻底正常工做。
这个办法行不通。为了能处理节点失败的状况,咱们就必须使用某些仲裁(quorum)的方法,咱们须要有一组 接受者(Acceptors) ,一般是一个奇数,如 3 、5 或 7 。若是一个值被大多数 接受者(Acceptors) 选定,那么咱们认为这个值被认为是选定的。这样即便在少数服务器崩溃的状况下,还有多数服务器存留能够接受值。甚至在接受后崩溃的状况下,仍是能够肯定被选定的值。仲裁(quorum)方法可让咱们在某些服务器崩溃后,仍然能保证集群能正常工做。
不过让仲裁(quorum)能正常工做须要注意一些问题。
例如,咱们假设每一个 接受者(Acceptors) 都接收它第一次收到的值,而后让多数票的值获胜,如上图咱们能够看到,也存在没有任何值是大多数的状况。服务器 S1 和 S2 接受的值是 red ,服务器 S3 和 S4 接受的值是 blue ,服务器 S5 接受的值是 green 。没有任何值在五个服务器中的三个达成一致的。这也就意味着 接受者(Acceptors) 有时须要改变他们的想法,在某些状况下,它们接受了一个值后须要接受另一个不一样的值。也就是说,几乎没法在一轮投票下就能达成一致,每每须要进行几轮的投票才能达到一致。这里接受(accepted)并不表明被选定(chosen),一个值只有在集群大多数节点接受以后才被认为是选定的。
如今来看另一个问题,当 接受者(Acceptors) 在更混乱的状况下,它们会接受任何值,但这会带来两个问题,我会在本页和下页中分别讨论这两个问题。
第一个问题是咱们可能会最终选择多个值。
例如,服务器 S1 会提议一个值 red ,让其余的服务器接受,这样服务器 S一、S二、S3 接受了这个值,那么如今被选定的值是 red ,由于它已经被多数服务器(3/5)选择。不过随后服务器 S5 来了,提议了一个不一样的值 blue ,而后让 接受者(Acceptors) 接受这个值,由于它们会接受任何传递给它们的值,那么此时服务器 S3 接受的值是 blue ,尽管它以前接受的值是 red 。因此如今咱们选择的值是 blue 。这样就违背了咱们所定义的基础的安全属性的要求 — 咱们只能选定一个值。
这个问题的解决办法是,若是第二个 提议者(Proposers) 有新的提议,这里是服务器 S5 ,此时若是已经有选定的值,那么它就必须放弃它本身的值,并提议当前已经选定的值,因此在这种规约下,在服务器 S5 向其余服务器发出请求要求接受它的值以前,它就须要查看集群里的其余服务器,看是否有其余值的存在,若是已经有其余选定的值,服务器 S5 就须要放弃它本身的值,而后使用 red 来代替,这样最终就可使 red 成为选定的值,咱们以第二次选择为终结点,可是最终选择的值是第一次选定的那个值(red)。这也就说,咱们须要使用一个两段协议(two-phase protocol)。
不幸的是,只有两段协议(two-phase protocol)自己是不够的。上图说明了这个问题。
例如,服务器 S1 提议了一个值 red 。它首先检查其余的服务器,其余的服务器尚未接受任何值,因此它开始向其余服务器发起请求,但愿它们能接受本身的值 red 。不过同时,在其余服务器真正应答以前,另一个服务器 S5 又提议了另外一个值 blue 。这时它也发现尚未其余服务器肯定了选定值,那么它就开始发送消息,但愿其余服务器能选择 blue 。而后,若是这个请求先结束,服务器 S三、S四、S5 接受并选定 blue ,但与此同时,red 值的服务器仍然处于运行中,由于 接受者(Acceptors) 会接受多个值,因此最终能够看到,会发生仲裁,最终 red 值会被选定,这样就违背了基本安全的属性要求。
这个问题的解决办法是,一旦咱们已经选定了值,任何其余的竞争性提议必须被放弃。在上面的例子中,咱们就须要服务器 S3 在已经接受了值 blue 后,拒绝对 red 值的接受请求。要想这么作,咱们会给提议安排顺序,新的提议优先于全部提议。也就是说 blue 的请求更晚,它会截断 red 请求,这样请求就不会以选择竞争值为结束。
因此总结以下:咱们须要一个两段协议(two-phase protocol)。在发起请求前先进行检查,而后咱们须要请求有序,这样就能消除老的请求。
接下来咱们看看如何使请求有序。
采用的方式是为每一个请求分配一个惟一的请求序号,使用一个从未被以前请求使用的序号,也就是高的序号要比低序号有更高的优先级,因此当服务器开始执行这个协议, 提议者(Proposers) 必须选择一个新的请求序号,它必须是惟一的,并且比其余序号更高。一种实现方式是将两个值进行拼接,以服务器 ID 为开始,为每一个服务器分配一个惟一值,并将其置于请求序号(proposal number)的低位,其余的服务器都不会生成这个请求。在请求序号高位,咱们放置一个自增循环数,在集群下全部服务内共享,最近的序号要比以前生成的要高。要想这么作,全部服务器都必须保存这个最大值,并根据它生成每一个序号,这个值被称为 maxRound 。这个值必须被持久化到磁盘或其余稳定的媒介,以确保能够在系统崩溃后可以恢复,这样就能保证在系统崩溃恢复之后,请求序号不会重复。
本节会对基础 Paxos 作一个小结,详细的内容会在后续介绍
在以前提到过,咱们须要用一个两段协议(two-phase approach),在第一阶段, 提议者(Proposers) 会尝试让一个值被选定,它会向全部服务器发送 RPC 请求,咱们将这个请求称为 准备(Prepare)。这个请求有两个目的:首先,它会尝试找到其余可能被选定的值,这样就能够保证使用被选定的值,而不是本身的值;其次,若是有其余存在的但未被选中的请求值,它会阻止老的请求,这样就能够避免发生竞争。在第二阶段,会发起另一个 RPC 请求以及一个选择好的值,若是这个阶段大多数都达成一致,咱们就认为这个值已经被选定。
上图显示了完整的基础 Paxos 协议,让咱们来看看一个完整的请求过程,正如以前所提到的,整个过程是由 提议者(Proposers) 驱动的。 提议者(Proposers) 会由某个它但愿选定的值为开始,而后会经历两轮广播消息:准备阶段(prepare phase)和接受阶段(accept phase)。可是首先
(1)须要选择一个提议序号,缘由在前面已经解释过,须要提供一个惟一的,以前没有使用过的数。而后就会进入准备阶段(prepare phase),在这个过程当中,
(2) 提议者(Proposers) 会向集群的全部 接受者(Acceptor) 发起远程过程调用(remote procedure call),每条消息都包含提议序号(Prepare(n)),
(3)当 接受者(Acceptor) 收到 Prepare(n) 请求,它会作两件事情:首先,它须要保证永远不会接受一个提议序号更低的请求,能够经过保存内部变量(minProposal)的方式来达到目的,随着时间的推移,这个值会自动增加,若是当前的请求就是具备最高序号的提议,那么就会更新当前的 minProposal;第二步,就是须要响应并返回接受后的值,若是它以前已接受了某个提议,它会同时记录接受的值及其序号,并返回它当前接受的、具备最高序号的提议值。
(4) 提议者(Proposers) 会等待大多数 接受者(Acceptor) 的响应,一旦它接收到响应,它会检查看是否有返回,以及接受的提议值是什么,这样它会从全部的响应中挑选最高的提议序号,并用这个具备最高提议序号的值做为接受值,以替代它所提议的初始值,并用这个值继续后面的计算。若是没有 接受者(Acceptor) 返回已接受的提议值,那么它会使用本身的初始值继续后面的计算。这里就完成了协议的第一阶段。
(5)这里 提议者(Proposers) 会发送接受请求(Accept(n, value))到集群的全部服务器,包括一个提议序号 n ,这个值必须与准备阶段的序号值保持一致,以及一个值。这个值能够是 提议者(Proposers) 所提议的初始值,也能够是从 接受者(Acceptor) 返回的接受值。这条消息会广播到集群的全部服务器。
(6)当集群服务器接收到这条消息后,它会将接受请求的提议序号与本身保存的提议序号做比较,若是接受到的提议序号没有保存的序号高,那么就会拒绝这个接受请求,但若是更高,那么就会接受这个提议,并记下这个接受请求的提议序号,以及它的值,并更新当前的提议序号,保证它是最大的。不管接受仍是拒绝这个请求, 接受者(Acceptor) 都会返回它当前的提议值。这样 提议者(Proposers) 就可使用这个返回值来判断请求是否被接受了。
(7) 提议者(Proposers) 会等待直到它接收到多数响应。一旦收到这些响应,它就会检查是否有请求被拒绝,它能够经过返回值和提议序号来进行比较,若是请求被拒绝了,那么此次提议须要回到第(1)步,从新开始。幸运的是它能够经过提议序号判断,那么下一轮就能够选取一个更高的提议序号和值,这样就更有机会在竞争中取胜。大多数状况下,更好的状态是 接受者(Acceptor) 都接受了请求,此时提议值被选定,协议结束执行。
为了保证这个协议能工做正常,集群必须保证三个值的稳定,minProposal、acceptedProposal 以及 acceptedValue ,它们须要保存在稳定的媒介,如磁盘或闪存中。
接下来用一些例子来讲明提议竞争的状态,以及关键点在于第二次提议的准备阶段。
总共有三种可能。
咱们假设有两个提议,值分别是 X 和 Y 。一个客户端请求服务器 S1 让 X 被选定,另外一个客户端请求服务器 S5 让 Y 被选定,上图的小方格表示特定服务器上的特定请求,因此在 S3 上的小方格 P3.1 表示提议序号 3.1,高位的 3 表示顺序数,地位的 5 表示服务器号,因此 P3.1 来自于 S1 。一样的在 S4 上的 A4.5 X 表示服务器 S4 接受了来自服务器 S5 的请求,以及值 X 。在第二个提议请求来以前,第一个提议请求已经选定了值,也就是说值 X 已经被集群的大多数服务器所接受。由于第二个提议请求也须要大多数服务器获得响应,因此必定能够保证会至少有一个准备(Prepare)请求会到达与前一个请求相同的服务器,这里是 S3 。因此服务器 S5 会发现已经接受的值 X ,当它响应准备请求(Prepare)时,它会放弃 Y 值,并为在全部接受(Accept)请求时使用 X 值。因此在这种状况下,服务器 S5 会成功,而且选定值为 S1 提议的 X 。
后面两种状况的前提是前值没有被选定的状况下,第二次请求进入了准备阶段(Prepare Phase)
有可能前一次的提议正在处于接受值的过程当中,第二次提议刚好见到了其中的接受值。如上图所示,服务器 S3 已经接受了第一个提议(P3.1 - A3.1 X),第二个提议者正好看见了这个接受值 X ,由于第二个提议者的准备阶段处于前一个提议者接受阶段以后(A3.1 X - P4.5)。因此在这种状况下第二个提议会使用当前已有的值 X ,并放弃 Y 值,并使用来自于 S3 的 X 值做为选定值。
这与前一种状况同样。先后两次提议请求都成功,并且最终都接受选定了相同的值 X 。
当第二次提议看到 X 值的时候,它并不知道这个值是否真的被选定了,由于它只与大多数服务器发生了对话,因此也有可能 S一、S2 已经完成了 X 的接受,而服务器 S5 并不知道。因此,一旦第二次提议看到前序接受的 X 值,它就必需要假设这个值已经被接受选定,并用这个值做为它本身的提议值。
第三个场景是在第二次提议的准备阶段到来以前,接受值尚未肯定,并对第二次提议的准备请求不可见的状况。它可能在别的某个服务器上被接受(S1),可是在第二次提议(S5)的检查范围内,没有前序的值被接受。在这种状况下,服务器 S5 会使用它本身的值 Y 。最终选定的值也是 Y 。这里比较幸运的是 S5 成功阻止了 S1 ,S1 的提议至少会在一台服务器(这里是 S3)上与 S5 的提议相竞争,因为 P4.5 有更高的提议序号值,因此这里 S3 会拒绝接受 X 请求,因此这也会阻止 S1 接受 X ,S1 发起的提议须要从新开始,当再次开始时,会新发起一轮提议,这时它会至少在一台服务器上发现来自 S5 的提议 Y ,因此 S1 发起的提议最终会以 S5 的提议值 Y 做为它的接受值。也就是说,最终在集群内达成了一致,选定值 Y 。
这里的关键点在于这些竞争的提议必须在至少一台服务器上有重叠,这样能够经过它们与服务器通讯的顺序来决定最终的结果。要么在前序提议值对后续请求可见的状况下,选定前序接受的值,要么在前序提议值对后续请求者不可见的状况下,会选定后续提议的接受值。任何一种方式都是安全的。
至此,足以说明 Paxos 协议在竞争状态下是安全的,不管如何竞争,最终都会选定某一值并达成一致。可是,这并不能说明基础 Paxos 协议是可用的(Live),可能会发生一组提议相互阻碍的状况,最终不会有任何选定值。下面会对此进行说明。
假设服务器 S1 成功接收到请求,并处于准备阶段(P 3.1)。在接受值 X 以前(A 3.1 X),另一个服务器 S5 正处于它的准备阶段(P 3.5),这会阻止前序值的接受(A 3.1 X)。而后 S1 会从新选择提议序号并再次开始提议过程(P 4.1),假设它正进入了第二轮的准备阶段,在接受值以前,服务器 S5 正试图完成接受值的选定 Y (A 3.5 Y),不过此时由于(P 4.1)的序号高于(A 3.5 Y),因此它阻止了(A 3.5 Y)的接受,这样 S5 的提议就失败了,而后 S5 又从新开始下一轮的提议,如此往复,这个过程会无限循环下去。
为了避免发生死锁,Paxos 须要以某种补充机制来保证它能够正确运行。一个简单的方式是让服务器等待一会,若是发生接受失败的状况,必须返回从新开始。在从新开始以前等待一会,让提议能有机会完成。可让集群下服务器随机的延迟,从而避免全部服务器都处于相同的等待时间下。在多 Paxos 协议(Multi-Paxos)下,会有些不一样,咱们会介绍另一种被称为领导人选举(leader election)的机制。保证在同一时间下只有一个 提议者(Proposers) 在工做。
基础 Paxos 协议也有它的缺陷。一旦值被选定以后,只有一台服务器(即发起提议的那台服务器)知道它选定的值是什么。 接受者(Acceptor) 没法知道它保存的值是否以及被选中。若是其余的服务器想要知道被选定的值,它就必须本身执行协议。
参考来源:
2013 Paxos lecture, Diego Ongaro
Wiki: Byzantine fault tolerance