How to Build a Highly Available System Using Consensus

                   如何使用一致性建立高可用性的系统



概要:鉴于在一致性算法中,副本能用于处理每次输入,lamport指出实现一个高可用性系统通常都是用一个可复制(备份)确定的状态机。他在Paxos算法里采用最大限度容错的方法来获得一致性但没有实时性的保证。通常的一致性花费代价很大,因此实际系统保留它用于紧急情况,并采用leases(超时锁)来用于大多数的计算。这篇论文阐述了高可用性计算的一般策略,给出了一般方法来理解并发和容错的程序,并把Paxos算法的作为该方法的一个例子。

 

1      介绍

        一个系统如果一经需求能立即提供服务,那么该系统就是可用的。利用可用性不算强的组件来建立一个高可用性系统的唯一方法是用冗余。因而,在一些部件出故障时系统仍能正常工作。最简单的冗余就是复制:保留每个部件的一些副本。

        这篇论文解释了如何用副本来建立一个高可用性的系统,并且它为关键算法给出了一个详细的规格说明和一个非正式的纠正校样。几乎所有的想法都源于LesLie Lamport : 复制的状态机、 Paxos一致性算法、说明和分析并发系统的方法。读了Lamport的论文后,我才写了这篇论文,理解这些方法及如何有效使用它们花费了很长时间。奇怪的是,尽管这些方法十分强大,但很少有人真正去理解它们。

        鉴于一致性容错的算法,在下个章节我们会介绍如何建立一个既高效可用性又强的复制状态机(备份状态机、副本状态机,不知道用哪个会比较好)。章节3将给出一致性问题的背景及应用。章节4阐述我们写规格说明的一些方法及用它描述某些形式下一致性的情况。章节5我们会介绍Paxos算法在一致性方面的基本思想及从这个思想和说明中推出该算法。最后,我们会介绍一些重要的优化及作出总结。

 

2       复制状态机

        光有冗余还是不够的,还必须使它们协调。最简单的方法是让每个非错误副本做同样的事。然后任一非错误副本都能提供输出;如果副本不会失败出故障,那么f个副本的同样输出能容忍f-1 个错误。其他的冗余(如错误矫正码)花费代价小,但是他们要依靠所提供服务的一些特殊属性。

        在这个章节,我们会解释如何用完全通用的高容错的方法来协调副本。然后我们作出一个称之为’leases’的优化,使得协调在几乎所有的情况下都能高效。

 

2.1    协调副本

              我们如何让每个副本做同样的事情了?采用Lamport首先提出的方法,我们建议一个副本作为一个确定的状态机;意思是从函数(state,input)转变为(new,state, output)。我们一般称这些副本为‘process’。那些以同样的状态开始和看见同样的输入序列的process会做同样的事情,即以同样的状态结束和产生同样的输出。因而,我们需要高可用性来确保所有的非错误process都能有同样的输入。这个技术术语是’consensus(一致性)(有时称之为’agreement’或者’reliable broadcast’)。非正式地,如果一些process在某些值上取得一致,我们这些process取得一致性;稍后我们会给出一个正式定义。

              因而一些process实现了同样的状态机并在某些值和输入序列中取得一致性的时候,它们会做同样的事情。这样的话,我们就有可能复制一个任意的计算并且使它具有高可用性。当然,在一些输入集上定义序列,我们可以使该序列成为输入值的一部分,比如将它们序列化为1,2,3,……

              在许多应用中,输入要求很多,从客户端到复制服务不等。例如,一个复制存储服务可能有Read(a)和Write(a,d)输入,一个飞机航班控制系统可能有ReadInstrument(i)何RaiseFlaps(d)输入。不同的客户端通常独立产生各自的请求,因此不仅要在请求上达到一致,而且服务请求的顺序上也要一致。最简单的方法就是用连续的整数来命名它们,从1开始。因为用一个process来分配连续的号码是很简单的事,所有上述任务在“初始拷贝“阶段会完成。因而,存储服务会在Input1= Write(x,3)及 Input2= Read(x)上达成一致。

              当请求的总序不是连续的整数时,也有些其他的方法达成一致。有关这方面的方法可以查阅Schneider的论文[11]。这些方法用来自全部序列集中的数字来标签每个输入(如(clientUID, timestamp)),并设计出一个方法能确保所有以前出现过的标签的输入值小于一个既定值。这很复杂,实际系统通常使用一个primary来序列化输入,来替换前面的方法。

 

