PBFT共识算法原理
拜占庭问题
拜占庭将军问题(Byzantine Generals Problem),是由Leslie Lamport在其同名论文中提出的分布式对等网络通信容错问题。在分布式计算中,不同的计算机通过通讯交换信息达成共识而按照同一套协作策略行动。但有时候,系统中的成员计算机可能出错而发送错误的信息,用于传递信息的通讯网络也可能导致信息损坏,使得网络中不同的成员关于全体协作的策略得出不同结论,从而破坏系统一致性。拜占庭将军问题被认为是容错性问题中最难的问题类型之一。
PBFT算法
介绍
- BFT从上世纪80年代开始被研究,目前已经是一个被研究得比较透彻的理论,具体实现都已经有现成的算法。其中,PBFT是当中最著名的算法,PBFT是Practical Byzantine Fault Tolerance的缩写,意为实用拜占庭容错算法。该算法是Miguel Castro和Barbara Liskov在1999年提出来的,解决了原始拜占庭容错算法效率不高的问题,将算法复杂度由指数级降低到多项式级,使得拜占庭容错算法在实际系统应用中变得可行。
- 为了保证pbft算法的正确性,节点总数量n和作恶节点数量f必须满足n > 3f。至于原因,我们接着往下看。
The resiliency of our algorithm is optimal: 3f + 1 is the minimum number of replicas that allow an asynchronous system to provide the safety and liveness properties when up to f replicas are faulty (see [2] for a proof). This many replicas are needed because it must be possible to proceed after communicating with n - f replicas, since f replicas might be faulty and not responding. However, it is possible that the f replicas that did not respond are not faulty and, therefore, f of those that responded might be faulty. Even so, there must still be enough responses that those from non-faulty replicas outnumber those from faulty ones, i.e., n - 2f > f. Therefore n > 3f.
PBFT算法三阶段
算法的核心三个阶段分别是 pre-prepare 阶段(预准备阶段),prepare 阶段(准备阶段), commit 阶段(提交阶段)。图中的C代表客户端,0,1,2,3 代表节点的编号,打叉的3代表可能是故障节点或者是问题节点,这里表现的行为就是对其它节点的请求无响应。0 是主节点。整个过程大致是如下:
首先,客户端向主节点发起请求,主节点 0 收到客户端请求,会向其它节点发送 pre-prepare 消息,其它节点就收到了pre-prepare 消息,就开始了这个核心三阶段共识过程了。
Pre-prepare 阶段:节点收到 pre-prepare 消息后,会有两种选择,一种是接受,一种是不接受。什么时候才不接受主节点发来的 pre-prepare 消息呢?一种典型的情况就是如果一个节点接受到了一条 pre-pre 消息,消息里的 v 和 n 在之前收到里的消息是曾经出现过的,但是 d 和 m 却和之前的消息不一致,或者请求编号不在高低水位之间(高低水位的概念在下文会进行解释),这时候就会拒绝请求。拒绝的逻辑就是主节点不会发送两条具有相同的 v 和 n ,但 d 和 m 却不同的消息。
Prepare 阶段:节点同意请求后会向其它节点发送 prepare 消息。这里要注意一点,同一时刻不是只有一个节点在进行这个过程,可能有 n 个节点也在进行这个过程。因此节点是有可能收到其它节点发送的 prepare 消息的。在一定时间范围内,如果收到超过 2f 个不同节点的 prepare 消息,就代表 prepare 阶段已经完成。
Commit 阶段:于是进入 commit 阶段。向其它节点广播 commit 消息,同理,这个过程可能是有 n 个节点也在进行的。因此可能会收到其它节点发过来的 commit 消息,当收到 2f+1 个 commit 消息后(包括自己),代表大多数节点已经进入 commit 阶段,这一阶段已经达成共识,于是节点就会执行请求,写入数据。
检查点协议
在上面的一致性协议中可以看到,系统每执行一个请求,服务器都需要记录日志(包括,request、pre-prepare、prepare、commit等消息)。如果日志得不到及时的清理,就会导致系统资源被大量的日志所占用,影响系统性能及可用性。另一方面,由于拜占庭节点的存在,一致性协议并不能保证每一台服务器都执行了相同的请求,所以,不同服务器状态可能不一致。例如,某些服务器可能由于网络延时导致从某个序号开始之后的请求都没有执行。因此,设置周期性的检查点协议,将系统中的服务器同步到某一个相同的状态。简言之,主要作用有2个:1、同步服务器的状态;2、定期清理日志。
同步服务器的状态,比较容易理解与做到。比如在区块链系统中,同步服务器的状态,实际上就是追块,即服务器节点会通过链定时广播的链世界状态或其他消息获知到自己区块落后了,然后启动追块流程。
定期清理日志,怎么做呢?首先要明确哪些日志可以被清理,哪些日志仍然需要保留。如果一个请求已经被f+1台非拜占庭节点执行,并且某一服务器节点i可以向其他服务器节点证明这一点,那么该i节点就可以将关于这个请求的日志删除。协议一般采用的方式是服务器节点每执行一定数量的请求就将自己的状态发送给所有服务器并且执行一个该协议,如果某台服务器节点收到2f+1台服务器节点的状态,那么其中一致的部分就是至少有f+1台非拜占庭服务器节点经历过的状态,因此,这部分的日志就可以删除,同时更新为较新状态。
具体实现时可以联想到上面的一致性协议总的水线检查。上面的低水线h值等同于稳定检查点,稳定检查点之前的日志都可被清理掉。高水线H=h+k,也就是接收请求序号上限值,因为稳定检查点往往是间隔很多的序号才触发一次,所以k一般要设置的足够大。例如,每间隔100个请求就触发一次检查点协议,提升水线,k可以设置为200。
这里解释一下稳定检查点的概念,可以理解为当2f+1个节点都达到了某个请求序号,该请求序号就是稳定检查点。所有稳定检查点之前的消息都可以被丢弃,减少资源占用。 对比Raft,Raft是通过快照的方式压缩日志,都需要一个清理日志的机制,不然日志无限增长下去会造成系统不可用
视图更换协议
在一致性协议里,已经知道主节点在整个系统中拥有序号分配,请求转发等核心能力,支配着这个系统的运行行为。然而一旦主节点自身发生错误,就可能导致从节点接收到具有相同序号的不同请求,或者同一个请求被分配多个序号等问题,这将直接导致请求不能被正确执行。视图更换协议的作用就是在主节点不能继续履行职责时,将其用一个从节点替换掉,并且保证已经被非拜占庭服务器执行的请求不会被篡改。即,核心有2点:1,主节点故障时,可能造成系统不可用,要更换主节点;2,当主节点是恶意节点时,要更换为诚实节点,不能让作恶节点作为主节点。
当检测到主节点故障或为恶意节点触发视图更换时,下一任主节点应该选谁呢?PBFT的办法是采用“轮流上岗”的方式,通过(v+1) mod N,其中v为当前视图号,N为节点总数,通过这一方式确定下一个视图的主节点。还有个更关键的问题,什么时候触发视图更换协议呢?我们继续往下讨论。
如果是主节点故障的情况,这种情况一般较好处理。具体实现时,一般从节点都会维护一个定时器,如果长时间没有收到来自主节点的消息,就会认为主节点发生故障。此时可触发视图更换协议,当然具体实现时,细节可能会不同,比如,也可以是这种情况,客户端发送请求给故障主节点必然导致长时间收不到响应,所以,客户端将请求发送给了系统中所有从节点,从节点将请求转发给主节点并启动定时器,如果主节点长时间没有将该请求分配序号发送PRE-PREPARE消息,认为主节点故障,触发视图更换协议。这2种情况比较好理解,但就这2种情况吗?其实还有以下几种情况也会触发视图更换协议:
从节点广播PREPARE消息后,在约定的时间内未收到来自其他节点的2f个一致合法消息。
从节点广播COMMIT消息后,在约定的时间内未收到来自其他节点的2f个一致合法消息。
从节点收到异常消息,比如视图、序号一致,但消息不一致。
这三点,都有可能是主节点作恶导致的,但也有可能是消息丢失等原因导致的。虽然不一定是因为主节点异常导致的,但从另一个角度看,解决了从节点不能无限等待其他节点投票消息的问题。
这里补充一点,触发视图更换协议后,将不再接收除检查点消息、VIEW-CHANGE消息、NEW-VIEW消息之外的消息。也就是视图更换期间,不再接收客户端请求,暂停服务。
解决了什么时候触发的问题后,下一个问题就是具体怎么实现呢?当因上面的情况触发视图更换协议时,从节点i就会广播一个VIEW-CHANGE消息<VIEW-CHANGE,v+1,n,C,P,i>,序号n是节点i的最新稳定检查点s,C是2f+1个有效检查点消息,是为了证明稳定检查点s的正确性,P是位于序号n之后的一系列消息的结合,这里要包含这些信息可以理解为是证据,也就是说,从节点不能随便就发送一个VIEW-CHANGE,什么证据都没有,别人怎么能认同你更换视图呢?。上面我们提到过下一任主节点是谁的问题?通过(v+1) mod N确定的一下任主节点p(在图中就是节点1),在收到2f个有效的VIEW-CHANGE消息后,就广播<NEW-VIEW,v+1,V,O>消息,这里V和O具体的生成方法参考原论文,主要是VIEW-CHANGE和PRE-PREPARE等消息构成的集合,主要目的是为了让从节点去验证当前新的主节点的合法性以及解决下面这个问题,还有要处理未确认消息和投票消息。
视图更换协议需要解决的问题是如何保证已经被非拜占庭服务器执行的请求不被更改。由于系统达成一致性之后至少有f+1台非拜占庭服务器节点执行了请求,所以目前采用的方法是:由新的主节点收集至少2f+1台服务器节点的状态信息(也就是上面在构造消息时所需的各种消息集合),这些状态信息中一定包含所有执行过的请求;然后,新主节点将这些状态信息发送给所有的服务器,服务器按照相同的原则将在上一个主节点完成的请求同步一遍,同步之后,所有的节点都处于相同的状态,这时就可以开始执行新的请求。
相关问题
- prepare和commit阶段为何都要2f+1个节点反馈确认?(这2f+1并不一定是相同的)
对于prepare和commit来说,节点需要在2f+1个状态复制机的沟通内就要做出决定,这是刚好可以保证一致性的,考虑最坏的情况:我们假设收到的有f个是正常节点发过来的,也有f个是恶意节点发过来的,那么,第2f+1个只可能是正常节点发过来的。(因为我们限制了最多只有f个恶意节点)由此可知,“大多数”正常的节点还是可以让系统工作下去的。所以2f+1这个参数和n>3f+1的要求是逻辑自洽的。
- client为何只需要f+1个相同的回复就可确认?
之前我们说,prepare和commit阶段为何都要2f+1个节点反馈,才能确认。client只需要f+1个相同的reply就可以了呢?我们还是来考虑最坏的情况,我们假设这f+1个相同的reply中,有f个都是恶意节点。
所以至少有一个rely是正常节点发出来的,因为在prepare阶段,这个正常的节点已经可以保证prepared(m,v,n,i)为真,所以已经能代表大多数的意见,所以,client只需要f+1个相同的reply就能保证他拿到的是整个系统内“大多数正常节点“的意见,从而达到一致性。
- 如果primary是恶意节点呢?
对于一致性,我们可以这么看:如果prepared(m,v,n,i)为真,那么prepared(m’,v,n,j)一定是错误的,因为对于同一个提案我们不可能有两种结果,从而保证整个系统的一致性。
假设primary节点是恶意的,那么意味着在replicas节点中⾄多有f-1个恶意的节点,prepared(m,v,n,i)为真,则证明有f+1个善意节点达成了了⼀致,prepared(m’,v,n,j)为真,意味着另外f+1个善意节点达成了一致,因为系统中只有2f+1个善意节点,因此最少有⼀个善意节点发送了两个冲突的prepare消息,这是不可能的。所以prepared(m,v,n,i)为真,那么prepared(m’,v,n,j)是错误的。
- 为什么不能只有前2个阶段消息
这个问题的等价问题是:为什么Pre-prepare和Prepare消息,不能让非拜占庭节点达成一致?
Pre-prepare消息的目的是,主节点为请求m,分配了视图v和序号n,让至少f+1个非拜占庭节点对这个分配组合<m, v, n>达成一致,并且不存在<m', v, n>,即不存在有2个消息使用同一个v和n的情况。
Prepared状态可以证明非拜占庭节点在只有请求m使用<v, n>上达成一致。主节点本身是认可<m, v, n>的,所以副本只需要收集2f个Prepare消息,而不是2f+1个Prepare消息,就可以计算出至少f个副本节点是非拜占庭节点,它们认可m使用<v, n>,并且没有另外1个消息可以使用<v, n>。
既然1个<v, n>只能对应1个请求m了,达到Prepared状态后,副本i执行请求m,不就达成一致了么?
并不能。Prepared是一个局部视角,不是全局一致,即副本i看到了非拜占庭节点认可了<m, v, n>,但整个系统包含3f+1个节点,异步的系统中,存在丢包、延时、拜占庭节点故意向部分节点发送Prepare等拜占庭行文,副本i无法确定,其他副本也达到Prepared状态。如果少于f个副本成为Prepared状态,然后执行了请求m,系统就出现了不一致。
所以,前2个阶段的消息,并不能让非拜占庭节点达成一致。
如果你了解2PC或者Paxos,我相信可以更容易理解上面的描述。2PC或Paxos,第一步只是用来锁定资源,第2步才是真正去Do Action。把Pre-prepare和Prepare理解为第一步,资源是<v, n>,只有第一步是达不成一致性的。
- 2个不变性
PBFT的论文提到了2个不变性,这2个不变性,用来证明PBFT如何让非拜占庭节点达成一致性。
第1个不变性,它是由Pre-prepare和Prepare消息所共同确保的不变性:非拜占庭节点在同一个view内对请求的序号达成共识。关于这个不变性,已经在为什么不能只有前2个阶段消息中论述过。
介绍第2个不变性之前,需要介绍2个定义。
committed-local:副本i已经是Prepared状态,并且收到了2f+1个Commit消息。
committed:至少f+1个非拜占庭节点已经是Prepared状态。
第2个不变性,如果副本i是committed-local,那么一定存在committed。
2f+1个Commit消息,去掉最多f个拜占庭节点伪造的消息,得出至少f+1个非拜占庭节点发送了Commit消息,即至少f+1个非拜占庭节点是Prepared状态。所以第2个不变性成立。
- 为什么3个阶段消息可以达成一致性
committed意味着有f+1个非拜占庭节点可以执行请求,而committed-local意味着,副本i看到了有f+1个非拜占庭节点可以执行请求,f+1个非拜占庭节点执行请求,也就达成了,让非拜占庭节点一致。
虽然我前面使用了2PC和Paxos做类比,但不意味着PBFT的Commit阶段就相当于,2PC和Paxos的第2步。因为2PC和Paxos处理的CFT场景,不存在拜占庭节点,它们的主节点充当了统计功能,统计有多少节点完成了第一步。PBFT中节点是存在拜占庭节点的,主节点并不是可靠(信)的,不能依赖主节点统计是否有f+1个非拜占庭节点达成了Prepared,而是每个节点各自统计,committed-local让节点看到了,系统一定可以达成一致,才去执行请求。
- 在3阶段协议中,对收到的消息都要进行消息合法性检查、视图检查、水线检查这3项检查,为什么呢?
这3项检查是十分有必要的,添加消息签名是为了验证投票是否合法,正确统计合法票数,不能是随便一个不知道的节点都能投票,那我怎么验证到底是谁投的呀。也就是说,要通过消息签名的方式确认消息来源,通过消息摘要的方式,确认消息没有被篡改。当然,考虑到性能因素,也可以使用消息认证码(MAC),以节省大量加解密的性能开销。PBFT算法,可以容忍节点作恶,消息丢失、延时、乱序,但消息不能被篡改。
视图检查比较容易理解,所有节点必须在同一个配置下才能正常工作。如果节点的视图配置不一致,比如主节点不一致、节点数量不一致,那统计合法票数的时候,真没法干了。
水线检查,是检查点协议的一部分,在工程实现时,不是所有的请求我都有处理,比如,你收到一个历史投票信息,你还有必要处理吗?当然,它的作用不止于此,还可以防止恶意节点选择一个非常大的序列号而耗尽序列号空间,例如,当一个节点分配了超过H上限的序列号,这时,正常节点会拒绝这个请求从而阻止了恶意节点分配的远超过H的序列号。
- 3阶段协议中每一阶段的意义是什么?
论文中有如下表述:
The three phases are pre-prepare, prepare, and commit.The pre-prepare and prepare phases are used to totally order requests sent in the same view even when the primary, which proposes the ordering of requests, is faulty. The prepare and commit phases are used to ensure that requests that commit are totally ordered across views.
即,pre-prepare和prepare阶段,主要的作用就是定序,个人理解就是要确认有足够数量的节点收到同一请求,并且与自己所收到的请求相一致。prepare以及commit阶段是确认大家执行的同一请求。
总结
- 特征/优点:
- 通信复杂度O(n^2)。
- 首次提出在异步网络环境下使用状态机副本复制协议,该算法可以工作在异步环境中,并且通过优化在早期算法的基础上把响应性能提升了一个数量级以上。作者使用这个算法实现了拜占庭容错的网络文件系统(NFS),性能测试证明了该系统仅比无副本复制的标准NFS慢了3%。
- 使用了加密技术来防止欺骗攻击和重播攻击,以及检测被破坏的消息。消息包含了公钥签名(其实就是RSA算法)、消息验证编码(MAC)和无碰撞哈希函数生成的消息摘要(message digest)。
- 适用于permissioned systems (联盟链/私有链),能容纳故障节点,也能容纳作恶节点。要求所有节点数量至少为3f+1(f为作恶/故障不回应节点的数量),这样才能保证在异步系统中提供安全性和活性。
- 解决了原始拜占庭容错(BFT)算法效率不高的问题,将算法复杂度由指数级降低到多项式级,使得拜占庭容错算法在实际系统应用中变得可行。
- 缺点:
- 仅仅适用于permissioned systems (联盟链/私有链)。
- 通信复杂度过高,可拓展性比较低,一般的系统在达到100左右的节点个数时,性能下降非常快。
- PBFT在网络不稳定的情况下延迟很高。