《Paxos Made Simple》译文

原文链接:https://www.microsoft.com/en-us/research/uploads/prod/2016/12/paxos-simple-Copy.pdf

                                        让Paxos更简单

                                                                  Leslie Lamport,2001年11月3日

摘要

当采用简单直接的方式描述,Paxos算法是非常简单的。

目录

  1. 简介
  2. 共识算法
    1. 问题
    2. 选中一个值
    3. 学习选中的值
    4. 进展
    5. 实现
  3. 实现状态机

     参考文献

 

 

1. 简介

Paxos算法可用于实现容错分布式系统,或许是因为之前以希腊神话故事的形式描述Paxos算法[5],对许多读者来说,Paxos算法一直很难理解。事实上,Paxos算法在分布式算法中,属于较为简单和易理解的一类。Paxos算法的核心是一个共识算法——参考文献[5]中的“会议”(synod)算法。在下一节,我们将展示,如何从我们期望共识算法所拥有的属性派生出这个共识算法(“会议”算法)。最后一节解释了完整的Paxos算法,将共识直接应用到构建分布式系统的状态机方法[4],即可得到Paxos算法。状态机方法是众所周知的一种方法,它可能是分布式系统理论中最常被引用的一种方法。

2. 共识算法

2.1 问题

假设有一组进程可以提出值(propose values)。共识算法可以确保所有被提出的值中,只有一个值能被选中(chosen)。如果没有值被提出,那么没有值会被选中。如果一个值被选中了,那么进程应该可以学习(learn)这个被选中的值。

共识算法的安全性(safety)要求如下:

  1. 只有被提出的值才可以被选中
  2. 只有一个值能被选中
  3. 除非值被选中了,否则进程不会知道它被选中了,即:进程只学习已经被选中的值

我们不会明确指定灵活性(liveness)要求。然而,灵活性的目标是确保某个被提出的值最终可以被选中,并且如果一个值被选中,进程最终可以学习该值。

在共识算法中,存在三种角色,由三类代理(agent)执行这些角色:提出者(proposer),接受者(acceptor)以及学习者(learner)。在实现中,单个进程可以充当多个代理。这里,代理到进程的映射不会对我们产生影响。

假定代理们通过发送消息彼此交流。我们采用惯用的异步非拜占庭模型,其中:

  1. 代理以任意速度运行,可能会故障(故障表现为停止运行),可能会重启。既然所有的代理可能在某个值被选中后故障,并且随后重启,那么除非信息能被故障后重启的代理记住,否则共识算法是不可能实现的。
  2. 消息传送所需的时间是不确定的,消息可以重复发送,可以丢失,但是不会损坏。

2.2 选中一个值

选中一个值最简单的方法是只有单个接受者代理。提出者发送一个提议(proposal)给接受者,接受者选中它收到的第一个被提出的值即可。尽管很简单,但这种解决方法却不那么令人满意。因为在单个接受者故障之后,将不会取得任何进展(progress)。

所以,我们尝试另一种方法来选中单个值。采用多个接受者代理。提出者发送提出的值给某个接受者集合。一个接受者可能会接受(accept)这个提出的值。当一个足够大的接受者集合接受了这个值,这个值就被选中了。那么多大才算足够大呢?为了确保只有单个值被选中,足够大的集合包括了任意大多数代理(译者注:假设有5个代理,任意大多数代理表示3个代理:5除以2的下界再加1)。因为任意两个大多数代理至少有一个共同的接受者。这种方法奏效的条件是一个接受者最多只能接受一个值。(从参考文献[3]开始,许多论文都观察到了“大多数”)。

在没有故障和消息丢失的情况下,即使只有单个提出者提出一个值,我们依然想要值被选中。这就需要:

P1. 接受者必须接受它收到的第一个提议。

但这个要求带来一个问题。几个值可能同时被不同的提出者提出,导致每个接受者接受了一个值,但是不存在被大多数接受者接受的单个值。即使只有两个被提出的值,如果每个已经被半数接受者接受,一个接受者故障的情况也可能会导致不知道哪个值被选中。

P1以及只有当一个值被大多数接受者接受才被选中这一条件说明,一个接受者一定可以接受多个提出的值。我们通过为每个提出的值分配一个编号来追踪接受者接受的不同的值,所以一个提议(proposal)包含一个提议号和一个值。为了防止混淆,不同的提议需要有不同的提议号。提议号的确定可以在实现过程中决定,现在我们假设有了提议号。包含单个值的单个提议被大多数接受者接受,就可以说这个值被选中。这种情况下,我们说这个提议(以及其中的值)被选中。