2.2    Leases:高效的计算

              容错一致性代价很高。单个进程的互斥存取(也可称之为锁)是开销很小的,但它不是容错的---如果单个进程保持这个锁不放时进程失败了,那其他的进程也没法使用这个资源。因此,一个进程必须在一个状态机或资源上保持一个lease直到期满。当一个进程保持一个lease时我们称这个进程为’master’。并且没有其他进程使用这个资源直到lease期满。当然要维持以上机制,进程间必须有同步时钟。更确切地说,如果两个进程间的最大时钟脉冲相位差是e, 进程p的lease在时间t时刻过期,进程P就会知道在时刻t-e之前没有其他的进程会使用该资源。

          当master进程保持lease时,它可以任意读写该资源。写操作必须在有限的时间内完成,以确保写操作要么失败,要么先于期满后开始的操作之前完成;对于一个资源来说,这是个很严重的问题,比如SCSI磁盘,它的顺序化机制不是很强,写操作的时间也是有上限的。

          在事务处理系统中,锁通常都是lease。如果它们期满,则事务会终止,这就意味着写操作没有完成,该事务相当于已经跳过了。一个进程如果在一个事务的作用域之外使用lease,那必须十分小心,必须保证原子性。例如,在每次原子写操作之后,必须确保资源处于良好的状态,或者利用日志来使用redo或者undo方法。当期满的资源需要复制时,后者的方法肯定是要用到的。

           在一个进程的lease期满前,进程通过更新它来继续控制资源。一经需要,进程也会释放它的lease。如果你不能与保持该lease的进程会话,也许它已经失败了,但是你必须等待lease期满后才能使用该资源。因此,在更新一个lease的代价和必须等待lease期满之间,会有一个折中。一个短暂的lease意味着在恢复中有一个短暂的等待和一个更新lease的高昂的代价。一个长的lease意味着在恢复中会有一个长时间的等待和一个更低廉的更新的代价。

           lease常被用于进程缓存状态的某些部分。例如,一个文件的高速缓存线路,获知它不能改变。既然lease是一种锁,它可以拥有有一种“mode”来决定它的保持者能进行什么操作。如果该lease是独占的,那么这个进程可以任意改变其状态。这正如owner进程存取一个高速缓存线路和一个多口磁盘。·

 

2.3  分层的leases

           在一个容错系统中的lease必须通过持续的一致性来确保和更新。如此频繁使用一致性会导致代价太高,我们的解决方案是采用分层的leases。运行一致性来挑选czar C,并在大多数状态下给C一个lease。现在C释放x和 y上的子lease,并将它给master. 每个master控制自己的资源。Master用czar来更新它们的子lease。以上操作代价不高,因为它不需要任何协调。Czar通过一致性来更新它的lease.虽然开销大点,但只有一个czar的lease。czar很简单并且很少会失败,因而我们可以得到一个更长的lease.

           分层的lease常用于备份文件系统中和它的集群中。

           结合一致性,lease和分层的思想,我们有可能建立一个高效的高可用的系统。

 

 

 

