一、为什么需要共识算法
在分布式系统中,多个节点如何对某个值达成一致,是设计可靠系统的核心难题。Raft 算法由 Diego Ongaro 和 John Ousterhout 在 2014 年提出,以"易于理解"为设计目标,把共识问题分解为三个相对独立的子问题:节点定域、日志写入、故障恢复,允许开发者分别理解和实现每个部分。
与 Paxos 相比,Raft 的优势在于:有清晰的算法描述、磨合的共识心理论,以另一种方式表示同一个算法,便于在校教材中使用和学习。本文将从理论到实战,完整解析 Raft 的核心机制和工程落地细节。
二、节点状态机:三种角色的切换
Raft 中的每个节点在任意时刻处于三种状态之中的一种:
// 节点状态定义
enum NodeState {
Follower // 追随者:被动响应,不主动发起请求
Candidate // 候选人:正在写寻领导者职位
Leader // 领导者:处理所有客户端请求,复制日志
}
状态切换规则:
- Follower → Candidate:在超时时间内没有收到 Leader 的心跳,升级为 Candidate 并发起选举
- Candidate → Leader:获得多数节点的票待,弹到领导者
- Candidate → Follower:发现新的 Leader 或遇高任期号,退回 Follower
- Leader → Follower:发现更高任期号,立即降级
三、节点观测:领导者选举
节点观测是 Raft 的核心逻辑。每个节点维护一个开发者状态,用于向其他节点请求投票:
struct RequestVoteArgs {
term: u64, // 候选人的当前任期号
candidate_id: u64, // 候选人 ID
last_log_index: u64, // 候选人日志最后条目索引
last_log_term: u64, // 候选人日志最后条目任期号
}
struct RequestVoteReply {
term: u64, // 投票者的当前任期号
vote_granted: bool, // 是否投票给候选人
voter_id: u64, // 本节点 ID(用于跟踪)
}
选举流程:
- 上升任期号,置自身为 Candidate
- 向所有其他节点广播 RequestVote
- 等待回复:获得多数票(#(n/2)+1)则弹到 Leader
- 超时则重新发起选举
安全性保证:每个节点在一个任期号内只能投一票("每个任期号一票" 原则),避免多个候选人同时当选。此外,候选人必须拥有所有已提交的日志条目(通过比较 last_log_term 和 last_log_index),确保新 Leader 不丢失已提交数据。
随机超时避免选票分裂:Raft 使用随机化的选举超时时间(通常 150ms~300ms),极大降低了选票分裂的概率。如果发生分裂没有候选人获得多数票,则超时后重新发起选举,随机性确保各节点不会持续同时发起。
// 随机超时设置(Rust 伪代码)
use rand::Rng;
fn election_timeout() -> Duration {
let mut rng = rand::thread_rng();
Duration::from_millis(150 + rng.gen_range(0..150))
}
// 节点主循环
async fn run_follower_loop(&mut self) {
loop {
tokio::select! {
// 收到 Leader 心跳,重置定时器
Some(append) = self.heartbeat_rx.recv() => {
self.reset_election_timer();
}
// 超时则发起选举
_ = tokio::time::sleep(self.election_timeout) => {
self.start_election().await;
}
}
}
}
四、日志写入:复制状态机
Leader 当选后,负责处理所有客户端的请求。其核心流程是将操作日志复制到所有 Follower 节点,当多数节点确认后,该日志被 "提交"(commit),然后应用到状态机。
// 日志条目结构
struct LogEntry {
term: u64, // 条目所在任期号
index: u64, // 条目在日志中的位置
command: Vec<u8>, // 要执行的命令数据
committed: bool, // 是否已提交
}
日志复制流程:
- 客户端发送命令给 Leader
- Leader 将新条目追加到本地日志
- Leader 并行向所有 Follower 发送 AppendEntries RPC
- Follower 确认接收(返回 success=true)
- Leader 等待多数节点确认后,标记该条目为 committed
- Leader 应用该条目到状态机,返回结果给客户端
- Leader 通知 Follower 提交(下次心跳携带 commit_index)
// AppendEntries RPC
struct AppendEntriesArgs {
term: u64, // Leader 任期号
leader_id: u64, // Leader ID
prev_log_index: u64, // 前一条日志索引
prev_log_term: u64, // 前一条日志任期号
entries: Vec<LogEntry>,// 要追加的日志条目
leader_commit: u64, // Leader 的 commit_index
}
// Follower 处理 AppendEntries
fn handle_append_entries(&mut self, args: AppendEntriesArgs) -> AppendEntriesReply {
// 1. 如果 term < currentTerm,拒绝
if args.term < self.current_term {
return AppendEntriesReply { term: self.current_term, success: false };
}
// 2. 重置选举定时器(确认 Leader 存活)
self.reset_election_timer();
// 3. 日志匹配检查:prev_log_index 处的 term 必须等于 prev_log_term
if !self.log.matches(args.prev_log_index, args.prev_log_term) {
return AppendEntriesReply { term: self.current_term, success: false };
}
// 4. 追加新条目,处理冲突
self.log.append_after(args.prev_log_index, args.entries);
// 5. 更新 commit_index
if args.leader_commit > self.commit_index {
self.commit_index = args.leader_commit.min(self.log.last_index());
}
AppendEntriesReply { term: self.current_term, success: true }
}
日志压缩与快照:随着日志持续增长,Raft 支持快照机制。Leader 可以向落后的 Follower 发送 InstallSnapshot RPC,将当前状态机快照传输过去,使 Follower 快速追上进度。
// InstallSnapshot RPC
struct InstallSnapshotArgs {
term: u64, // Leader 任期号
leader_id: u64, // Leader ID
last_included_index: u64, // 快照包含的最后条目索引
last_included_term: u64, // 快照包含的最后条目任期号
offset: u64, // 数据块在快照中的偏移
data: Vec<u8>, // 快照数据块
done: bool, // 是否是最后一块
}
五、集群成员变更:联合共识
Raft 支持在线添加或移除节点(不中断服务)。为了保证安全性,Raft 使用 "联合共识"(Joint Consensus)策略:
- Leader 提交一个包含新旧两种配置的联合配置日志条目(Cold,new)
- 在联合配置期间,所有决策需要同时获得 Cold 和 Cold,new 的多数节点同意
- Leader 然后提交只包含新配置的日志条目(Cnew)
- 新配置生效,旧节点可以安全关闭
这种机制避免了过渡期间两个不相交的多数派同时存在的 "脑裂" 问题。HashiCorp Raft 等实现中对此做了大量工程优化。
六、工程实践:etcd 中的 Raft 实现
etcd 是 CNCF 孵化的分布式 KV 存储,其底层使用 Go 自研的 Raft 实现(raft package),是目前生产级 Raft 实现的代表。
关键优化点:
- 批量化写入:Leader 不会为每条日志单独发 AppendEntries,而是在一个 BatchWindow 内聚合后批量发送,降低网络开销
- 流水线复制:Leader 不等上一个 AppendEntries 返回就发送下一个,利用带宽延迟积(BDP)提升吞吐
- PreVote 机制:正式发起选举前先做一次预投票,避免网络分区节点频繁发起选举导致任期号膨胀
- CheckQuorum:Leader 定期检测多数派是否可达,如果不可达则主动降级,避免分区两侧各自为政
- Learner 节点:新加入的节点先作为 Learner 只接收日志但不参与投票,追赶进度后再转为 Voter,避免慢节点拖慢整体
// etcd raft 的 Step 函数核心逻辑(简化)
func (r *raft) Step(m pb.Message) error {
switch m.Type {
case pb.MsgHup:
// 超时,发起选举
r.campaign(campaignElection)
case pb.MsgBeat:
// 心跳定时器触发,发送心跳
r.bcastHeartbeat()
case pb.MsgProp:
// 客户端提案,追加到本地日志
r.appendEntry(m.Entries...)
case pb.MsgApp:
// AppendEntries from leader
r.tickHeartbeat = r.randomizedTimeout()
r.leader = m.From
r.handleAppendEntries(m)
case pb.MsgAppResp:
// AppendEntries 回复,检查是否多数确认
if pr.Reject {
// 日志不匹配,递减 nextIndex 重试
r.tryDecrTo(pr.From, pr.Index, m.Index)
} else {
// 更新 matchIndex,检查 commitIndex 是否推进
r.updateMatchIndex(pr.From, m.Index)
r.maybeCommit()
}
}
}
七、故障场景推演
场景一:Leader 崩溃
集群从剩余节点中选出新 Leader(通常是最先超时的节点)。新 Leader 的日志至少和多数节点一样 "新",因此不会丢失已提交的条目。但可能有 Leader 已接收但未提交的 "幽灵日志",新 Leader 会用自己同任期的条目覆盖它们。
场景二:网络分区
如果 Leader 被隔离到少数派分区,它无法获得多数节点的 AppendEntries 确认,因此无法提交新日志。多数派分区会选出新的 Leader 继续服务。分区恢复后,旧 Leader 发现更高任期号,主动降级并同步缺失日志。
场景三:节点逐个重启
逐个滚动重启时,每次重启只影响一个节点。集群在多数节点在线的情况下持续服务。被重启的节点重新加入后,通过 InstallSnapshot 或 AppendEntries 同步日志即可恢复。但需要注意:并发重启超过半数节点会导致集群不可用。
八、性能调优参数
| 参数 | 典型值 | 说明 |
|---|---|---|
| ElectionTimeout | 150~300ms | 随机超时区间,避免选票分裂 |
| HeartbeatInterval | 10~50ms | Leader 心跳频率,通常 = ElectionTimeout / 10 |
| MaxSizePerMsg | ~1MB | 单次 AppendEntries 最大数据量 |
| SnapshotChunkSize | ~16MB | InstallSnapshot 每次发送的数据块大小 |
| BatchWindowSize | 10~100ms | 日志批量发送的聚合时间窗口 |
| MaxInflightMsgs | 2048~4096 | 流水线模式下最大在途消息数 |
九、总结
Raft 通过将共识问题解耦为领导者选举、日志复制和安全性三个模块,使得理解和实现都远优于原始的 Paxos 算法。其关键设计哲学包括:
- 强 Leader 模型:简化逻辑,所有写请求都经过 Leader
- 日志作为全序广播通道:每条日志有序且幂等
- 多数派决策:(n/2)+1 的法定人数保证安全性
- 任期号作为逻辑时钟:term 严格递增,高 term 击败低 term
在工程层面,etcd、TiKV、CockroachDB、 Consul 等系统都基于 Raft 构建,证明了其在生产环境中的可靠性。对于开发者而言,理解 Raft 不仅是掌握一个算法,更是建立分布式系统思维的重要一步。
推荐阅读:
- Raft 原始论文:In Search of an Understandable Consensus Algorithm (Ongaro & Ousterhout, 2014)
- Raft 可视化演示:RaftScope 和 The Secret Lives of Data
- etcd raft 源码:github.com/etcd-io/raft

发表评论 取消回复