可以允许多个提议被选中,但是一定要确保所有被选中的提议有相同的值。通过对提议号进行归纳,发现这需要保证:

P2. 如果值为v的提议被选中,那么每一个被选中的更大提议号的提议的值均为v。

既然提议号是全序的,条件P2确保了关键的安全性——只有一个值被选中。

至少具有一个接受者接受提议,提议才能被选中。所以,满足下列条件即可满足P2:

P2a. 如果值为v的提议被选中,那么接受者接受的每一个更大提议号的提议的值均为v。

我们仍然需要满足P1来确保存在提议被选中。因为通信是异步的,可能在某个特定的接受者c从未收到任何提议的情况下,有一个提议被选中了。假定一个新的提出者提出了一个更大提议号的提议,但是值和选中的提议的值不同。遵循P1,c会接受这个提议,但会违反P2a。P1和P2同时满足需要对P2a进一步改进:

P2b. 如果值为v的提议被选中了,那么任何提出者提出的更高提议号的提议的值均为v。

既然在接受者接受提议之前,一定存在某个提出者提出了该提议,那么满足P2b就满足P2a,满足P2a就满足P2。

为了研究如何满足P2b,考虑如何才能证明P2b被满足。我们假设提议号为m,值为v的提议被选中了,接下来说明提议号(n)大于m的任何提议的值均为v。可以通过在n上使用归纳法来简化证明,所以可以通过证明:假设提议号在m…(n-1)中的提议的值为v,那么提议号为n的提议的值为v(其中,i…j表示从i到j的数值集合),来证明P2b被满足。若提议号为m的提议被选中,那么一定有包括大多数接受者的某个集合C,C中每个接受者都接受了该提议。将这和归纳假设相结合,那么m被选中的假设意味着:

C中每个接受者均接受了一个提议,其提议号在m…(n-1)中;任何接受者接受的提议号在m…(n-1)中的每个提议都有值v。

既然包含大多数接受者的任何集合S至少会包括C中的一个成员,因此通过确保满足下列条件,来保证提议号为n的提议的值为v:

P2c:对于任何v和n,如果提议号为n,值为v的提议被提出,那么有一个包含大多数接受者的集合S,S满足下列条件之一:

(a)S中没有接受者接受过提议号小于n的提议

(b)v是S中接受者接受的提议号比n小的提议中,具有最高提议号的提议的值。

因此,我们可以通过保证P2c,来满足P2b。

为了确保P2c,如果提出者想要提出一个提议号为n的提议,那他一定要知道提议号小于n的提议中,具有最大提议号的提议(如果有的话),这个提议已经被或将要被某个大多数接受者集合中的每个接受者接受。想要知道已经被接受的提议是很简单的;但是预测将要被接受的提议是很困难的。提出者不尝试预测未来,而控制不会有这样将要被接受的提议。换句话说,提出者请求接受者不再接受提议号小于n的任何提议。故而产生下列提出提议的算法:

1. 提出者选择一个新的提议号n,并给某个接受者集合中的每个接受者发送一个请求,要求其回复:

   承诺永远不再接受提议号小于n的提议,并且

   回复已经接受的提议号小于n的,最大提议号的提议(如果有的话)。

   将这样的请求称为编号为n的“准备”(prepare)请求。

2. 如果提出者从大多数接受者处收到“准备”请求的回复,那么它可以提出一个提议号为n,值为v的提议,其中,v或者是收到的回复中具有最大提议号的提议的值,或者是提出者选中的任何值(如果接受者没有回复提议)

之后,提出者通过发送请求给某个接受者集合来提出提议,其中请求的内容是请求接受者接受该提议(不需要是和响应“准备”请求一样的接受者集合)。我们称之为“接受”请求。

提出者的算法就此诞生。那接受者呢?接受者可能从提出者处收到两种请求:“准备”请求和“接受”请求。接受者可以在不影响安全性(safety)的情况下忽略任何请求。所以,我们只需说明何时允许接受者响应请求。接受者始终可以响应“准备”请求。只有当接受者没有承诺不接受“接受”请求的时候,它才可以响应“接受”请求,接受提议。也就是说:

P1a. 当且仅当接受者没有响应提议号比n更大的提议的“准备”请求时,它才可以接受提议号为n的提议。