3 一致性

           如果一些进程在某些“outcome”的值上达成一致,它们就取得了一致性(如果它们能在任意值上达成一致,那么解决方案就是小菜一碟了:始终等于0)。因此,一致性的接口有两个动作(action): 一是赋值,二是读出结果。当所有的非容错进程获得结果时,一致性算法就终止了。

           除了复制状态机,一致性还有许多专门的应用。例如,以下三个:

           分布式事务中,无论一个事务是提交了还是终止,所有的进程都必须达成一致。每个事务针对其输出结果需要保证隔离的一致性。

 

            参与者—一组提供高可用性服务的进程,需要和那些组成员的进程达成一致。

          

            在不知进程组成员的情况下,可以挑选出该组的leader。

 

           如果没有出现错误,一致性是很容易的。这里有个简单的实现。有个固定的fixed leader进程。它获得所有的Allow 动作,选择输出,并通知每个进程。如果失败,你就会倒霉。标准的两阶段提交就是这么工作的。如果所有的参与者准备就绪,那么allowed值会提交。如果至少有一个失败则会终止。若leader进程失败,那么结果可能是未知的。

           另外一个简单的实现是:一个进程集,每个进程选择一个值。如果大多数进程都选择同样的值,那么会有输出(可能大多数都是子集,因为两个可能都有非空交集)。如果没有大多数的进程选择同样的值,那么就没有输出。如果它们失败,则结果是未知的。

 

4详细说明

     我们在一个状态和一系列动作的集合下研究该系统:状态是某些(也可以是无限的)状态空间的一个元素,动作(也可以是不确定的)的集合是将系统从一个状态转变到另外一个状态。数据抽象,并发程序,分布式系统,和容错式系统都用这种方法来建模。通常,我们称状态空间为’varibales’(小空间)的笛卡尔积。

4.1 如何指明系统的状态。

     在具体化一个系统时,我们指派某些action(动作)或者变量为“外部的”,剩下的为“内部的”。我们关心的是外部action的顺序(也就是说,外部变量值的顺序),因为我们假定你不能在系统外观察到内部的action或变量。我们称这个顺序为该系统的’trace’。一个规格说明是trace的集合,或者是trace上的述语。这样的集合(trace的集合)称之为“property”。

     我们定义了两种特殊的property。非正式地,“安全”的property断言:以前没有糟糕的事情发生;它将顺序程序采取部分正确的泛化。“活跃”的property断言:好的情况最后发生了;它是终止的泛化。通过查看一个trace的一些有限的prefix(前缀),你可以发现该trace没有“安全” property。任意一个property(任意action的顺序的集合)是安全 property 和l活跃 property的集合。

     在这篇论文里,我们只处理安全 property。 看起来还是很合理的,因为我们知道,没有针对异步一致性的终止算法。这也是件好事,因为活跃 properity更难处理。

     通过状态机来定义安全 property是很方便的。这些状态机的action被划分为外部的和内部的。状态机内部action的所有顺序定义一个安全 property。不要把这些状态机和复制状态机混淆了。

     我们定义系统Y来实现另一个系统X,步骤如下:

 

·系统Y的每个trace是系统X的trace。系统X的安全 property包含系统Y的安全 property.

 

·系统Y的活跃 property包含系统X的活跃 property。

 

上述第一点确保,观察系统Y你无法区分出它是不是系统X;系统X不会做的糟糕的事,系统Y也不会做。第二点确保,系统X要做的好事,系统Y都会做。关于活跃 property,我们到此为止。

     采用以上方法,我们用状态来具体化一个系统,必须首先定义状态空间,然后描述动作(action)。我们选择状态空间来使规格说明更清晰,而不是反映实现的状态。对于每个action,我们都会描述该action对状态的行为以及它是内部的还是外部的。用参数和结果模型化一个action,比如Read(x),Read(x)返回3;当所有的客户端读到x时并且结果是3时,这个action发生了。

     如何写这些规格说明,下面有提示:

·注释很重要,因为它能帮助你思考。采用一套合适的词汇。

·少即是多。Action越少越好。

·非确定性越少越好,因为它允许更多的实现。

 

