引言
大家前段时间应该都看到了Facebook发布区块链Libra的消息。与大名鼎鼎的比特币相比,Libra有一个核心的特点就是修改了共识算法,从PoW换为了基于拜占庭将军问题演化而来的LibraBFT。今天要介绍的PBFT(Practical Byzantine Fault Tolerance)算法,是最经典的解决拜占庭将军问题的算法之一,也是LibraBFT很多核心思想的来源。
PBFT算法的全称叫实用拜占庭容错算法,是用来保证在分布式系统有节点可能背叛的情况下,对某个请求达成一致。本文会先介绍一些拜占庭将军问题的基本知识,然后再详细介绍PBFT算法的整个过程,希望能帮助初学者对拜占庭将军问题和PBFT算法有一个基本的认知。因为篇幅和水平的限制,省略了证明的过程,如果有错误和建议请随时提出。
大家之前可能对Paxos、Raft、Zab等传统分布式一致性算法有所了解,本文所说的共识算法与传统一致性算法的主要区别在于是否有背叛节点,传统一致性算法主要解决的是在机器故障、网络分区等情况下保证一致性,而本文中提到的共识算法不仅有传统的问题,还要防范背叛节点联合起来对系统进行有预谋的主动破坏。
起源-拜占庭将军问题
拜占庭将军问题简介
PBFT算法的提出,就是为了解决拜占庭将军问题,所以想要了解PBFT算法,就要从拜占庭将军问题讲起。拜占庭将军问题(Byzantine Generals Problem),是由莱斯利·兰波特在其同名论文中提出的分布式对等网络通信容错问题。
在分布式计算中,不同的计算机通过通讯交换信息达成共识而按照同一套协作策略行动。但有时候,系统中的成员计算机可能出错而发送错误的信息,用于传递信息的通讯网络也可能导致信息损坏,使得网络中不同的成员关于全体协作的策略得出不同结论,从而破坏系统一致性,这种问题被称为拜占庭将军问题。拜占庭将军问题被认为是容错性问题中最难的问题类型之一。
我们用更通俗的语言来描述一下,假设了这样一个场景,n 个将军被分隔在一个城市周围的不同地方,他们之间只能通过信使进行通信。n个将军中大部分是忠诚的将军,他们会按照约定的去做,但是有一些背叛的将军(又叫拜占庭节点),他们会做任意想做的事情。为了取得战斗的胜利,忠诚的将军必须达成一致,比如一起进攻或者一起撤退,否则会有灾难性的后果。所以他们需要有一个算法来保证无论叛徒做什么,所有忠诚的将军都能达成一致。
PBFT算法作为一致性算法的一种,主要考虑的是一致性,也就是说在有节点背叛的状态下,所有正常节点达成一致,具体达成的一致是什么(进攻或者撤退),算法并不关心。
经典的拜占庭将军问题解法
兰波特在论文中在不同的约束条件下,提出了数种解决方案。这里我们详细介绍其中一种,这种解法基于以下一些约束条件:
有了上述约束条件,我们先从只有四个节点(将军),其中一个为主节点的简单场景入手,看一看如何设计一个算法来保证进攻或者防守命令的一致性。
分布式系统中有一个经典定理叫FLP不可能定义,其核心是如果通信不畅通的情况下,分布式系统永远无法达成一致。因此我们的算法都假设在超时时间之内通信都是畅通,如果通信不通,就把对方归为拜占庭节点。
通信可以确认来源和防止伪造是解决拜占庭问题的必要条件之一,在分布式系统中一般通过签名和摘要等密码学来保证,本文讲的经典解法不需要密码学保证,只需要消息可以确认来源。
这种经典算法可以保证一致,一共分为以下三步:
如果主节点是拜占庭节点
拜占庭节点的定义是随机行为,为了便于分析算法是否可靠,我们假设拜占庭节点都是叛徒节点,故意来破坏算法的一致性。
我们先来分析如果主节点是拜占庭节点,看看他可以进行哪些破坏行为,这些行为是否会影响一致性:
算法步骤1,主节点A向从节点B发送命令0,向从节点CD发送命令1
算法步骤2,从节点BCD收到命令后,把收到的A命令同步给其他两个从节点
算法步骤3,各个节点按照majority算法计算各自的请求结果。如图所示,除了主节点,其余节点都收到了两个1和一个0,majority算法是大于两(2f)个1结果就是1,忠诚的三个从节点达成了一致。
如果从节点是拜占庭节点
如果从节点是拜占庭节点的情况下,那么他可以做的破坏行为有一下几种:
这种情况下只有自己与其他人不一致,忠诚节点之间是一致的,不会影响一致性。
我们按照拜占庭从节点给不同节点发送不同消息来执行一下算法,来看一看是否会破坏一致性。拜占庭从节点不发送消息等同于宕机的情况较为简单,大家可以自己套用算法执行看看。
算法步骤1,主节点A向从节点BCD发送命令1
算法步骤2,从节点BCD收到命令后,把收到的A命令同步给其他两个从节点,其中D为拜占庭从节点,分别给B和C发送了1和0两条不同的消息
算法步骤3,各个节点按照majority算法计算出请求的结果。如图所示,AB节点收到的都是1,所以算法执行结果一定是1,C节点收到两个1,majority算法的结果也是1,ABC三个忠诚节点之间达成了一致。
算法的容错能力
大部分拜占庭问题解法容错能力都是在总节点数为3f+1时,最多能在有f个拜占庭节点时达成一致性,如果拜占庭节点超过f个就可能无法达成一致,上述经典拜占庭算法也不例外。当总结点数是一个是单机问题,总结点是两个节点时,有一个拜占庭节点只剩一个忠诚节点,讨论一致性也没有意义。所以我们用反证法证明总结点数为三时,如果有一个拜占庭节点一定无法达成一致。如果主节点是拜占庭节点,我们来执行以下算法的流程:
2. 从节点BC互相同步收到的命令
3. 从节点BC各自收到一个1一个0,此时收到的结果没有一方占据多数,且各自从主节点收到的命令也不相同,此时无论如何设计majority算法都无法达成一致。
本文只讨论了基础情况下,经典拜占庭问题算法的执行过程,容错能力的证明较为简单,主要是通过反证法来进行。假设3f+1个节点中,主节点加f个节点是拜占庭节点,一定可以破坏一致性。而在拜占庭节点小于等于f个时,可以达成一致性的证明主要是依靠数学归纳法来实现,此处就不具体讨论,有兴趣的可以看一下。
进阶-从拜占庭容错到实用拜占庭容错
上述经典拜占庭问题的解法可以保证在有拜占庭节点情况下的一致性,但是缺少了很多部分,导致不实用,PBFT算法就在经典算法的基础上完善了下列部分,让算法变得更实用:
PBFT(Practical Byzantine Fault Tolerance)算法由Miguel Castro 和Barbara Liskov在1999年提出来的,是一个基于状态机复制的算法,设计用来解决拜占庭将军问题。算法在异步通信条件下,可以保证高可用(liveness)和安全性(safety)。
PBFT中的一些基础概念
我们以一个分布式文件系统为例,介绍一下实用拜占庭容错的过程。
- client c,分布式文件系统之外的独立客户端,可以发起请求
- request m,客户端发起的请求,包含一个操作
- fault f,拜占庭节点的个数
- replica i,编号为i的节点
- n,总结点数,又是也写为3f+1
- view v,视图编号,用于表示当前主节点。主节点编号i = v mod n,v在切主过程中单调递增
- timestamp t,时间戳,有过期时间delay(t)用于防止重放
- operation o,请求中的操作,例如记录一条日志
- checkpoint,检查点用于垃圾回收,在视图切换时会详细说明
- 高低水位,用于平衡最快节点和最慢节点之间的执行差距
PBFT算法的基本流程
简单来说,PBFT算法可以分为以下四步,其中步骤2和步骤3之间的部分就使用了经典拜占庭容错算法
PBFT算法请求的五个阶段
主节点不是拜占庭节点的情况下,一次正常客户端的请求分为以下五个阶段,如下图所示,通过这五个阶段可以保证在从节点是拜占庭节点时,不会影响一致性。
Client c向主节点发送消息m 发起一次请求。(如果请求没有被快速执行,客户端就向全部节点广播)
主节点接收到c的请求,校验合法后,为当前请求分配一个自增序号n,然后主节点向所有从节点广播,其中v是当前视图编号,m是请求本身。此处等同于经典算法的步骤1。
当从节点收到消息后会做以下校验:
a. request和pre-prepare消息签名正确(验证来源)
b. pre-prepare消息与自身存储的视图编号相等(保证没切主)
c. 该节点没有接收过v、v相同,但是摘要不同的消息(验证完整性)
d. n在高低水位之间(防止主节点破坏)
如果校验通过,会就会进入prepare阶段。进入prepare阶段后,节点会向其他所有节点广播消息,此阶段基本等同于经典算法的步骤2。
当任意节点收到2f(包括自己,不包括主节点)个经过确认的prepare消息后,节点拥有prepared certificate,进入commit阶段,进入commit阶段后,节点会执行o,然后广播。任意一个正常节点进入commit状态就可以认为该请求已经执行成功,之后无论发生什么都不应该丢失。此处基本等同于经典算法的步骤3。
当节点收到2f+1条commit消息后,向客户端发送消息,客户端c收到f+1条reply消息后,即认为本次请求成功 。(有一条正常节点的commit其实就可以认为成功,为了防止f个拜占庭节点所以要等待f+1个reply消息)
PBFT算法的垃圾回收
一个节点进入commit阶段执行完操作,对于其自身来说,已经达到了一致性的终点,但是此后还有reply阶段和意外切主的恢复,所以说整个请求流程的日志记录都必须保留。
所以需要一种机制来清理数据,这里使用的就是checkpoint的方式。当连续执行了k条请求之后,在第k条请求执行完,进行全网广播说自身的checkpoint到达了k条。大家同时在k条广播,如果一个节点收到了2f+1条在k的checkpoint,就会认为之前的数据都已经不重要了,可以把之前的数据清理掉了。
checkpoint还有一个作用就是在高低水位,高低水位是节点可以执行的操作序号的上下限。低水位就是stable checkpoint所在的位置,高水位是stable checkpoint所在位置加2k(几倍于k可配置),这样可以保证分布式系统中,执行最快的一批节点和执行最慢的一批节点之间的之间操作差不大于2k。
PBFT算法的视图更换
PBFT算法视图更换是为了保证系统能从主节点故障中恢复。
先来说说视图更换的触发,这部分主要由从节点的超时机制触发。超时机制具体分为以下几个部分:
通过上述超时机制,可以防止拜占庭节主节点带来的影响。再来发现主节点是拜占庭节点之后的切主的流程,切主的流程分为以下几个步骤:
通过new-view、view-change和重放pre-prepare三个操作,PBFT算法就可以保证在视图切换中保证一致性。
PBFT算法的缺点
总结
与区块链PoW算法的对比
通过上边文章,我们已经基本了解了PBFT算法。最后我们再与比特币的PoW算法做个对比,让大家认识到PBFT类共识算法与PoW算法的核心区别:
最后的最后
从经典BFT算法演进到PBFT算法,完善了许多的工程细节。根据PBFT算法,我们已经可以实现一个支持拜占庭容错的分布式文件系统。不过最近几年随着区块链的火热和密码学的发展,解决拜占庭问题的算法也在不断发展,比如facebook使用的hotstuff算法就是基于门限签名来实现的。不过万变不离其宗,了解了拜占庭问题最经典的算法,相信你在看其他拜占庭容错算法一定会更加如鱼得水。