观察到,P1a包含P1。

假定提议号是唯一的,我们现在有一个完整的算法可以选择满足安全性的一个值。最终的算法还需进行一些小优化。

假设接受者收到编号为n的“准备”请求,但是它已经响应了一个编号比n更大的“准备”请求,从而承诺不接受任何编号为n的新提议。既然它不会接受编号为n的提议的话,那么接受者就没有必要回复这个新收到的“准备”请求。所以,我们让接受者忽略这样的“准备”请求。同时,也会让接受者忽略已经接受的提议的“准备”请求。

根据这个优化,接受者需要记住已经接受的最大提议号的提议以及响应的最大编号的“准备”请求的编号。由于无论什么故障,必须保证P2c,所以即使接受者故障后重启,也一定要记住这个信息。提出者可以随时放弃提议并忘记所有提议——只要它永远不用相同的提议号提出不同值的提议。

结合提出者和接受者的行为,算法包含下列两个阶段:

阶段1 (a)提出者选择一个提议号n并发送附带n的“准备”请求给大多数接受者。

          (b)如果接受者收到附带n的“准备”请求,并且n大于它已经响应的任何“准备”请求,那么会回复请求,并承诺不会接受任何提议号比n小的提议,并且附带它已经接受的最大提议号的提议(如果有的话)。

阶段2 (a)如果提出者从大多数接受者处收到“准备”请求(附带n)的回复,那么会发送提议号为n,值为v的提议的“接受”请求给某个大多数接受者的集合。其中,v是收到的回复中提议号最大的提议的值,如果回复中没有任何提议,那么v可以是任意值。

          (b)如果接受者收到提议号为n的提议的“接受”请求,除非已经响应了编号大于n的“准备”请求,否则它会接受这个提议。

提出者可以提出多个提议,只要每个提议遵循这个算法即可。提出者在算法执行过程中,可以放弃提议。(即使在提议被放弃很久之后,提议的请求和/或回复消息到达目的地,算法也可以保持正确性。)如果某个提议者已经开始尝试提出更高编号的提议,放弃提议可能是一个好主意。因此,如果一个接受者因为收到了更高编号的“准备”请求而忽视 “准备”请求或者“接受”请求,它应该通知提出者,提出者进而放弃自己的提议。这是一个不影响正确性的性能优化。

2.3 学习选中的值

要学习一个已经选中的值,学习者必须知道该提议已经被大多数接受者接受。明显的算法是让每个接受者在接受提议后,回复所有学习者,告诉他们这个提议。这让学习者可以尽快地发现一个选中的值,但是需要每个接受者回复每个学习者——回复的数量等于接受者数乘以学习者数。

非拜占庭故障的假设使得一个学习者从另一个学习者处知道值被接受是很容易的。可以让接受者回复他们的接受信息给一个特定的学习者。当一个值被选中的时候,该学习者通知其余学习者。这种方法需要一个额外的轮次来让所有学习者发现选中的值。也有点不可靠,因为特定的学习者可能会故障。但是它需要的回复数量只等于接受者数加上学习者数。

更一般地,接受者可以给一组特定的学习者发送他们的接受消息,其中,每个学习者可以在一个值被选中的时候通知所有的学习者。使用更多的特定学习者是以更高的通信复杂度为代价提供更高的可靠性。

因为消息会丢失,可能在没有学习者知道的情况下,一个值被选中了。学习者可以询问接受者他们接受了哪些提议,但是如果有一个接受者故障,学习者可能会不知道一个特定的提议是否被大多数接受者接受了。在这种情况下,只有当新提议被选中,学习者才会知道哪个值被选中了。如果学习者需要知道一个值是否被选中,它可以让提出者使用上述描述的算法提出一个提议。