4.2 具体化一致性

     现在,我们关于一致性,具体说明一下。变量outcome初始化为nil。动作Allow(v)

 能被调用多次。动作Outcome可以读变量outcome;但是它必须返回nil,或者一些Allow动作的参数变量v;而且它必须一直返回同样的v。

     更准确地说,有如下两个要求:

     一致性:每个Outcome的非nil结果都须是相同的。

     有效性:一个非nil 的otcome等于某些允许值。

 

有效性,即意味着outcome不能是任意值,但是必须是一个允许的值。选取一些值并把它们分配给变量outcome来获取一致性。

      关于spec,有个很精确的版本,我们称之为版本C。它给出了状态和状态机的动作。状态是:

      Outcome: ValueU{nil} initially nil

动作是:

 

 

      有些外部动作被标记为a*.。 guard是先决条件,在当前状态下动作要发生时必须是准确的;对于这些动作都必须是准确的(由空白指示)。这个choose…..or…..表示非确定性选择,正如Dijkstra的看守命令。

      当做出来选择以后,Outcome就要返回nil。这反应了一个事实:用副本实现,Outcome经常与副本会话来实现,并且该副本可能还未获知选择.

      下面我们将用终止给出一个spec T来实现一致性。一旦内部终止动作发生,outcome肯定就不会是nil。通过调用Done就能摘掉算法是否已经终止。用方框标注它们,就可以标记这个改变。

 


          

 

 

           终止是否发生,spec T里面并没有说。在一个实现里面,Outcome一直返回nil以满足T。这看起来似乎无法令人满意,但在异步实现上这是最好的。换句话说,更强的spec将不会考虑异步实现。

           最后,关于延期一致性,下面有个更复杂的spec D。它累积允许值,然后在内部action里面选择一个。


           显而易见,D实现了T。为证明这点,在下章节采用抽象函数来描述。然而,需要一个预取变量或者后台模拟,因为实现滞后很久才作出决定,C和T只要看到允许值就会选择输出结果。给出D spec,我们有两个原因。原因之一是,一些人发现比起T来,要更容易理解,虽然它有更多的状态和更多的动作。另一个原因是将预取变量的需求移动到工具---该工具有D能实现T的功能,这样简化了更精准的工具:Paxos能实现D。

          

 

5 实现

        这章我们首先解释抽象函数,以演示满足具体化的一个实现。这个方法是通用的且实际的。然后,我们讨论设计和理解实现的一些隐晦点,为简单实现,我们用抽象函数来阐明该方法和隐晦点。下个章节,我们会展示如何用这个方法和隐晦点去导出Lamport的Paxos一致性算法。

     

5.1 证明Y实现X

           ‘implements’的定义告诉我们必须做什么(忽视活跃):表示Y的每个trace都是X的trace。从头开始做这个是很困难的,因为每个trace的长度一般都是无限的,并且Y有许多trace。因而,该证明需要一个归纳,我们喜欢这样的证明方法:只需做一次归纳,而且是一劳永逸的。幸运的是,有一般的方法可以证明:Y实现了X,而且没有在每个例子里面明确推论trace。这个方法是由Hoare首次提出的,用来证明数据抽象的正确性。Lamport和其他人将它推广到任意的并发系统中。

           该方法流程如下。首先,定义一个抽象函数f从状态Y到状态X。然后Y模仿X:

1)        f映射初始状态Y到初始状态X。

2)        每个动作Y和每个可达到的状态Y,有个X动作的序列在从外部上看是一样的,如下图标变换所示:

 

 


一系列X动作从外面上看是一样的。当所有的内部动作都被丢弃时,Y动作都是一样的。因此,如果Y动作是内部的, 那么所以的X动作肯定都是内部的。如果Y动作是外部的,所以的X动作必须是内部的。除了一个以外,但这个必须与Y动作一致。

