引言
在分布式系统中,多个节点需要就某个值达成一致——这就是共识(Consensus)问题。 共识算法是构建高可用、强一致分布式系统的基石,ZooKeeper 的 Zab、etcd 的 Raft、 Google 的 Chubby 都是其工业实现。
本文将梳理从 Paxos 到 Raft 的演进脉络,重点解释核心概念和设计取舍。 如果你从未接触过共识算法,这篇文章可以作为良好的起点。
什么是共识问题
想象一个由三台服务器组成的集群。用户发来一条写请求:“将 X 的值设为 1”。 如果所有服务器都正常,一切好办。但如果其中一台宕机或网络中断了呢? 我们仍然需要确保存活的服务器对该值达成一致。
这就是 共识算法 要解决的核心问题:即便存在故障, 集群中的多数节点也能对某个值达成一致,并且这个一致的结果是确定的、可被其他节点获知的。
FLP 不可能定理告诉我们:在异步通信模型中,即使只有一个节点可能故障, 也不存在一个确定性算法能保证所有非故障节点达成共识。 因此实际系统中的共识算法都做了不同程度的假设(如部分同步模型)。
Paxos:理论奠基
Lamport 在 1998 年提出了 Paxos 协议。这是共识算法的鼻祖, 后来的几乎所有共识算法都在 Paxos 的基础上做了简化和改进。
Paxos 中的核心角色有三个:
- Proposer(提议者):提出议案,争取让某个值被接受
- Acceptor(接受者):对议案进行投票
- Learner(学习者):学习已被多数接受的最终结果
经典的 Paxos 分两个阶段执行:
Phase 1:Prepare 阶段
Proposer 选择一个提案编号 N,向所有 Acceptor 发送 Prepare(N) 请求。 Acceptor 承诺不再接受编号小于 N 的提案,并返回已接受的最高编号提案(如果有的话)。
Phase 2:Accept 阶段
如果 Proposer 收到多数 Acceptor 的响应,它就发送 Accept(N, Value) 请求。 Acceptor 接受该提案,除非已经响应过更高编号的 Prepare。
// Paxos 核心伪代码
class Acceptor:
min_proposal = 0
accepted_proposal = nil
accepted_value = nil
def receive_prepare(n):
if n > min_proposal:
min_proposal = n
return (accepted_proposal, accepted_value)
return reject
def receive_accept(n, value):
if n >= min_proposal:
min_proposal = n
accepted_proposal = n
accepted_value = value
return ok
return reject
Paxos 的正确性早已被严格证明,但它存在几个问题:
- 难以理解:Lamport 用了一个寓言故事来解释,反而让事情变得更复杂
- Multi-Paxos:如何将单轮 Paxos 扩展为日志复制协议,原始论文没有给出清晰的指导
- 实践中的坑:活锁、Leader 选举的额外复杂性等
Raft:可理解的共识
Ongaro 在 2014 年的博士论文中提出了 Raft。它的目标是——让共识算法容易被理解, 同时在实际系统中易于实现。
Raft 的核心创新在于将共识问题分解为三个相对独立的子问题:
1. 领导者选举(Leader Election)
在 Raft 中,节点有三种状态:Leader、Follower 和 Candidate。 系统正常运行时有且仅有一个 Leader,所有的写操作都由 Leader 处理。
Leader 定期发送心跳(Heartbeat)来维持权威。如果 Follower 在超时时间内未收到心跳, 它会转换为 Candidate 并发起选举。收到多数节点投票的 Candidate 成为新 Leader。
选主超时是一个关键参数。Raft 使用随机化超时(通常是 150-300ms) 来避免多个 Candidate 同时发起选举导致的选票瓜分。
2. 日志复制(Log Replication)
Leader 收到客户端请求后,将操作追加到自己的日志中,然后并行发送 AppendEntries RPC 给所有 Follower。 当多数节点确认写入后,Leader 将该条目提交(commit),并通知 Follower。
// Raft 日志条目结构
type LogEntry struct {
Index uint64 // 日志索引
Term uint64 // 任期编号
Command []byte // 操作指令
}
// 日志复制流程
func (l *Leader) Replicate(entry LogEntry) {
l.log = append(l.log, entry)
for _, peer := range l.peers {
go l.sendAppendEntries(peer)
}
// 等待多数确认
if l.quorumAcknowledged(entry.Index) {
l.commitIndex = entry.Index
l.applyEntry(entry)
}
}
3. 安全性(Safety)
Raft 通过几条约束来保证安全性(State Machine Safety):
- Election Safety:每个任期最多只有一个 Leader
- Leader Append-Only:Leader 不会覆盖或删除自己的日志条目
- Log Matching:如果两个日志条目有相同的 Index 和 Term, 那么它们之前的所有日志都一致
- Leader Completeness:被提交的日志条目最终一定存在于后续 Leader 的日志中
Raft vs Paxos
两者的核心区别:
| 维度 | Paxos | Raft |
|---|---|---|
| 可理解性 | 困难 | 相对容易 |
| 角色 | Proposer/Acceptor/Learner | Leader/Follower/Candidate |
| Leader 角色 | 非必须 | 强 Leader |
| 成员变更 | 复杂 | Joint Consensus |
| 日志复制 | 隐式 | 显式,AppendEntries RPC |
工业实践
以下是共识算法在工业界的典型应用:
- etcd:使用 Raft 实现的分布式 KV 存储,Kubernetes 的核心组件
- Consul:HashiCorp 的服务发现和配置工具,基于 Raft
- ZooKeeper:使用 Zab 协议(类 Paxos),Hadoop 生态的基石
- TiKV:分布式 KV 数据库,使用 Raft 实现多副本一致性
总结
Paxos 奠定了共识算法的理论基础,Raft 则在可理解性和工程实践上迈出了重要一步。 理解这两个算法,对于学习分布式系统来说是必不可少的。
值得一提的是,共识算法虽然看上去抽象,但它的设计哲学——将复杂问题分解为可管理的子问题、 用合理的假设简化模型——是值得所有系统设计者借鉴的。