2.4 进展(Progress

很容易构建一个场景,两个提出者一直不断地提出提议,提议号越来越大,没有一个提议被选中。提出者p为一个提议号n1完成了阶段1。另一个提出者q接着为提议号n2(n2>n1)完成了阶段1。提出者p为提议号n1的提议发出的阶段2的“接受”请求被忽视了,因为接受者承诺不去接受提议号小于n2的提议。所以,提出者p开始一个新的提议号n3(n3>n2)并完成了阶段1,导致提出者q的阶段2的“接受”请求被忽视了,以此类推。

为了确保进展,一定要选择一个特定的提出者作为唯一尝试去提出提议的人。如果特定的提出者可以和大多数接受者成功交流,并且使用的提议号比已经使用的任何提议号都大,那么它将可以成功提出一个被接受的提议。如果它知道有一个更大提议号的提议,通过放弃一个提议,再次尝试,这个特定的提出者最终可以选择一个提议号足够大的提议。

如果足够多的系统组件(提议者,接受者和通信网络)正常工作,则可以通过选择一个特定的提出者来实现灵活性(liveness)。Fischer,Lynch和Paterson提出的著名的结论[1]表明,选择一个提出者的可靠算法或者依赖随机性或者依赖实时性——例如,通过使用超时。但是,无论选举的成败与否,始终能确保安全性(safety)。

2.5 实现

Paxos算法[5]假设一个进程网络。在其共识算法中,每个进程都扮演着提出者,接受者和学习者的角色。算法选择一个领导者,充当特定提出者和特定学习者的角色。Paxos共识算法就是上面描述的算法,其中请求和响应作为普通消息发送。(响应消息标有相应的提议号以防止混淆。)在故障情况下依然具有的稳定存储用于存储接受者必须记住的信息。在发送回复之前,接受者需先将回复记录在稳定存储中。

剩下的就是描述确保任意两个提议的提议号都不同的机制。不同的提出者从不相交的数值集合中选择他们的提议号,所以两个不同的提出者不会提出相同提议号的提议。每个提出者记下(在稳定存储中)它提出的最大的提议号,会使用比用过的提议号更大的提议号来开始阶段1。

3. 实现状态机

实现分布式系统的一种简单的方法是作为客户端集合提出命令到一个中央服务器。可以将服务器描述为以某种顺序执行客户端命令的确定状态机。状态机有一个当前状态; 它的每一步是将一个命令作为输入,产生一个输出和一个新状态。例如:分布式银行系统的客户端可能是柜员,状态机的状态可能包括所有用户的账户余额。当且仅当余额大于提取的金额时,可以通过执行一个减少账户余额的状态机命令来进行取款操作,会产生一个新旧余额的输出。

如果服务器故障,那么使用单个中央服务器的系统就会故障。因此我们使用一群服务器,每个服务器独立地实现状态机。因为状态机是确定的,如果所有的服务器都执行相同的命令序列,那么他们将产生相同的状态序列和输出序列。提出命令的客户端就可以使用任何服务器产生的输出。

为了保证所有服务器执行相同的状态机命令序列,我们实现了一系列单独的Paxos共识算法实例(译者注:上述提出的Paxos算法可以确保单个值被选中,实际应用中可能需要选中多个值,这里实例对应于实际应用中需要多个值被选中的场景),第i个实例选中的值是命令序列中第i个状态机命令。每个服务器在Paxos算法的每个实例中扮演所有的角色(提出者,接受者和学习者)。现在,假定服务器集合是固定的,因此共识算法的所有实例使用相同的代理集。

正常操作中,选择单个服务器作为领导者,它在共识算法的所有实例中充当特定的提出者(唯一尝试提出提议的提出者)。客户端发送命令给领导者,领导者决定命令序列中,每个客户端命令应该出现的位置。如果领导者决定某个客户端命令应该是第135条命令,领导者会使该命令作为共识算法的第135条实例被选中。通常,领导者会成功。在故障的情况下,或者在另一个服务器也觉得自己是领导者并且关于第135条命令有不同的意见时,领导者可能会失败。但是共识算法确保第135条命令最多只有一个命令被选中。

这种方法的效率关键在于,在Paxos共识算法中,直到阶段2被提出的值才会被选中。回想一下,在提出者算法的阶段1完成后,要么确定要提出的值,要么提出者可以提出任何值。

接下来,我会描述Paxos状态机在正常操作期间是如何工作的。接着,我将讨论哪些部分可能会出现问题。我会考虑领导者发生故障,新领导者被选举时会发生什么。(系统启动是一种特殊情况,启动时尚未提出任何命令。)

作为共识算法所有实例的学习者,新领导者应该知道已经选中的大多数命令。假定它知道命令1-134,138以及139,也就是,它知道共识算法中实例1-134,138以及139中被选中的值(稍后我们会介绍为什么在命令序列中会出现这样的间断)。接着,领导者会执行实例135-137以及所有大于139的实例的阶段1(下面会描述如何执行)。假定这些执行的结果会确定实例135和140可以提出的值,其余实例可以提出的值不受约束。然后领导者会为实例135和140执行阶段2,从而选择命令135和140。

领导者,和其他已经知道领导者知道的所有命令的服务器一样,现在可以执行命令1-135。然而,不能执行命令138-140,因为命令136和137还没有被选中。领导者可以让接下来收到的两个客户端命令作为命令136和137。但是,我们让领导者立即提出一个特别的“空操作”(“no-op”)作为命令136和137,来填补这个间断,以使状态不被改变(对于实例136和137,领导者通过执行共识算法的阶段2来实现选中“空操作”)。一旦这些空操作命令被选中,命令138-140就可以被执行。(译者注:对于领导者为什么不使用收到的客户端命令,而使用“空操作”填补实例136和137,在参考文献[5]中有进行介绍,有兴趣的读者可以翻阅。)

命令1-140现在就被选中了。领导者已经完成了共识算法中所有大于140的实例的阶段1,可以在阶段2中自由地为这些实例提出任何值。它将命令141分配给接下来客户端提出的命令,在共识算法实例141的阶段2提出的值为该客户端命令。领导者将收到的下一个客户端命令作为命令142提出,依此类推。

领导者提出命令141后,可以在知道141被选中之前提出命令142。有可能提出命令141所发送的所有消息都丢失了,并且在其他服务器学习领导者提出的命令141的值之前,命令142被选中了。如果领导者没有收到实例141的阶段2的任何回复消息,他会重传这些消息。如果一切顺利,他提出的命令将会被选中。然而,也可能失败,在命令序列中大家就不知道命令141,留下间断。通常,假设领导者可以超前a个命令——也就是,在命令i被选中后,它可以提出命令i+1到i+a。可能会出现多达a-1个命令的间断。

新选中的领导者会为共识算法的所有实例(无穷多个)执行阶段1——在上述情形中,是实例135-137以及大于139的所有实例。对于所有实例,领导者使用相同的提议号,通过给其他服务器发送一条“准备”请求来完成阶段1。在阶段1,只有接受者之前已经接受某个提出者发送的阶段2 的消息,他才会不只是回复简单的OK消息(附带已经接受的提议,在上述情形中,是实例135和实例140的情况)。因此,一个服务器(接受者)可以用单个合理的短消息响应所有的实例。因此,阶段1执行无数个实例不会产生任何问题。

由于领导者故障和选举新领导者是极少发生的事件,执行状态机命令的有效成本——即在命令/值上达成共识——仅是执行共识算法阶段2的成本。可以证明,Paxos共识算法阶段2的成本是在故障条件下达成共识的所有算法中最小的[2]。因此,Paxos算法基本上是最优的。

在系统正常操作的讨论中,总是假定有一个领导者,除了当前领导者故障和新领导者选举这段时间。在异常情况下,领导者选举可能会失败。如果没有服务器作为领导者,那么将没有新命令会被提出。如果多个服务器认为他们是领导者,那么他们都可以在共识算法的同一个实例中提出值,这会阻止任何值被选中。然而,安全性(safety)依然可以保证——两个不同的服务器永远不会对第i个状态机命令选中的值产生不同意见。选举单个领导者只是为了确保进展(progress)。

如果服务器集合可以变更,那么必须有某种方法来确定哪些服务器实现了共识算法的实例。最简单的方法是通过状态机本身来确定。当前服务器集合可以是状态的一部分,并且可以通过普通的状态机命令实现变更。可以在第i个状态机命令中,决定执行共识算法第i+a个实例的服务器集合。这种方法简单地实现了任意复杂的重新配置算法。

 

参考文献:

[1] Michael J. Fischer, Nancy Lynch, and Michael S. Paterson. Impossibility of distributed consensus with one faulty process. Journal of the ACM, 32(2):374–382, April 1985.

[2] Idit Keidar and Sergio Rajsbaum. On the cost of fault-tolerant consensus whentherearenofaults—atutorial. TechnicalReportMIT-LCS-TR-821, Laboratory for Computer Science, Massachusetts Institute Technology, Cambridge, MA, 02139, May 2001. also published in SIGACT News 32(2) (June 2001).

[3] Leslie Lamport. The implementation of reliable distributed multiprocess systems. Computer Networks, 2:95–114, 1978.

[4] Leslie Lamport. Time, clocks, and the ordering of events in a distributed system. Communications of the ACM, 21(7):558–565, July 1978.

[5] Leslie Lamport. The part-time parliament. ACM Transactions on Computer Systems, 16(2):133–169, May 1998.

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章