一个直接的归纳显示,Y实现了X:针对任意一个Y的行为,通过把每个Y行为映射到一系列X行为(外部上看该X行为是一样的)上去,我们都可以构建一个X行为,从外部上看该X行为是一样的。然后 ,该系列X行为与原始的系列Y行为都是一致的。

如果Y实现了X,是否可以一直使用这个方法来证明它?答案是“基本可以”。 按照某些规则增加额外的历史记录和预取变量来修改Y可能是必须的。这些规则能确保修改的Y与原来的Y有同样的trace。在正确的历史记录和预取变量下,很有可能找到一个抽象函数。另一个选择是,我们可以使用抽象关系而非抽象函数,并做后台模拟和前台模拟。

为了证明Y模拟了X,我们需要知道Y处于什么样的状态,因为Y任意状态下的每个行动都会模拟一系列X行动;事实上,抽象函数功能不能由任意的状态Y来定义。最方便地表示Y的可达状态,是通过invariant(每个可达状态)。把该invariant写成连词是有好处的;然后我称每个连词为invariant。通常我们会需要一个比模拟需求更强的invariant;其他的优势是一个更强的归纳假设,它有可能会确定模拟的需求。

该proof的结构如下所示:

·定义一个抽象函数

·每个动作保持该invariant,建立invariant表示可达的状态,

·通过每个Y动作来模拟一系列X动作(外部相同),来建立模拟。

该方法仅仅只在action下生效,而且不需任何有关trace的推导。进一步而言,它独立处理每个action。只有该invariant连接该action。所以,如果我们改变(或者增加)一个动作Y,我们只需要核实新的action能保持invariant,并模拟一系列X动作(外部相同)。

根据该方法,关于推导、理解和证明该实现的正确性,有如下几点提示:

·首先写规格说明

·设计出实现的方法。这是关键并具有创出性的一步。通常,你可以在抽象函数里面体现关键想法。

·必须检查每个实现的action来模拟一些spec动作。添加invariant会使这个更容易。每个action必须维持它们。改变实现(或者spec)直到它生效。

·首先保持实现的正确性,然后再考虑效率问题。效率越高,意味着invariant越复杂。可能,你需要改变spec来得到一个更有效的实现。

一个有效的程序是逻辑边缘上的一次练习。

                                 Dijsktra

 

下面,我们将对每个实现给出抽象函数,对每个Paxos 算法给出invariant。实际上的证明(由invariant保持并且每个Y动作都模拟一个适当的X动作序列)是很常见的,我们忽略它们。

 

5.2 简单实现的抽象函数

           回想两个简单的,非容错的实现。在第一个实现里,leader进程告知每个其他的进程outcome(这是两阶段的做法)。对C而言,抽象函数是:


                  

上面是非容错的—如果leader失败,那么它就会失败。

         第二个实现里,有一系列进程,每个进程选择一个值。如果大多数进程选择同样的值,那就是outcome。 对C而言,抽象函数是:


         这是非容错的---大多数进程不一致的话就会失败,否则只是大多数中的一个失败。

 

6 Paxos 算法

         在这个章节,我们会讲解实现一致性的Paxos算法。这个算法是由Liskov和Oki独立发明的,并作为备份数据存储系统的一部分。它的核心思想是众所周知的异步一致性算法。下面是它的一些基本属性:

        

         由一些leader进程启动,leader进程引导一些agent(代理)进程来取得一致性。

         在同一时间内无论有多少个leader进程,leader进程或者agent进程无论以多大的频率失败或者恢复,无论有多少消息失败,延迟或者复制,它都是正确的。

         在一段相当长的时间内,leader进程能与绝大多数代理进程会话两次。如果在这段时间内只有一个leader进程,它将终止。

         如果一直有很多leader进程,那么它不会终止(幸运的是,因为我们知道有保证的终止是不可能的)。

 

         为得到一个完整的一致性算法,我们把上述算法和草率的基于超时的算法结合起来选择一个leader进程。如果在某段时间内草率算法没提供leader进程,或者多于一个的leader进程,那么该一致性算法可能会终止。但是如果草率算法曾经在很长的时间内产生过leader,那么该算法将会终止,无论早期的事情多么杂乱。

         我们首先解释Paxos最简单的版本,不必担心存储数据容量或已在信息中发送的数据容量。然后,我们介绍一些使系统更高效的优化步骤。为了得到一个有效的系统,我们经常会使用lease。

 

