容错性一直是分布式系统中需要处理的问题,而拜占庭将军问题被认为是容错性问题中最难的问题类型之一。分布式账本作为一个分布式系统,也面临该问题,著名的比特币则是采用的 PoW 来应对该问题,类似的还有以太坊中的 PoS 等。本文则简单介绍了另一种解决方案,即实用拜占庭容错算法(Practical Byzantine Fault Tolerance) 该算法的衍生版本也在一些分布式账本系统中得到应用,比如 Neo。
关于拜占庭将军问题的简单描述如下:
一组拜占庭将军分别各率领一支军队共同围困一座城市。为了简化问题,将各支军队的行动策略限定为进攻或撤离两种。因为部分军队进攻部分军队撤离可能会造成灾难性后果,因此各位将军必须通过投票来达成一致策略,即所有军队一起进攻或所有军队一起撤离。因为各位将军分处城市不同方向,他们只能通过信使互相联系。在投票过程中每位将军都将自己投票给进攻还是撤退的信息通过信使分别通知其他所有将军,这样一来每位将军根据自己的投票和其他所有将军送来的信息就可以知道共同的投票结果而决定行动策略。
以上内容摘自 维基百科-拜占庭将军问题。
在早期 1982 年的论文中提出的算法,并不具备非常高的实用性。因为为了达成共识,节点需要观察其余节点对消息的反应,这就需要消息需要以一个类似“递归”的形式在不同的轮次中的各个节点间传递。如果需要进一步了解其中的细节,除了参考论文实现外,还可以参考这篇文章 Mark Nelson - The Byzantine Generals Problem 。
而实用拜占庭容错算法,则提供了一个相对于早期算法而言,更易于实现、更高效的算法。
实用拜占庭容错算法,其实是将拜占庭将军问题简化成以下两点来考虑:
当系统只存在问题 1 时,该问题的其实就回到类似内网中的节点同步问题。Viewstamped Replication 或者 paxos 等算法都提供了对该问题的解决方案。而实用拜占庭容错算法,实际是在 Viewstamped Replication 算法上的升级、额外提供了对问题 2 的解决方案。
实用拜占庭容错算法,可以粗略的描述为:
在实用拜占庭容错算法中,将每次的共识称之为 view,在每次共识中,都会选择其中一个节点作为 primary,其余节点为 backups。每次的共识,即 view 都有一个自增的 id。当次 view 中的 primary 的选取规则为 P = v mod |R|
v 表示 view id,|R|
表示节点总数,R 为 replica 的简写。
算法工作流程为:
实用拜占庭容错算法,对共识节点的数量做了如下要求,假设节点总数为 n,恶意节点数为 f,n = 3f + 1
为可以容纳 f 个恶意节点的最少总数。下面对这个数值关系做更加进一步的说明。
首先要知道,系统中采用的是少数服从多数的原则。当系统中只存在问题 1 时,那么当一个操作到达时,最优情况下,会接受到 n 个节点的反馈,也就是 f 为 0。当 f 不为 0 时,则可以接收到的反馈数量为 n-f
,此时为了得到共识结果,必须满足 n - f > f
。如果进一步了解 Viewstamped Replication 算法的话,会发现这一步的数值关系和其中要求的一样。这里的容错的 f 表示的是 fail-stop 节点数。
当引入的问题 2 后,接收到的 n - f
个反馈中,还可能存在 f 个反馈是恶意的,那么此时为了达成共识,则要求 n - f - f > f
,即 n > 3f
。
当我们在实现性地部署一些使用拜占庭容错算法实现的软件时,文档中会建议采用 4 个节点来做实验,并且让建议关掉其中 1 台,观察到系统仍然可以完成共识。这里实际上会让人产生一些错误的映像,认为 4 个节点只能容纳 1 个节点出错。其实这个实验仅仅展示了系统可以容纳 1 台 fail-stop 节点。在容纳一台 fail-stop 节点的同时,仍然可以存在另一台 malicious 节点,换句话说,完整的实验是,关掉一台节点,模拟 fail-stop,于此同时,让一台节点进行错误的反馈,最终发现系统仍能达成共识。