拜占庭将军问题与 PBFT 算法
故事发生在公元 9 世纪。
拜占庭帝国(东罗马帝国)有一支军队,分布在多个城市,由多位将军指挥。
将军们需要达成共识:进攻还是撤退?
但问题是:这些将军中有叛徒。
叛徒会传递虚假消息,会阻止其他将军达成共识。
这就是著名的拜占庭将军问题——分布式系统领域最经典的思想实验之一。
拜占庭将军问题
问题描述
n 位将军,围攻一座城市
只有当多数将军同时进攻,才能获胜
将军们只能通过信使通信
问题:如何让所有忠诚的将军达成一致的行动计划?
约束:
1. 忠诚将军的行动是一致的
2. 叛徒可以发送任意消息
3. 叛徒可以干扰但不破坏算法核心结论
**如果存在 m 个叛徒,则至少需要 n >= 3m + 1 位将军,才能容忍 m 个恶意节点。
为什么是 3m + 1?
场景:m = 1(1 个叛徒),需要 n >= 4
将军:A(忠诚)、B(忠诚)、C(叛徒)、D(忠诚)
1. A 发消息给所有人:「进攻」
2. C(叛徒)给 A 发「进攻」,给 B、D 发「撤退」
3. A 收到:进攻=2,撤退=1 → 进攻
4. B 收到:进攻=1,撤退=2 → 撤退(与 A 不一致!)
问题:A 和 B 做出了不同的决定
解决:需要 4 个忠诚将军 + 1 个叛徒 = 5 位将军
这样:
- 消息比例:4 忠诚 vs 1 叛徒
- 多数派是忠诚的为什么是 3m + 1 的数学推导
假设系统能容忍 m 个恶意节点
总节点数 n,诚实节点 f,恶意节点 m
要达成共识,需要多数派确认:n/2 + 1
最坏情况:
- f 个诚实节点
- m 个恶意节点
- 共识需要 (f + m)/2 + 1 票
诚实节点必须占多数,才能保证共识正确:
f > m
同时,n = f + m
由 f > m 得:
f > (n - f)
2f > n
f > n/2
所以 n < 2f = 2(n - m)
n < 2n - 2m
2m < n
n > 2m
即:n >= 2m + 1
但这是保证活性的最低要求。为了保证安全性:
n >= 3m + 1PBFT:实用拜占庭容错
PBFT(Practical Byzantine Fault Tolerance)是由 Miguel Castro 和 Barbara Liskov 在 1999 年提出的算法。
目标:在异步网络中,实现能容忍恶意节点的共识。
三阶段协议
PBFT 使用三阶段协议:Pre-prepare、Prepare、Commit
java
/**
* PBFT 算法简化实现
*
* 三阶段:Pre-prepare、Prepare、Commit
*/
public class PBFT {
private final int f; // 能容忍的恶意节点数
private final View view; // 当前视图
private final Map<String, PrePrepared> prePreparedLog = new HashMap<>();
private final Map<String, Prepared> preparedLog = new HashMap<>();
private final Map<String, Committed> committedLog = new HashMap<>();
/**
* 客户端请求
*/
public void handleRequest(ClientRequest request) {
// 1. 主节点(Primary)验证请求
if (!isPrimary()) {
throw new NotPrimaryException();
}
// 2. 广播 Pre-Prepare 消息
broadcastPrePrepare(request);
}
/**
* Phase 1: Pre-Prepare
*
* 主节点:
* - 为请求分配序列号 n
* - 签名 Pre-Prepare 消息
* - 发送给所有备份节点
*/
public void broadcastPrePrepare(ClientRequest request) {
long viewNumber = view.getViewNumber();
long sequenceNumber = getNextSequenceNumber();
PrePrepareMessage message = new PrePrepareMessage();
message.setView(viewNumber);
message.setSequence(sequenceNumber);
message.setDigest(hash(request.getOperation()));
message.setRequest(request);
// 签名并广播
signAndBroadcast(message);
}
/**
* 备份节点处理 Pre-Prepare
*/
public void onReceivePrePrepare(PrePrepareMessage message) {
// 验证消息
if (!verifyMessage(message)) {
return;
}
// 检查视图号和序列号
if (message.getView() != view.getViewNumber()) {
return;
}
if (!isValidSequenceNumber(message.getSequence())) {
return;
}
// 检查请求摘要
if (!checkDigest(message.getDigest())) {
return;
}
// 记录 Pre-Prepared 状态
prePreparedLog.put(getKey(message), new PrePrepared(message));
// 广播 Prepare 消息
broadcastPrepare(message);
}
/**
* Phase 2: Prepare
*
* 所有节点:
* - 收到 Pre-Prepare 后
* - 广播 Prepare 消息
* - 等待 2f 个 Prepare 确认
* - 进入 Prepared 状态
*/
public void broadcastPrepare(PrePrepareMessage prePrepare) {
PrepareMessage message = new PrepareMessage();
message.setView(prePrepare.getView());
message.setSequence(prePrepare.getSequence());
message.setDigest(prePrepare.getDigest());
message.setNodeId(nodeId);
// 签名并广播
signAndBroadcast(message);
}
/**
* 检查是否收到足够多的 Prepare
*/
public boolean checkPrepareMessages(PrePrepareMessage prePrepare) {
String key = getKey(prePrepare);
Prepared prepared = preparedLog.get(key);
// 需要收到 2f 个 Prepare 消息(包括自己的)
// f 个恶意节点 + f 个诚实节点 + 自己 = 2f + 1
return prepared.getCount() >= 2 * f;
}
/**
* Phase 3: Commit
*
* 进入 Prepared 状态后:
* - 广播 Commit 消息
* - 等待 2f + 1 个 Commit 确认
* - 执行请求
*/
public void broadcastCommit(PrePrepareMessage prePrepare) {
CommitMessage message = new CommitMessage();
message.setView(prePrepare.getView());
message.setSequence(prePrepare.getSequence());
message.setDigest(prePrepare.getDigest());
message.setNodeId(nodeId);
signAndBroadcast(message);
}
/**
* 检查是否收到足够多的 Commit
*/
public boolean checkCommitMessages(PrePrepareMessage prePrepare) {
String key = getKey(prePrepare);
Committed committed = committedLog.get(key);
// 需要收到 2f + 1 个 Commit 消息
return committed.getCount() >= 2 * f + 1;
}
/**
* 执行请求并返回结果
*/
public byte[] executeRequest(ClientRequest request) {
// 状态机更新
byte[] result = stateMachine.execute(request.getOperation());
// 返回结果给客户端
return result;
}
}View Change 机制
当主节点故障时,需要切换视图:
java
/**
* View Change:当主节点故障时触发
*/
public class ViewChange {
/**
* 触发条件:节点等待 Pre-Prepare 超时
*/
public void onTimeout() {
viewNumber++;
// 1. 停止处理当前视图的消息
stopProcessing();
// 2. 收集 Checkpoint 证明
List<CheckpointMessage> checkpoints = collectCheckpoints();
// 3. 构造 View-Change 消息
ViewChangeMessage message = new ViewChangeMessage();
message.setView(viewNumber);
message.setNodeId(nodeId);
message.setCheckpoints(checkpoints);
message.setPreparedCertificates(preparedCertificates);
// 4. 发送给新主节点
sendToPrimary(message);
}
/**
* 新主节点处理 View-Change
*/
public void onReceiveViewChange(ViewChangeMessage message) {
// 收集 2f 个 View-Change 消息
viewChangeMessages.add(message);
if (viewChangeMessages.size() >= 2 * f + 1) {
// 启动新视图
startNewView(message.getView());
}
}
/**
* 启动新视图
*/
public void startNewView(long newViewNumber) {
view.setViewNumber(newViewNumber);
// 1. 从 View-Change 消息中恢复状态
recoverState();
// 2. 重新广播缺失的消息
rebroadcastMissingMessages();
// 3. 开始处理新视图的消息
startProcessing();
}
}PBFT vs Raft
| 维度 | PBFT | Raft |
|---|---|---|
| 容错类型 | 拜占庭故障(恶意节点) | 崩溃故障(节点宕机) |
| 节点数量 | n >= 3m + 1 | 奇数节点,多数派即可 |
| 通信复杂度 | O(n²) | O(n) |
| 性能 | 较低 | 高 |
| 实现难度 | 高 | 中等 |
| 适用场景 | 联盟链、敏感系统 | 通用分布式系统 |
PBFT 的局限
1. 通信复杂度高
- 每条消息需要 O(n²) 次通信
- 不适合大规模节点
2. 节点数量受限
- 节点越多,性能越差
- 通常只支持几十个节点
3. 同步假设
- PBFT 假设网络不会无限延迟
- 如果网络真的异步,性能无法保证区块链中的 BFT 变体
区块链继承了 BFT 的思想,但做了大量改进:
PoW(工作量证明)
比特币的共识算法
特点:
- 不需要事先知道节点
- 任何人都可以加入
- 通过算力竞争达成共识
优点:
- 完全去中心化
- 无需信任任何节点
缺点:
- 耗能巨大
- 确认时间长PoS(权益证明)
以太坊 2.0 的共识算法
特点:
- 根据持币量选择验证者
- 验证者质押代币作为保证金
- 作恶会被惩罚(罚没质押)
优点:
- 能耗低
- 确认时间短
缺点:
- 富者愈富
- 可能存在 Nothing at Stake 问题BFT 变体在联盟链中的应用
Hyperledger Fabric、PBFT、Raft 等
特点:
- 节点数量有限且已知
- 需要许可才能加入
- 性能高,安全性好
适用场景:
- 企业联盟链
- 私有链
- 需要高性能和确定性的场景面试追问方向
Q1:PBFT 为什么通信复杂度高?
因为 PBFT 需要三个阶段的确认(Pre-prepare、Prepare、Commit),每个阶段都需要节点之间相互通信:
Pre-prepare:主节点 → 所有备份节点
Prepare:所有节点 → 所有节点(n * (n-1) 次)
Commit:所有节点 → 所有节点(n * (n-1) 次)
总复杂度:O(n²)Q2:PBFT 适合什么场景?
- 联盟链:节点数量有限,性能要求高
- 敏感系统:需要容忍恶意节点(如区块链)
- 私有链:节点可信但需要容错
不适合:
- 公有链(节点太多)
- 追求极致性能(通信开销大)
Q3:Raft 能容忍恶意节点吗?
不能。Raft 只能容忍节点崩溃(Crash Fault),不能容忍恶意节点(Byzantine Fault)。
如果 Raft 节点发送虚假消息,会导致系统不一致。
总结
拜占庭将军问题和 PBFT 算法揭示了分布式系统的深层挑战:
- 拜占庭将军问题:n >= 3m + 1 才能容忍 m 个恶意节点
- PBFT:三阶段协议实现拜占庭容错
- 局限性:O(n²) 通信复杂度,节点数量受限
- 工程选择:根据场景选择 PBFT、Raft 或区块链共识
"拜占庭将军问题教会我们:在不可信的环境中达成共识,需要付出代价。PBFT 用通信复杂度换取安全性,这是一笔值得的交易。"