6.1 算法思想

         首先,我们回顾下前面讲到的结构。有一些agent进程,被标注为 I 。agent进程的行为是确定的;agent会按照指示进行。Agent进程有固定的存储容量,以防止崩溃。在算法的单次运行中,agent的配置是确定的(虽然用Paxos算法它会改变)。一些被标记为顺序集L的leader进程,将会告知agent具体做什么。Leader进程来去自如,它们是不确定的,它们没有固定的容量。

         Paxos算法的核心思想源于前面描述的非容错大多数一致性算法。如果agent进程不能大多数达成一致,那么算法将陷入困境,否则如果只是某些进程失败,那么剩下的进程将无法确定是否已经达到一致性。

         为了解决这个问题,Paxos算法有一系列标注为N 的round。 Round N有个leader进程,该进程在值Vn取得大多数一致。如果一个round陷入困境,另一个round会重新开始。如果round n 在Vn 取得大多数一致,那么Vn 就是结果。

         显而易见,要让上述描述生效,每两个取得大多数一致的round都必须有同样的值。Paxos算法中很复杂的一部分已经确保了这个属性。

         在每个round中leader进程

·查询agent进程,了解它们在过去一个round里的状态

·选择一个值并命令agent进程,让大多数进程接受它

·如果成功,把这个值作为结果分配给每个进程

 

       一个成功的round 平均花费2½个round的时间。如果leader多次失败,或者多个leader竞争,那会花费更多的round来达到一致性。

 

6.2 状态和抽象函数

      agent的状态是每个round中固定的“status”变量。“固定”的意思是,它不会因为agent的失败受到任何影响。变量status要么是个Value,要么是特殊标志“no ,neutral”中的一个。Agent的动作是被定义的,因此如果该status是neutral,那么该status只能改变。因此该agent状态被定义为一个数组S:


      如果大多数进程status 为no,那么该round会死掉;如果大多数的status是Value,那么这个round会成功。

         Leader的状态是当前round下的工作状态(如果它在当前round下不工作,则为nil)、在该round下的值(或者它没被选中为nil)和leader的允许值集合。


抽象函数里 allowed值是leader设置的集合。


以上定义我们必须有。


上面的意思是,在一个给定的round里,只有一个值agent都有相同的值。现在,我们能给出抽象函数的outcome:


以上的定义我们必须有:


 

     通过保证在一段时间内每个leader只在一个round内工作(不会重新使用一个round 号并且一个round至多有一个leader),我们保持invariant 1(一个round至多有一个值)。为保证后面的条件,使用(sequence number, leader identity)作为round号,我们可以使得leader的身份成为round号的一部分。然后是 N=(J,L) ,J通常都是整型的顺序集,进程leader I 为nl选择了(j, I),其中j是I以前没用过的一个J。例如,j 可能是本地时钟的当前值。稍后我们就会看到当leader选择j时,如何避免使用稳定的存储容量。

 

6.3 Invariants

       我们会介绍一个stable predicate的概念。这个predicate一旦是正确的,将永远是正确的。这点很重要的,因为作用于正确的predicate会很安全。其他的情况下因为并发或崩溃可能会改变。

         既然Si,n的非 neutral值不能改变,下面的predicate值是稳定的:


 

