引言

分布式系统中最核心的问题之一是如何让多个节点就某个值达成一致——这就是共识(Consensus)问题。Paxos作为共识算法的理论基石已经存在了几十年,但其晦涩难懂的名声让无数工程师望而却步。2014年,Diego Ongaro和John Ousterhout提出的Raft算法,以"可理解性"为第一设计目标,让共识算法从理论殿堂走向了工程实践。etcd、Consul、TiKV、CockroachDB等关键基础设施都基于Raft构建。

一、Raft解决的问题

在分布式系统中,网络分区、节点崩溃、消息丢失和乱序是常态。共识算法的目标就是:在部分节点故障的情况下,保证存活节点对某个值达成一致,且一旦决定就不可更改。

Raft将共识问题分解为三个相对独立的子问题:

1. Leader选举(Leader Election):从集群中选出一个节点作为Leader,所有写请求经过Leader

2. 日志复制(Log Replication):Leader接收日志条目,复制到多数节点后提交

3. 安全性(Safety):确保每个节点的日志最终一致,不会丢失已提交的条目

二、状态机与日志

Raft的基础是复制状态机(Replicated State Machine)模型。每个节点维护一个有序日志,日志中的每个条目包含一个状态机命令。当日志条目被"提交"(committed)后,应用到状态机。只要所有节点的日志最终一致,状态机的输出最终也一致。

每个日志条目有三个关键属性:

- Term(任期号):单调递增的整数,每次选举后增加

- Index(索引):日志条目在日志中的位置

- Command(命令):要应用到状态机的操作

三、Leader选举

Raft集群中的节点有三种状态:Leader、Follower、Candidate。正常情况下只有一个Leader,其余都是Follower。

Leader定期向Followers发送心跳(AppendEntries RPC,不带日志内容)。如果Follower在"选举超时"时间内未收到心跳,就认为Leader已宕机,转变为Candidate并开始新选举:

1. 增加当前Term,投票给自己

2. 重置选举超时

3. 向所有其他节点发送RequestVote RPC

获得多数票(包括自己)的Candidate成为新Leader。如果有两个Candidate同时发起选举且各获得相等票数,则本轮无Leader产生,等待超时后下一轮选举。这种"分票"(split vote)情况通过随机选举超时来减少概率——每个节点的超时时间随机分布在某个区间[150ms, 300ms]之间,使得同一时刻多个节点超时的概率极低。

四、日志复制流程

日志复制是Raft的核心,流程如下:

1. 客户端发送写请求到Leader

2. Leader将命令追加为新的日志条目

3. Leader并行向所有Follower发送AppendEntries RPC

4. Follower收到后,验证PrevLogIndex和PrevLogTerm匹配

5. 一旦多数Follower确认收到,Leader提交该条目(更新commitIndex)

6. Leader将结果返回客户端,并在下次心跳中通知Followers提交

关键细节:如果Follower发现PrevLogIndex位置的前一条日志条目与Leader不匹配(Term或Index不一致),它会拒绝此次AppendEntries,Leader需要回退并从更早的位置开始复制。这个"回退"机制在Raft论文中描述为NextIndex递减——实际工程中可能有优化(如Follower返回冲突条目的FirstIndex来加速回退)。

五、安全性保证

Leader选举和日志复制都不足以保证正确性——Raft还需要严格的选举限制来确保安全性:

已提交的不丢失:Raft保证一旦日志条目被提交,它一定会在未来所有Leader的日志中。如何保证?因为一个Candidate要赢得选举,必须获得多数票,而一个已提交的条目已经被多数节点拥有。根据鸽巢原理,多数投票的多数节点集合必然与已提交的多数节点集合有交集——即至少有一个投票者拥有该已提交条目。加上以下规则确保这些条目覆盖:

选举限制:Candidate的日志必须"至少和投票者一样新"。具体规则:比较最后一条日志条目的Term,Term大的更新;Term相同则Index大的更新。如果Candidate的日志比投票者旧,投票者拒绝投票。

只提交当前Term的条目:Raft不允许通过计数副本方式提交旧Term的条目。只有当前Term的Leader可以通过计数副本提交日志条目,并且通过"提交即拥有"的规则隐式提交了之前Term的所有条目。这个设计避免了"已经应用到状态机的条目被覆盖"的情况。

六、集群成员变更

实际系统需要在不停止服务的情况下增删节点。Raft的原始论文提出了联合共识(Joint Consensus)方法——在过渡阶段要求配置变更同时满足旧配置和新配置的多数同意,避免在切换期间因多数集合的不同定义导致脑裂。

实践中很多系统使用单步变更(Single-Server Changes)——一次只增减一个节点。这种方法更简单,但限制较多——在3节点集群中,不能同时移除一个节点并添加另一个,否则会因多数节点数量不变但成员集合变化而产生不安全窗口。

七、工程优化

Pre-Vote:防止一个脱离集群的节点持续发起选举并增加Term导致集群Leader频繁切换。Pre-Vote要求候选人在正式选举前先发送PreVote RPC,只有在获得多数PreVote响应后才增加Term并正式发起选举。

Lease Read:Leader在心跳多数确认后的Lease期内,可以直接读取而无需额外的网络通信。这要求时钟漂移有上界(Bound on Clock Drift)。

Leader Stickiness:正常情况下,如果Leader还活着,即使收到更高Term的消息,也不主动退位。实现方法是:收到AppendEntries时检查来源的Term是否严格大于自己的Term。

日志压缩(Snapshot):Raft日志不能无限增长。通过Snapshot机制,将状态机的当前状态和最后应用于状态机的日志条目(LastIncludedIndex和LastIncludedTerm)打包为快照,之前的日志可以丢弃。对于慢节点或新节点,Leader可以通过InstallSnapshot RPC发送快照来同步。

八、开源实现对比

etcd/raft(Go):Kubernetes的基石,使用Pre-Vote和CheckQuorum等优化,是Raft最广泛部署的实现之一。

TiKV/RocksEngine(Rust):PingCAP的分布式KV存储,基于Raft的Multi-Raft(多个Region各自维护Raft组)实现了水平扩展能力。

LogCabin(C ):Ongaro的原始实现,作为Raft正确性的参考。

braft(C ):百度开源的Raft实现,深度优化了Pipeline复制和Parallel AppendEntries等性能特性。

九、结语

Raft的成功证明了"可理解性"对于分布式系统算法的重要性。通过Leader选举、日志复制和安全性三个清晰的子问题分解,Raft将复杂的共识变成了可以教学和实现的工程方案。理解Raft不仅让我们能正确使用etcd等分布式组件,更重要的是培养了洞察分布式系统本质问题的思维——在混乱的网络世界中寻找确定性的艺术。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部