本文已参与「新人创作礼」活动,一起开启掘金创作之路。
定义
实用拜占庭容错算法 Practicl Byzantine Fault Tolerance 由卡斯特罗(Miguel Casstro)和利斯科夫(Barbara Liskov) 在1999年提出来的,
- 解决原始拜占庭容错算法效率不高,信道必须可靠(未考虑消息延迟,消息丢失)问题。
- 满足将军总数大于n,背叛将军个数为f,n>3f+1情况下,忠诚的将军能达成共识。
- 将算法复杂度由指数级( n为节点总数,m为恶意节点数)降到多项式级( n为节点总数)
算法协议
- 三阶段提交协议:解决达成共识问题
- 视图更换协议:用于解决主节点失效,或主节点恶意节点情况。一个视图代表着某个节点作为主节点,更换一个视图代表更换了一个主节点。
- 检查点协议: 标记已经完成共识的检查点,用于垃圾回收
其它算法
- 摘要算法 防止消息篡改
- 非对称密钥签名、验签算法。仅仅在 视图切换view-change和new-view消息使用
- 消息验证码算法MAC 提升签名性能,对比非对称密钥签名MAC算法性能更高。防止消息篡改,并能验证数据发送方。
名词解释
- 主节点primary:负责对请求进行排序,转发客户端请求
- 副节点backupa:负责验证请求是否有效,并和主节点达成共识
- 客户端client:发送请求,要求节点执行某个操作
- 序列号sequence number :对请求进行编号,用于请求排序
- 视图view :一个主节点和多个备份节点形成一个视图,一个节点作为一个新的主节点代表一个一个视图的开始,更换节点会更换视图。
- 检查点checkpoint: 某个序列号对应的请求收到了超过2/3的节点确认,称为一个检查点
基本流程
其中n为总节点个数,f为拜占庭节点个数
- 客户端发送请求给主节点,要求执行操作
- 主节点接受客户端请求,进入预准备PRE-PREPARE阶段,在本地日志记录PRE-PREPARE消息,并广播PRE-PAREPARE到其它所有副节点。
- 副节点收到主节点PRE-PAREPARE节点,对消息进行验证。验证通过后,进入准备PREPARE阶段,广播PREPARE消息给其它所有节点
- 每个节点收到其它节点PREPARE消息后,进行消息验证,当接收到2f+1个验证通过的PREPARE消息(包含自己),在本地日志中记录,PRE-PREPARE、PREPARE消息,进入提交COMMIT阶段,并广播COMMIT消息给所有节点
- 每个节点收到其它节点COMMIT消息后,进行验证,当接收到2f+1个验证通过的COMMIT消息(包含自己),在本地日志中记录,COMMIT消息。广播REPLY消息发送给客户端。
- 客户端收到节点REPLAY消息,进行验证,客户端阻塞等待,当收到f+1个REPLAY消息并验证通过,则返回结果。
三阶段提交协议
初始化阶段,主节点p
由 p = v mod n
计算得出。
结合下图说明三阶段提交协议,pbft达成共识的过程。
下图中C
代表客户端,0
,1
,2
,3
分别代表3个节点。其中0
为主节点,1
,2
为正常工作节点,3
为异常宕机节点。
在pbft算法中,客户端读写请求都是首先发送给主节点,主节点发送请求给其它节点进行共识。
结合下3阶段协议图,按阶段进行说明
REQUEST
客户端c
在时间t
,执行操作o
,客户端将消息组装为REQUEST消息并对消息进行签名σc
,格式<REQUEST,o,t,c>σc
,将消息发送给主节点。
PRE-PREPARE
主节点收到客户端请求,进入预准备PRE-PREPARE阶段。
基于当前视图v
对请求分配编号n
,并对客户端请求m
(可以理解为<REQUEST,o,t,c>σc
)进行摘要,摘要结果为d
,封装PRE-PREPARE并对消息进行签名,签名记为σp
。
格式为<PRE-PREPARE,v,n,d>σp
。
记录PRE-PREPARE消息到本地日志中,记录客户端请求m
到本地日志中,广播PRE-PREPARE消息以及客户端请求m
,<<<PRE-PREPARE,v,n,d>σp>,m>
给其它副节点。
PRE-PREPARE阶段,主要对请求分配编号对请求进行排序。
PREPARE
副节点收到主节点PRE-PAREPARE消息,对消息进行验证。满足一下条件表示验证通过,验证不通过则会拒绝该消息
- 副节点当前视图也是
v
,表明PRE-PREPARE消息发起者确实是主节点 - 客户端请求
m
的摘要是PRE-PREPARE中的d
,PRE-PREPARE签名σp
,验签正确。客户端请求m
中客户端验签正确保证消息未被篡改(如客户端消息被截获,主节点作恶篡改消息)且由客服端发送。 - 副节点当前视图
v
中,序列号n
未被使用过。即该节点未接受过编号同样为n
,但请求不为m
的PRE-PAREPARE消息 - 序列号
n
处于高低水位值之间,即n∈[h,H]
,h
为低水位,H
为高水位,一般H=h+k
,k
为常量。这个限制是为防止某个主节点作恶,使用过大序列号恶意消耗序列号空间。 每个副节点验证PRE-PREPARE通过后,进入PREPARE阶段,组装PREPARE消息,格式为<PREPARE,v,n,d,i>σi
。v
,n
,d
为当前视图编号,请求序列号,客户端消息摘要,i
为当前节点编号σi
为当前节点i对PREPARE消息的签名。
副节点将PREPARE消息记录到本地日志中,并广播PREPARE给其它所有节点。
副节点在PREPARE阶段,表明当前节点接受了主节点的提议,同意在视图v
中把序列号n
分配给客户端请求m
。
COMMIT
每个节点(包括主节点)收到其它节点的<PREPARE,v,n,d,i>σi
PREPARE消息,会进行验证。
- 客户端请求
m
的摘要是PREPARE中的d
,PREPARE消息的签名σi
验证正确 - 当前节点视图编号为
v
- 在视图
v
中,请求序列号n
处于高低水位值之间,即n∈[h,H]
每个节点当接收到2f+1(包含自己)个验证通过的PREPARE消息后,节点进入COMMIT阶段,组装COMMIT消息,格式为<COMMIT,v,n,D(m),i>σi
,v
,n
,D(m)
为当前视图编号,请求序列号,客户端消息摘要,i
为当前节点编号σi
为当前节点i对COMMIT消息的签名。
节点将COMMIT消息记录到本地日志中,并广播给其它节点。
REPLY
注意REPLY不属于3阶段提交中的3阶段。3阶段是指PRE-PREPARE,PREPARE,COMMIT 3个阶段。
每个节点收到其它节点的<COMMIT,v,n,D(m),i>σi
后,会进行验证。
- COMMIT消息的摘要
D(m)
、签名σi
验证正确 - 当前节点视图编号为
v
- 在视图
v
中,请求序列号n
处于高低水位值之间,即n∈[h,H]
每个节点当接收到2f+1(包含自己)个验证通过的COMMIT消息,会执行客户端消息m
中的操作o
,进入REPLAY阶段,组装REPLAY消息,格式为<REPLAY,v,t,c,i,r>σi
,v
为当前视图编号,t
为客户端发送请求时间戳,c
为client指发送给哪个客户端,i
为当前节点编号,r
为节点i执行操作的回复,σi
为节点对消息的签名。
客户端接受处理
客户端收到节点的<REPLAY,v,t,c,i,r>σi
REPLAY消息,
客户端接受到f+1个REPLAY消息后,验证REPLAY消息签名σi
验证正确,t
时间戳等于发送REQEUST消息时间戳,所有r
一致,会返回结果。
异常情况
- 主节点作恶(如:不转发客户端消息、篡改客户端消息、对不同节点发送不同客户端消息),会引发视图变更,替换主节点
- 主节点故障,引发视图变更,替换主节点
- 客户端超时未收到响应,客户端会广播请求给所有的节点。节点收到重复请求,如果请求已经处理过,会将请求再次发送给客户端。如果请求未处理过,副节点会将请求转发给主节点。
2f+1和f+1
在COMMIT阶段和REPLAY阶段,都要受到2f+1个节点响应才进入下一阶段,而客户端仅仅等待f+1个响应就返回。
在COMMIT阶段和REPLAY阶段,拜占庭容错能支持f作恶节点和f个宕机节点同时存在,当收到2f+1个响应时,最多有f个作恶节点,其它f+1节点为正常节点,根据多数派原则少数服从多数,能判断出正确的响应,因此需要是2f+1。
在客户端响应阶段,客户端收到f+1个响应,最坏有f个作恶节点,另外1个肯定是正常节点,1个正常节点发送了响应消息,说明该节点已经完成了COMMIT阶段的共识,已有2f+1个节点完成COMMIT,执行了客户端的请求操作。因此客户端只需要f+1个响应即可。
检查点协议
PBFT通过三阶段协议对请求达成共识,每一阶段都会进行本地日志记录,如果不进行垃圾回收,删除旧日志记录,会增加系统存储资源负担。PBFT算法设计了检查点协议丢弃本地消息日志中的就消息。
根据前面的三阶段协议,客户端收到某个请求执行结果的时候,表明该请求至少已经被f+1个节点提交过了,这个时候可以删除旧消息了,但是每次收到执行结果时去删除旧消息,会比较浪费资源。因此PBFT采用周期性清除,周期记为T
,比如当请求序号整除100时进行清除(批量清除的意思)T为每隔100个消息进行清除。
检查点协议工作流程
当周期T到达时,每个节点会组装CHECKPOINT消息,消息格式为<CHECKPOINT,n,d,i>σi
,n
为最近一次请求的序列号,d
为最近一次请求的客户端请求摘要,σi
为CHECKPOINT消息的签名。节点广播消息给其它节点。
每个节点收到2f+1个CHECKPOINT消息验证签名通过后,表明n是一个稳定的检查点stablecheckpoint。
每个节点把共识后的CHECKPOINT消息记录到本地日志。
当一个检查点证明有效、稳定后,节点会把本地消息日志中客户端请求序列小于或等于n的消息(PRE-PREPARE,PREPARE,COMMIT)都删除掉,同时也会删掉旧的检查点和CHECKPOINT消息。
高低水位线
最新的稳定检查点statblecheckpoint即为低水位线记为h
,高水位线记为H
,H=h+k
,k
为一个可设置数的常量。若客户端请求序号为n,n>H时请求会被阻塞,直至高低水位线更新。
当稳定检查点更新时,高低水位线也会进行更新。
其中k
要比checkpoint的周期T
要大,比如T
为100,k
可以为200
视图更换协议
PBFT使用视图变更协议允许主节点出故障情况下仍然能够正常运行,保证系统活性。视图变更协议是通过超时机制触发的。
视图变更超时机制
论文中提到:当节点收到一个有效的请求,而且这个请求没有被执行过的时候,当前视图如果没有启动计时器则启动一个新的计时器,等待超时,超时时间内如果在接收到这个请求则停止计时器。
解释
客户端发送了请求给主节点,客户端等待响应超时。
客户端重新发送了请求给所有副本,副本验证客户端请求通过,并判断这个请求自己并未执行过,说明主节点未转发客户端请求给副节点。
副节点针对当前视图启动一个新定时器,如果当前视图还没有定时器,并将请求转发给主节点。
在超时时间内,若主节点重新将请求触发3阶段协议转发给副节点,定时器停止。
若在超时时间内,副节点未收到重新发送的请求,启动视图更换协议,请求视图变更,更换主节点。
注意:在视图变更期间,除了CHECKPOINT,VIEW-CHANGE和NEWVIEW消息外,副节点不会接受其它消息。
视图变更流程
每个副节点在视图v
计时器超时,副节点会提议系统所有节点切换到下一个视图v+1
,此时除了CHECKPOINT,VIEW-CHANGE和NEWVIEW消息外,副节点不会接受其它消息。副节点组装VIEW-CHANGE消息,消息格式为<VIEW-CHANGE,v+1,n,C,P,i>σi
,其中n
为最后一个稳定检查点statblecheckpoints
,C
为证明该稳定检查点s
的CHECKPOINT消息集合。P
是Pm
的集合,m
是副节点i中序列号大于n的PRE-PAREPARE和PREPARE的客户端请求m。Pm
包含以下信息
- 不包含原始请求m的PRE-PREPARE消息
- 由2f个其它副节点签名的和PRE-PREPARE消息匹配的PREPARE消息。(匹配是指PREPARE消息签名有效,在视图中分配的请求序列号一致,客户端请求摘要相同)
当新的主节点(由
p = v+1 mod n
计算得到)在v+1视图,收到2f个节点发送的有效的切换视图为v+1的VIEW-CHANGE消息,会向其他节点发送NEW-VIEW消息,消息格式<NEW-VIEW,v+1,V,O>σp
。
V
是新的主节点p收到的有效的视图标号为v+1的VIEW-CHANGE消息集合。
O
是上一个视图中未处理完的有效PRE-PREPARE消息集合。
注意:假设O
中包含的PRE-PREPARE消息编号范围为min~max,min为V
中的statabelcheckpoint,若max>min则组装新消息<PRE-PREPARE,v,n,d>σp
。若mx=min,组装消息<PRE-PREPARE,v,n,d(null)>σp
。
其它节点收到NEW-VIEW消息后,验证消息有效性,切换视图到v+1
,对O
中的PRE-PREPARE消息重新执行一遍三阶段协议。