下面有个更复杂的stable predicate:


         换句话说,当你在n之前就已看到round,n将会被anchored丢弃,跳过死亡的round,你会看到同样的值n。 如果所有之前的round都死掉,无论n 的值是什么它都会被anchored。将它明确定义,我们需要N的总序,因此我们使用词典顺序。

         现在,为了达到一个成功的round,我们所需要做的就是保持invariant 2。至于如何保持该invariant, 当我们得到一个表格可以很容易维持一个分布式算法时,我们会加强它,即一系列动作的集合,每个动作只使用进程当前状态值。


         所以,我们只需 选择每个非nil 的Vn,只有一个。N被anchored。

现在剩下的算法就很清晰了。

 

6.4 算法

       一个leader必须选择一个round里的一个值,以便round被achored。因此,Leader I 选择了一个新的nl,并在小于等于nl的round里查询所有的agent并了解他们的状态。在一个agent响应以前,它会把比nl要早的round的neutral 状态变为no, 以便leader有足够的信息来anchor 该round。来自大多数agent的查询响应会给leader进程提供给足够的信息使round nl  anchored,具体流程如下:

      

              leader从nl开始回顾,跳过那些没有报告Value 状态的round, 因为它们都肯定会死掉(记住,I已收到大多数进程响应,并且报告状态要么是Value,要么是no)。当 I 变为有Value状态的round n时,它会选择值Vn 作为ul. 由于n被invariant 4 所anchored,      n和n1之间所有的round都会死掉,如果ul 变成它的值,nl 会被anchored。

 

                  如果所有以前的round都死掉,那么leader会选择给ul选择任意允许值。在这种情况下,nl   然会被anchhored。

 

         因为“anchored“和”dead” 是稳定的属性,这个选择在任何状态下都是有效的。

 

       在第二个round,leader会命令每个进程接受round nl 里面的 ul在roundnl处于neutural状态的agent会接受,通过把它的状态变为roundnl(因为它还没响应后面的查询)中的ul;在这种情况下,它将它的状态报告给leader。如果leader 收集到了大多数ul报告,那么它就得知roundnl已成功了,将ul作为算法的结果,把这个情况发送给最后半个round里的所有进程。因此,全部进程花费5条报文或 2½个round。

        既然该round在那一刻成功(spec 的Agree 动作发生并且抽象结果改变),通过接受它的值,一些agent便构成大多数,即使没有任何agent和leader知道已经发生了。事实上,如果一些agent接受该值后失败并且是在将它们的报告给leader之前,或者如果leader失败了,,round还是有可能成功的,并且在leader不知道这个事实的情况下。


      

      

       如图的例子有助于理解。如图所示,在算法的两个运行环境里,有三个round,agent a, agent b, agent c ,allowed={7,8,9}。在左边的运行里面三个round都死掉,如果leader收到三个agent的消息,可以任意选取allowed里面的值。如果leader只收到a, b或a,c的消息,那么leader知道round 3已经死掉了但是不知道round 2 已经死掉了,因此必须选择8。如果leader只收到b和c的消息,leader只知round 3死掉了,因此必须选择9。

       在图表右边的运行里,无论leader收到那个agent的消息,leader都不会知道round 2已经死掉。事实上,它还是成功了,除非leader收到a ,c 的消息但leader并不知情。尽管如此,leader必须选择9, 因为leader在最近的非死亡round里看到了该值。因此,一个成功的round(比如round 2)充当障碍,以阻止后面的round选择不同的值。

       在两个运行(run)里面都有三个round的原因是:至少有两个不同的leader加入,或者在完成每个round之前,leader就失败了。要不然,round 1 早就成功了。如果leader一直你追我赶,强迫agent在更早的round到达命令阶段之前就把它们的早期的状态设置为no,那么算法会进入死循环。

       下面是算法的细节。它的动作如上面的table里描述的那样,并和一些其他动作结合起来发送和接收报文。


      

       上面的算法对网络的性能要求是最低的:报文的丢失,复制,或者重发送都没问题。因为结点可以失败再恢复,一个性能更好的网络也不会使事情变得更简单。我们将网络模型化为一个从Leader到agent广播中介;实际上这是通过把单独报文发送给每个agent来实现的。Leader和agent都可以没有限度的重新发送报文;实际上agent只在响应leader的重新发送的要求下才会重新发送。

