Paxos和Raft是分布式数据库中最核心的两种共识算法,它们在工程实现上的差异远比论文描述的要大得多。简单来说,Paxos追求的是理论上的最优解,但工程落地极其复杂;Raft则反过来,牺牲了一部分理论灵活性,换取了工程上的可理解性和可实现性。如果你正在做分布式数据库选型或者自研共识模块,搞清楚这两者在实际代码层面的差异,比读十遍论文都管用。
从本质上讲,Paxos是一个"协议族",包含Basic Paxos、Multi-Paxos、Fast Paxos等多个变体,而Raft是一个单一的、完整的共识协议。这个根本区别直接决定了工程实现的复杂度。Paxos在工业界的代表是Google的Chubby、Spanner,Raft的代表是TiKV、CockroachDB、etcd。下面我从多个维度把它们的工程差异掰开了讲。
一、Leader选举机制的工程差异
Paxos本身并不强制要求Leader,Basic Paxos中每个提案都可以由任意节点发起,这在工程上意味着每次写操作都要走完整的两阶段流程,性能极差。所以实际工程中几乎都用Multi-Paxos,也就是先选出一个Leader,后续操作由Leader统一协调。但问题在于,Multi-Paxos的Leader选举并没有被Paxos协议本身严格定义,各家实现方式不同,有的用外部选举服务,有的用Paxos自身的变种来做。
Raft则把Leader选举作为协议的核心组成部分,白纸黑字写得清清楚楚。每个节点只有三种状态:Leader、Follower、Candidate。选举超时后节点变成Candidate,给自己投一票,然后向其他节点请求投票。收到多数票就成为Leader。这个过程在代码里就是一个状态机,逻辑非常明确。
// Raft选举核心逻辑伪代码
func (rf *Raft) startElection() {
rf.state = Candidate
rf.votedFor = rf.me
rf.votesGranted = 1
for each peer in rf.peers {
go rf.requestVote(peer)
}
rf.electionTimer = randomTimeout()
}
func (rf *Raft) requestVote(peer int) {
args := RequestVoteArgs{
Term: rf.currentTerm,
CandidateId: rf.me,
LastLogIndex: rf.log.lastIndex(),
LastLogTerm: rf.log.lastTerm(),
}
reply := peer.RequestVote(args)
if reply.VoteGranted {
rf.votesGranted++
if rf.votesGranted > len(rf.peers)/2 {
rf.becomeLeader()
}
}
}工程上的关键差异在于:Raft的选举有明确的任期(Term)概念,每次选举Term递增,天然解决了"脑裂"问题。而Paxos的Multi-Paxos如果自己实现选举,很容易出现多个节点同时认为自己是Leader的情况,需要额外的 fencing 机制(比如epoch number)来兜底。这就是为什么很多用Paxos的系统要依赖外部的锁服务或者ZooKeeper来做Leader选举。
二、日志复制的实现复杂度对比
Paxos的日志复制在理论上非常优雅:Leader提出一个值,经过两阶段提交后所有节点达成一致。但工程实现时有一个巨大的坑——Paxos允许乱序提交。也就是说,槽位5的提案可能比槽位3先被提交。这在论文里没问题,但在实际数据库中,你必须等前面的槽位都提交了才能应用到状态机,否则数据就是乱的。
为了解决这个问题,Multi-Paxos的工程实现通常要加一层"日志连续性"的约束,本质上就是把Paxos改造成了类似Raft的顺序提交模式。Google Spanner的做法是用Paxos做每个Paxos Group(一小段日志)的复制,但组内是严格顺序的。这其实已经和Raft的思路很接近了。
Raft在这方面天然友好。它要求Leader必须按顺序发送AppendEntries RPC,Follower必须按顺序应用日志。如果Follower发现中间有缺口,直接拒绝,Leader就会回退重试。这个机制在代码里就是一个简单的nextIndex数组和matchIndex数组的维护。
// Raft日志复制核心逻辑
func (rf *Raft) sendAppendEntries(peer int) {
args := AppendEntriesArgs{
Term: rf.currentTerm,
LeaderId: rf.me,
PrevLogIndex: rf.matchIndex[peer],
PrevLogTerm: rf.log.termAt(rf.matchIndex[peer]),
Entries: rf.log.entriesFrom(rf.matchIndex[peer]+1),
LeaderCommit: rf.commitIndex,
}
reply := peer.AppendEntries(args)
if reply.Success {
rf.matchIndex[peer] = args.PrevLogIndex + len(args.Entries)
rf.updateCommitIndex()
} else {
rf.matchIndex[peer]-- // 回退重试
}
}从工程维护角度看,Raft的日志复制代码量通常是Paxos的一半甚至更少。Paxos需要处理的边界情况更多:比如提案编号冲突、多个并发提案的竞争、日志截断后的恢复等。Raft把这些都简化成了"Leader说了算,Follower无条件跟"的模式。
三、成员变更的工程处理方式
这是两者差异最大的地方之一,也是实际生产环境中最容易出问题的地方。Paxos在成员变更上没有标准方案,各家都是自己摸索。最常见的做法是"联合共识"(Joint Consensus),即先把新旧配置都写入日志,等新配置的多数派达成一致后再切换。这个过程中系统同时运行两套配置,逻辑非常复杂,代码容易出错。
Raft在论文里专门用了一节讲成员变更,提出了"单节点变更"的方法:每次只增加或删除一个节点,通过两阶段过渡(先用旧配置的多数派提交变更,再用新配置的多数派提交变更)。这个方法在工程上简单得多,但缺点是变更速度慢,不能批量操作。
TiKV在Raft基础上做了扩展,支持批量成员变更,但本质上还是把批量操作拆成多次单节点变更。而Paxos系的系统如Spanner,成员变更依赖于外部的Paxos Group管理服务,实现更加重量级。
四、快照与日志压缩的实现策略
分布式数据库不可能无限保留所有历史日志,必须定期做快照(Snapshot)来压缩。Paxos和Raft在这方面的思路类似,都是Leader决定什么时候做快照,然后把快照发送给Follower。但细节上有差异。
Raft的快照机制非常明确:Leader可以在任何时候发送InstallSnapshot RPC,Follower收到后直接用快照替换本地日志。如果Follower发现自己的日志比快照新(比如刚当过Leader但没来得及提交快照),就拒绝快照。这个逻辑在代码里就是一个简单的lastIncludedIndex和lastIncludedTerm的比较。
Paxos的快照处理更复杂,因为Paxos允许乱序提交,所以快照必须包含一个"已提交的最高槽位"信息,而且要确保快照不会覆盖掉还在飞的提案。实际工程中,Paxos系系统通常用"chunk"的概念来管理快照,每个chunk对应一段连续的Paxos实例,快照就是某个chunk的完整状态。
五、网络分区与容错的工程表现
在网络分区场景下,Paxos和Raft的理论保证是一样的:都能保证多数派可用时系统继续工作,少数派自动停止服务。但工程实现上,Raft的处理更"傻瓜式"——Follower收不到Leader的心跳就超时选举,Candidate收不到多数票就自动退回Follower。整个过程完全自动化,不需要人工干预。
Paxos在分区场景下的工程处理要棘手得多。因为Multi-Paxos的Leader如果和多数派断开了,它自己不知道,还会继续接受客户端请求并尝试复制。这时候就需要额外的"Leader租约"机制或者外部的fencing token来防止旧Leader继续写数据。Spanner用的是TrueTime API配合Paxos来解决这个问题,但这依赖于硬件级别的时钟同步,普通团队根本玩不起。
六、性能与吞吐量的实际对比
从纯理论角度看,Paxos的Fast Paxos变体可以在一次RTT内完成提交(如果没有冲突),而Raft至少需要两次RTT(Leader到Follower,Follower回Leader)。但在实际工程中,这个理论优势几乎体现不出来。原因有三:第一,Fast Paxos的实现极其复杂,几乎没有开源系统真正用了它;第二,Raft可以通过pipeline和batch优化把多次AppendEntries合并成一次网络往返;第三,Paxos的乱序提交在高并发下反而会导致大量回退和重试,实际吞吐量并不高。
根据多个基准测试的结果,在相同硬件条件下,Raft实现的系统(如TiKV、CockroachDB)和Paxos实现的系统(如Spanner)在写入吞吐量上差距在10%-20%以内,但Raft的代码复杂度和运维成本要低得多。对于大多数团队来说,这个性价比是值得的。
七、选型建议与总结
如果你是自研分布式数据库,除非你有非常强的理论功底和工程团队,否则强烈建议从Raft入手。Raft的代码可理解性、社区生态、开源实现的成熟度都远超Paxos。etcd、TiKV、CockroachDB、HashiCorp的Raft库都是生产级的参考实现。
Paxos更适合那些有深厚分布式系统积累的团队,比如做全球级分布式数据库、需要跨数据中心强一致的场景。Spanner的成功不仅仅是因为Paxos,更是因为它背后有TrueTime、原子钟、全球网络等一整套基础设施。普通团队照搬Paxos很容易踩坑。
总结一句话:Paxos是"正确但难用"的算法,Raft是"够用且好用"的算法。工程世界里,能稳定跑起来的方案永远比理论最优的方案更有价值。理解两者的差异,不是为了选出谁更好,而是为了在具体场景下做出最合适的取舍。
