写在开头

最近在看分布式共识领域的经典算法——PBFT的原论文,对其中三个阶段的设计原因不甚了然,在查阅了许多资料以及讨论之后终于对其有比较清晰的认识,所以将其整体为这篇文章,希望能对读者有所帮助。

PBFT的三阶段共识

这篇文章假设读者已经看过PBFT原论文或者其他介绍PBFT的文章,对完整的共识过程不再赘述,只放一张图以供快速回顾。当然除了三阶段共识过程,日志和检查点等内容也需要了解。

fabric中为什么使用pbft_新视图

为什么需要三个阶段

要讨论这个问题,我们需要先看看视图切换的一些细节。

视图切换

视图切换用来解决主节点失效的问题。从节点每收到一个主节点的消息就会重置计时器,计时器超时说明主节点可能已经故障了(或者是恶意节点),此时从节点广播一个view change消息,其中包含最近的检查点和2f+1个节点对该检查点的共识证据C、序号在最近检查点之后已经达到prepared状态的消息的集合P。
经过一段时间,新视图中的主节点收到了大于等于2f+1个view change消息(包括自己),则它构造一个new view消息,其中包含V和O,V是2f+1个view change消息的集合,O由以下规则生成:

  1. 从V中所有的C里选择最近的检查点,将该检查点的序号作为min_s
  2. 从V中所有的P里选择序号最大的消息,将该序号作为max_s
  3. 对于 (min_s,max_s] 这一区间内的每一个序号n,case 1:如果能够在V中所有的P里找到至少一个序号与之相同的消息,则将(v+1,n,d)作为新视图里的一条pre-prepare消息;case 2:如果V中没有任何一个P包含序号与之相同的消息,则同样创建一条pre-prepare消息,只不过这条消息的d是空的,收到这样消息的节点会按正常的三阶段共识算法进行共识,只不过最后不执行任何操作。

生成new view消息后,主节向从节点广播,从节点收到后,对消息进行验证(包括重新运行生成O的算法以验证O的正确性),验证正确后从节点对O中每一条消息广播prepare消息。

也就是说,上一个视图中prepared的消息会在新视图中被重放,从pre-prepare开始重新共识。
可能有人会问,如果有的消息在某些节点上已经完成commit阶段、向client发送REPLY了呢?这些节点会正常参加新视图中该消息的共识,只不过在执行阶段,通过本地的记录它会发现自己之前已经执行过这一请求了,所以就什么也不做。

假如没有commit

现在我们开始假设,如果共识只有两个阶段,节点A中某个序号n对应的消息m收到了来自其他2f个节点的prepare消息,进入prepared状态,因为没有commit阶段,节点A直接执行m并向client发送REPLY。此时网络中只有少数节点跟A一样对消息m达到prepared状态,其他节点仍然在等待足够的prepare消息。
这时节点们发现已经很久没有收到主节点发来的消息了,所以他们将本地的消息打包,广播了一条view change消息。根据原文,view change过程中节点是无法接收到与view change无关的消息的,自然也无法接收prepare。
当新视图中主节点准备生成V时,它或许可以发现某个序号n只在少数节点中达到了prepared状态,它可以选择假装没看见,将其他2f+1个节点的P打包进V里,并由此生成O并广播。这样子新视图里n对应的将是一个空消息。当然主节点可以做得更绝,它可以发出(n,m’)的pre-prepare消息,这样不明就里的其他节点就会被欺骗,最后对m’达成共识。
到这一步可以发现,如果没有commit阶段,两个视图中n可能对应不同的消息m和m’,这就违背了PBFT设计的原则,因为PBFT要求即使是跨视图,n与m的匹配也应该是唯一的。

commit保障了什么

为什么一个节点在收到2f+1个commit消息后可以放心地执行请求?因为即使发生视图切换,由于有2f+1个节点已经对(n,m)达到了prepared状态,下一视图中的主节点构造的O中也必须包含(n,m),所以前后视图执行的请求是一致的。

其他问题

  1. 构造O的步骤中什么情况下会出现case 2?
    假设在视图v里有4个节点:a节点对消息123达成prepared, b节点对234达成prepared,c节点是789,d节点是89。
    这样最新的checkpoint对应的n即min-s我们假设是0,则最大的n应该是9,所以56消息就是没有的,这时就要共识空消息了。