一个完整的证明需要在进程间模型化沟通渠道。一些报文会丢失或者复制。如果报文(i,s)在渠道里,除了在S某些neutral的组件上,Si都会在s上达成一致,

 

6.5 终止:选择leader

         算法什么时候终止?如果一个原有的round没有成功,那么leader不会启动另一个round。然后leader在查询与引导大多数进程都成功时,算法肯定会终止。Leader不必有同样的大多数进程,agent也不必同时结束。因而我们需要一个单独的leader,同一时间只运行一个round。如果有一些leader,那么一个正在运行的最大的round会最终成功。但是如果新的leader始终要启动更大的round,那么可能一个也不会成功。在上面的例子里,我们可以看到这点。这也是幸运的,因为我们从Fischer-Lynch-Paterson 的结果得知,没有一个算法能确保终止。

         如果暂时没有失败,那么很容易避免同时有两个leader。进程有时钟,发送、接收和处理报文的最大时间段也是已知的:

        

                  每个将要结束的leader都会广播它的名字

                   完成广播后,你会变为leader()。。。除非你已收到一个更大名字的广播。

 

         当然,如果报文延迟,或者在响应报文时处理迟到了,该算法会失败。当它失败时,可能暂时有两个leader。运行最大的round的leader会成功,除非还有其他的问题导致另一个leader出现。

 

 

7  优化

       没必要存储或发送agent的完整状态。相反,如下所示,一切都可以在有限的字节内编码表示。相关部分si只是近期Value的值和后面no的状态:


      

 

我们可以编码为(v, lasti, nexti)。 这就是一个agent所需存储和报告的信息。

       类似,leader在一份报告中所要记忆的是:最大的round,被报告的value和agent。仅仅计数报告是不够的,因为已报告的报文会由于重新发送而复制。

       Leader不必与agent一样,虽然它们可以一样而且经常一样。虽然在给出的算法里,leader可以选择以前没用到过的n,Leader不必是稳定的状态,。取而代之,它可以在失败后等待大多数的nexti, 并选择更大j的nl。这会产生nl,这个nl比leader(目前为止所有命令报文里出现的)里任何其他值都大(因为在大多数agent里nl以前不是nexti的值, 那么nl不会出现命令报文里)。这就是我们所需要的。

       如果Paxos 用于在非阻塞提交算法中取得一致性,那么第一个round里(查询/报告)可以与准备报文、它的响应结合起来。

       优化里最重要的是一系列的一致性问题,通常是备份状态机的连续步骤。我们试图用同样的leader,称之为“primary“,并运行一系列由索引P序列化的Paxos实例。Agent状态如下:


              每次leader改变,只需做一次查询。我们也可以在Paxos的下一个实例中将outcome报文和命令报文结合起来。结果是每个一致性都是两条报文(一个round)。

 

8 结论

      我们已经总结了如何用一致性建立高可用性的系统。思想是复制状态机,并在每次输出时取得一致性。为了更高效,使用lease来替换一致性大多数步骤(进程里的action).

       我们推导出一致性的最大限度容错,都是没有实时性保证的。Lamport的Paxos算法基于重复的round直到你得到一个大多数,并确保每个round都有同样的值。我们知道如何用小报文来实现它,并且拥有同样leader的序列里每个一致性在一个round下。

       最后,我们介绍了如何设计和理解并发的、容错的系统。方法是写一个简单的spec作为状态机,从实现到spec过程中找到抽象函数,建立合适的invariant,并用实现来模拟spec。这个方法解决了很多难题。

 

      

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值