简介
实用拜占庭容错 (Practical Byzantine Fault Tolerance, PBFT) 算法是Miguel Castro和Barbara Liskov发表于1999年OSDI(Operating Systems Design and Implementation)会议的研究成果。PBFT描述了一种解决拜占庭容错问题的副本复制算法,解决了原始拜占庭容错算法效率不高的问题,将算法复杂度由指数级降低到多项式级,使得拜占庭容错算法在实际系统应用(异步环境)中变得可行。
PBFT是一种状态机副本复制算法,即服务作为状态机进行建模,状态机在分布式系统的不同节点进行副本复制。每个状态机的副本都保存了服务的状态,同时也实现了服务的操作。将所有的副本组成的集合使用大写字母R表示,使用0到|R|-1的整数表示每一个副本。为了描述方便,假设|R|=3f+1,这里f是有可能失效的副本的最大个数。尽管可以存在多于3f+1个副本,但是额外的副本除了降低性能之外不能提高可靠性。
所有的副本在一个被称为视图(View)的轮换过程中运作。在某个视图中,一个副本作为leader节点(也称primary主节点),其他的副本作为follower节点(也称backup备份节点)。视图是连续编号的整数。leader节点由公式p = v mod |R|计算得到,这里v是视图编号,p是副本编号,|R|是副本集合的个数。当leader节点失效的时候就需要启动视图更换(view change)过程。
主节点由公式p = v mod |R|计算得到,这里v是视图编号,p是副本编号,|R|是副本集合的个数。当主节点失效的时候就需要启动视图更换(view change)过程。
算法步骤
(1)取一个副本作为主节点,其他的副本作为备份;
(2)用户端向主节点发送使用服务操作的请求;
(3)主节点通过广播将请求发送给其他副本;
(4)所有副本执行请求并将结果发回用户端;
(5) 用户端需要等待F+1个不同副本节点发回相同的结果,作为整个操作的最终结果。
算法分为三个阶段:pre-prepare、prepare 和 commit。Pre-prepare和Prepare保证同一个view中的请求有序,Prepare和Committed保证不同view中的请求被有序地执行。
pre-prepare阶段
目的:作为一种证明,确定该请求是在视图v中被赋予了序号n,从而在视图变更的过程中可以追索。
当primary节点收到请求时,则开始pre-prepare阶段:primary对收到的请求标记序号n(n是按序增长的),把pre-prepare消息进行广播,并把此消息记入自己的log中。消息格式为<PRE-PREPARE,v,n,m>,v是primary节点当前的视图号,n是这个请求的序号,m是请求。
Backup节点收到<PRE-PREPARE,v,n,m>消息时进行检查,需满足的条件:
(1)请求和预准备消息签名正确,并且d与m摘要一致;
(2)当前视图编号是v;
(3)该备份节点从未在视图v中接受过序号为n但是摘要d不同的消息m;
(4)预准备消息的序号n必须在水线(watermark)上下限h和H之间。
完成标志:
若备份节点i接受了预准备消息,则进入prepare阶段。
prepare阶段
一个backup(i)节点接受<PRE-PREPARE,v,n,m>消息后,则进入prepare阶段,该节点i向所有副本节点发送准备消息,消息格式为<PREPARE,v,n,d,i>,其中d是m的digest,i把<PRE-PREPARE,v,n,m>消息和<PREPARE,v,n,d,i>消息都记入自己的log中。
如果一个节点收到2f个来自不同节点且与PRE-PREPARE匹配的<PREPARE,v,n,d,i>消息时(匹配的条件是要有相同的v和相同的n),我们把这2f个<PREPARE,v,n,d,i>消息作为prepared的证明。
完成的标志:副本节点将(m,v,n,i)记入消息日志。
接受准备消息需满足的条件:
(1)每个副本节点验证预准备和准备消息的一致性主要检查:视图编号v、消息序号n和摘要d。
(2)预准备阶段和准备阶段确保所有正常节点对同一个视图中的请求序号达成一致。(对此结论的形式化证明:如果prepared(m,v,n,i)为真,则prepared(m`,v,n,j)必不成立,这就意味着至少f+1个正常节点在视图v的预准备或者准备阶段发送了序号为n的消息m)
commit阶段
如果一个节点(i)得到了prepared的证明,则广播commit消息到每一节点,并把此commit消息记入log中。消息格式为<COMMIT,v,n,D(m),i>。如果一个节点收到2f+1个来自不同节点且有相同的n、相同的v、相同的d的<COMMIT,v,n,D(m),i>消息,我们把这2f+1个commit消息作为committed的证明。当一个请求被提交到一个节点,这个节点执行请求,并把结果返回给客户端。节点保存的有上一个返回给客户端的应答(last reply),从中可以得到last reply的时间戳,节点不接受时间戳小于last reply的时间戳的请求。
每个副本接受确认消息的条件是:1)签名正确;2)消息的视图编号与节点的当前视图编号一致;3)消息的序号n满足水线条件,在h和H之间。一旦确认消息的接受条件满足了,则该副本节点将确认消息写入消息日志中。(补充:需要将针对某个请求的所有接受的消息写入日志,这个日志可以是在内存中的)
接受确认消息需要满足的条件:
我们定义确认完成committed(m,v,n)为真得条件为:任意f+1个正常副本节点集合中的所有副本i其prepared(m,v,n,i)为真;本地确认完成committed-local(m,v,n,i)为真的条件为:prepared(m,v,n,i)为真,并且i已经接受了2f+1个确认(包括自身在内)与预准备消息一致。确认与预准备消息一致的条件是具有相同的视图编号、消息序号和消息摘要。
确认被接受的形式化描述:
确认阶段保证了以下这个不变式(invariant):对某个正常节点i来说,如果committed-local(m,v,n,i)为真则committed(m,v,n)也为真。这个不变式和视图变更协议保证了所有正常节点对本地确认的请求的序号达成一致,即使这些请求在每个节点的确认处于不同的视图。更进一步地讲,这个不变式保证了任何正常节点的本地确认最终会确认f+1个更多的正常副本。
视图变更
使用计时器的超时机制触发视图变更事件,以防止备份节点无期限地等待请求的执行。视图变更协议在主节点失效的时候仍然保证系统的活性。视图变更可以由超时触发,以防止备份节点无期限地等待请求的执行。备份节点等待一个请求,就是该节点接收到一个有效请求,但是还没有执行它。当备份节点接收到一个请求但是计时器还未运行,那么它就启动计时器;当它不再等待请求的执行就把计时器停止,但是当它等待其他请求执行的时候再次情动计时器。
如果备份节点的计时器在视图中超时,备份节点启动视图变更以将系统变更到视图v+1。它停止接受消息(除了检查点,视图变更和新视图消息)并广播<view-change,v+1,n,C,P,i>消息到所有follower节点。这里n是节点i已知的最新稳定检查点s的序列号,C是2f+1个有效的证明了s正确性的检查点消息集合,P是在节点i已经准备好(prepared)的序列号高于n的请求所对应的Pm的集合。每个Pm包含有效的预准备消息(不包含相应的客户端消息)和2f个来自不同节点的与之相匹配的有效的准备消息。
当新leader节点从其他follower节点接收到2f个有效视图变更消息时,它会将<new-view,v+1,V,O>消息广播到其他follower节点,其中V是包含leader节点接收到的有效视图变更消息加上leader节点发送的视图变更消息(或将已发送),O是一组预准备消息(没有捎带请求)。O计算如下:1、leader节点确定最新的稳定检查点的序列号min-s和准备消息中的最高序列号max-s。2、leader节点为视图v+1创建一个新的预准备消息,其序号n在min-s和max-s之间。有两种情况: (1)在V中的一些视图变更消息的P分量中至少有一个集合具有序列号n,或者(2)没有这样的集合。在第一种情况下,主要创建一个新的消息<PRE-PREPARE ,v+1,n,d>,其中d是预备消息中的请求摘要,该请求具有最高视图编号的序列号n。在第二种情况下,它创建一个新的预准备消息<PRE-PREPARE ,v+1,n,d-null>,其中d-null是特殊空请求的摘要; 空请求像其他请求一样通过协议,但其执行是无操作的。(使用类似的技术填补空白。)
接下来,leader节点将O中的消息插入到它的日志中。如果min-s大于其最新的稳定检查点的序列号,leader节点也将具有序列号min-s的检查点稳定性的证明插入日志中,并丢弃来自日志中无异议消息记录。然后进入视图v+1:这时它能够接受视图v+1的消息。
一个备份节点接受新视图消息的条件是,如果签名正确,并且包含的视图变更消息对于视图v+1有效,并且集合O是正确的。它通过执行类似于leader节点使用的创建O的计算来验证O的正确性。然后,将新信息添加到其日志中,并且为O中的每个消息广播准备消息,并且将这些准备消息添加到其日志中,并进入视图v+1。此后,协议正常进行。副本重做min-s和max-s之间的消息协议,但是它们避免重新执行客户端请求(通过使用他们存储的关于发送给每个客户端的最后一个答复的信息)。
副本可能缺少一些请求消息或稳定的检查点(因为这些不会在new-view消息中发送)。它可以从另一个副本获取丢失的信息。例如,副本i可以从某些检查点消息认证了检查点s的正确性的副本之一获取缺少的检查点状态。由于这些副本中的f+1个是正确的,所以副本将始终拥有检查点s或稍后被认证的稳定检查点。我们可以通过分块状态避免发送整个检查点,并用修改它的最后一个请求的序列号对每个分区进行标记。为了使副本更新,只需要发送过期的分区,而不是整个检查点。
垃圾回收
为了节省内存,系统需要一种将日志中的无异议消息记录删除的机制。为了保证系统的安全性,副本节点在删除自己的消息日志前,需要确保至少f+1个正常副本节点执行了消息对应的请求,并且可以在视图变更时向其他副本节点证明。另外,如果一些副本节点错过部分消息,但是这些消息已经被所有正常副本节点删除了,这就需要通过传输部分或者全部服务状态实现该副本节点的同步。因此,副本节点同样需要证明状态的正确性。
在每一个操作执行后都生成这样的证明是非常消耗资源的。因此,证明过程只有在请求序号可以被某个常数(比如100)整除的时候才会周期性地进行。我们将这些请求执行后得到的状态称作检查点(checkpoint),并且将具有证明的检查点称作稳定检查点(stable checkpoint)。
副本节点保存了服务状态的多个逻辑拷贝,包括最新的稳定检查点,零个或者多个非稳定的检查点,以及一个当前状态。写时复制技术可以被用来减少存储额外状态拷贝的空间开销。
检查点的正确性证明的生成过程如下:当副本节点i生成一个检查点后,向其他副本节点广播检查点消息<CHECKPOINT,n,d,i>,这里n是最近一个影响状态的请求序号,d是状态的摘要。每个副本节点都默默地在各自的日志中收集并记录其他节点发过来的检查点消息,直到收到来自2f+1个不同副本节点的具有相同序号n和摘要d的检查点消息。这2f+1
个消息就是这个检查点的正确性证明。
具有证明的检查点成为稳定检查点,然后副本节点就可以将所有序号小于等于n的预准备、准备和确认消息从日志中删除。同时也可以将之前的检查点和检查点消息一并删除。
检查点协议可以用来更新水线(watermark)的高低值(h和H),这两个高低值限定了可以被接受的消息。水线的低值h与最近稳定检查点的序列号相同,而水线的高值H=h+k,k需要足够大才能使副本不至于为了等待稳定检查点而停顿。加入检查点每100个请求产生一次,k的取值可以是200。
算法优化
(1)简化回复(digest replies)
客户端只需要一个备份节点发送完整回复消息,其余备份节点只需发送回复消息的摘要。可获取完整回复以验证正确性,同时减轻网络带宽消耗。
(2)执行优化(减少消息延迟)
(3)优化只读操作(read-only operations)
不改变服务状态