引言
在分布式系统中,共识(Consensus)是最核心的问题之一。如何让多个节点就某个值达成一致,即使在部分节点故障的情况下也能正常工作?Raft算法由Diego Ongaro和John Ousterhout在2014年的论文《In Search of an Understandable Consensus Algorithm》中提出,以其"可理解性"为目标,迅速成为业界最广泛采用的共识算法之一。从etcd、TiKV到Consul、RabbitMQ,Raft支撑着现代分布式系统的底层一致性。
一、为什么需要Raft?
在Raft之前,Paxos是分布式共识领域的事实标准。然而Paxos以其难以理解和实现著称——Leslie Lamport本人多次修改论文试图让读者理解,但实际工程中能正确实现Multi-Paxos的团队屈指可数。Raft的设计目标很明确:在保证正确性和性能的前提下,让工程师能够轻松地理解和实现。
Raft通过几个关键设计选择来达成这一目标:
- 分解子问题:将共识分解为Leader选举、日志复制、安全性三个相对独立的子问题
- 减少状态空间:相比Paxos的非确定性,Raft的规则更加确定和直观
- 随机化超时:通过选举超时随机化来避免选票分裂(split vote)
二、Raft核心机制
2.1 节点状态机
Raft集群中的每个节点处于三种状态之一:
| 状态 | 职责 |
|---|---|
| Leader | 处理所有客户端请求、复制日志到Follower、定期发送心跳 |
| Follower | 被动响应Leader的RPC请求,如果选举超时未收到心跳则转为Candidate |
| Candidate | 发起选举,获得多数票则成为Leader,发现更高任期或新Leader则退回Follower |
节点之间的状态转换由任期号(Term)驱动——Raft将时间划分为不定长的任期,任期号是全局单调递增的数字。
2.2 Leader选举
当Follower在选举超时时间内未收到Leader的心跳时,它会认为Leader已故障,于是增加当前任期号,转为Candidate状态,并行向其他节点发送RequestVote RPC。
选举的关键约束:每个任期每个节点最多投一票。当Candidate获得集群多数(N/2+1)票时,它就成为新Leader。为了减少同时发起选举导致的选票分裂,选举超时时间被设置为随机值(如150ms-300ms之间的随机数)。
一个巧妙的设计是:Follower在投票前会比较Candidate的日志是否"至少和自己一样新"——即候选人最后一条日志的任期号不小于自己,如果任期相同则索引不小于自己。这确保了当选的Leader包含所有已提交的日志条目。
2.3 日志复制
Leader选出后开始服务客户端请求。每个修改操作都被编码为一条日志条目(log entry),经历以下生命周期:
- Leader将日志追加到本地日志
- Leader并行向所有Follower发送AppendEntries RPC
- 当多数Follower确认收到后,Leader提交(commit)该条目
- Leader将结果返回给客户端,同时通知Follower已提交的条目
Raft的日志具有匹配特性(Log Matching Property):如果两个日志包含相同索引和任期的条目,那么这两个日志在该索引之前的所有条目完全一致。这个特性由AppendEntries RPC的一致性检查保证——Leader在发送AppendEntries时会携带前一条日志的索引和任期,Follower必须首先确认其匹配才会接受新条目。
三、安全性保证
3.1 选举限制
Raft通过"日志至少一样新"的投票规则保证:任何当选的Leader必然包含了之前所有任期中已提交的全部日志。因为要获得多数票,Candidate的日志必须至少和多数节点的日志一样新,而已提交的日志必然存在于多数节点上。
3.2 Leader只提交本任期的日志
这是一个容易被忽略但至关重要的规则。Leader不能通过计算副本数来提交之前任期的日志。原因是考虑如下场景:
- 任期S1是Leader,写入了索引2处的条目,但只复制了自己和一个Follower
- S1宕机,S2成为任期3的Leader,在索引2处写入了新条目
- S2宕机,S1恢复,重新成为Leader
- 如果S1通过计算副本数提交了索引2的旧条目(因为它现在在多数节点上),但此时S2在索引2处还有新条目!
因此Raft要求:新Leader只能通过提交本任期的日志来间接提交之前任期的日志(当本任期的日志被提交时,之前任期的所有日志自然也被提交了)。
3.3 日志压缩与快照
随着运行时间的增长,日志会无限膨胀。Raft通过快照(Snapshot)机制来解决这个问题。每个节点独立地在日志达到一定大小时创建快照:将当前状态机的完整状态写入快照文件,丢弃此前所有日志。
当新节点加入或远落后于Leader的Follower需要同步时,Leader会发送InstallRPC将快照传输给Follower,然后补充快照之后的日志条目。
四、工程实践中的关键考量
4.1 PreVote机制
在基础Raft中,一个节点如果无法连接到Leader,就会增加任期并发起选举。但这可能导致网络分区中的节点任期号不断膨胀——当它重新连接时,由于任期号远高于Leader,会导致Leader下台。PreVote机制要求Candidate在正式递增任期前先发起一轮PreVote RPC,只有在不递增任期的前提下获得多数票才继续正式选举,从而避免了不必要的不稳定。
4.2 Leader Lease与Read Index
Raft的日志复制只能保证写操作的一致性。对于读操作,Leader直接读取状态机可能读到过期数据(如果它已经不是真正的Leader了)。解决方案有:
- Read Index:Leader先记录当前的提交索引,然后等待状态机至少应用到该索引,然后读取
- Lease Read:利用Leader租约(一段时间内确信自己仍是Leader),直接从状态机读取
4.3 联合共识(Joint Consensus)
集群配置变更(增加/移除节点)是一个高危操作。如果直接从旧配置切换到新配置,可能出现两个重叠任期的Leader。Raft采用联合共识策略:先切换到包含新旧配置的联合配置C(old,new),只有在联合配置中提交了新配置后,才最终切换到C(new)。这确保了在任何时刻不会出现两个不相交的多数派。
五、性能优化面面观
在实际部署中,Raft的吞吐量上限由Leader的网络带宽和磁盘I/O决定。常见优化手段包括:
- 批量提交(Batching):将多个客户端请求打包成一个日志条目或一次RPC往返
- Pipeline:不等前面AppendEntries完成就发送下一个,充分利用网络带宽
- 并行Apply:日志提交和状态机应用可以并行进行
- Learner节点:只接收日志但不参与投票的节点,用于读写分离和跨地域复制
六、总结
Raft的成功不仅在于算法设计的优雅,更在于它切中了分布式系统工程中的一个痛点:理论和实践的鸿沟。通过将复杂的共识问题分解为清晰的子问题,并消除不必要的非确定性,Raft让数千个开发团队能够正确地构建分布式系统。从etcd中支撑Kubernetes集群的版本管理,到TiKV中保障TiDB的数据一致性,Raft已经深深嵌入现代云原生基础设施的血脉之中。理解Raft,是理解分布式系统的一把钥匙。
参考资料
- Diego Ongaro, John Ousterhout. "In Search of an Understandable Consensus Algorithm." USENIX ATC 2014.
- Raft可视化演示:https://raft.github.io
- etcd Raft实现:https://github.com/etcd-io/raft

发表评论 取消回复