etcd 是 Kubernetes 集群状态存储的核心组件,支撑着数以万计的集群稳定运行。etcd 背后正是 Raft 算法——一个以”易理解”为设计目标的共识算法。2012 年 Diego Ongaro 和 John Ousterhout 在论文《In Search of an Understandable Consensus Algorithm》中提出 Raft,旨在解决 Paxos 难以理解和实现的问题。
一、Raft 要解决什么问题
分布式共识问题的核心:一组节点需要对某个值达成一致,即使部分节点故障或网络中断,已决定的值也不能被推翻。
Raft 通过”复制状态机”(Replicated State Machine)模型实现共识:所有节点从相同初始状态出发,按相同顺序执行相同命令,最终状态必然一致。
Raft 将共识问题分解为三个相对独立的子问题:
- Leader 选举:选出一个 Leader 负责所有决策
- 日志复制:Leader 将操作日志复制到多数节点
- 安全性:如果某个日志条目在某一任期被提交,则不会在后续任期被覆盖
二、Leader 选举
2.1 三种角色
Raft 节点在任意时刻处于以下三种角色之一:
| 角色 | 职责 | 转换条件 |
|---|---|---|
| Follower(跟随者) | 被动接收 Leader 的日志和心跳 | 选举超时后转为 Candidate |
| Candidate(候选者) | 发起选举,争取多数票 | 获得多数票转为 Leader |
| Leader(领导者) | 处理所有客户端请求,复制日志 | 发现更高任期时转为 Follower |
2.2 选举流程
2.3 选举安全性
Raft 通过两条规则保证选举安全:
- 每个节点在同一任期内最多投一票(先到先得)
- 候选者的日志至少和自己一样新才能获得投票
// 投票条件func canVoteFor(candidate, self) bool { // 1. 当前任期未投票 if self.votedFor != "" && self.votedFor != candidate.id { return false } // 2. 候选者日志至少和自己一样新 if candidate.lastLogTerm < self.lastLogTerm { return false } if candidate.lastLogTerm == self.lastLogTerm && candidate.lastLogIndex < self.lastLogIndex { return false } return true}2.4 随机化选举超时
选举超时在 150-300ms 之间随机选取,避免多个节点同时发起选举导致选票分散。
func randomElectionTimeout() time.Duration { return time.Duration(150 + rand.Intn(150)) * time.Millisecond}三、日志复制
3.1 日志结构
每个日志条目包含三个字段:
| 字段 | 含义 |
|---|---|
| Term(任期号) | 创建该条目时的 Leader 任期 |
| Index(索引) | 日志中的位置 |
| Command(命令) | 状态机要执行的命令 |
3.2 复制流程
Leader 收到客户端请求后:
- 将操作追加到本地日志
- 并行向所有 Follower 发送 AppendEntries RPC
- 等待多数节点确认
- 提交日志条目,应用到状态机
- 响应客户端
3.3 日志一致性
Raft 维护以下日志匹配性质:如果两个日志条目具有相同的索引和任期,则它们存储的命令相同,且之前所有条目也相同。
当 Follower 的日志与 Leader 不一致时,Leader 通过递减 nextIndex 找到两者最后一致的点,然后从该点开始覆盖 Follower 的冲突条目。
四、成员变更
集群成员变更不能简单地从旧配置直接切换到新配置,因为不同节点切换时机不同,可能出现两个多数派重叠,导致两个 Leader 同时当选。
Raft 采用”联合共识”(Joint Consensus)方案分两步完成变更:
- 切换到联合配置 C_old + C_new,任何决策都需要新旧配置各自的多数派同意
- 确认联合配置生效后,切换到新配置 C_new
五、快照与日志压缩
日志无限增长会耗尽存储,Raft 通过快照机制压缩日志:
- 每个节点独立创建快照,将当前状态机状态持久化
- 快照包含 lastIncludedIndex 和 lastIncludedTerm
- 快照之前的日志条目可以丢弃
- Follower 落后太多时,Leader 发送 InstallSnapshot RPC 而非日志条目
六、etcd 中的 Raft
etcd 是 Raft 最知名的生产级实现,广泛用于 Kubernetes、Consul 等系统。
6.1 关键配置
import "go.etcd.io/raft/v3"
conf := &raft.Config{ ID: uint64(1), ElectionTick: 10, // 选举超时 = 10 * 心跳间隔 HeartbeatTick: 1, // 心跳间隔 MaxSizePerMsg: 1024 * 1024, MaxInflightMsgs: 256,}node := raft.StartNode(conf, []raft.Peer{})6.2 常见问题与应对
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 选举失败 | 多数节点同时超时 | 增大 election_timeout |
| 日志不一致 | 网络分区 | Leader 修复 Follower 日志 |
| 脑裂 | 网络分区导致双 Leader | 任期比较,低任期 Leader 退位 |
| 成员变更失败 | 配置切换冲突 | 联合共识,一次变更一个节点 |
6.3 性能优化
- 批量提交:将多个日志条目打包提交,减少 RPC 次数
- 流水线复制:不等前一条目确认就发送下一条目
- ReadIndex 优化:读请求无需写入日志,通过 ReadIndex 直接读取
七、Raft 与 Paxos 对比
| 维度 | Raft | Paxos |
|---|---|---|
| 理解难度 | 低,分阶段阐述 | 高,证明复杂 |
| Leader | 强 Leader,所有请求经 Leader | Multi-Paxos 可选 Leader |
| 工程实现 | etcd、Consul、CockroachDB | Chubby、Google Spanner |
| 日志结构 | 连续索引,易于理解 | 日志条目可空洞 |
| 成员变更 | 联合共识,两阶段 | 更复杂 |
Raft 通过强 Leader 简化了共识问题,是工程实践中广泛使用的共识算法。理解 Raft 的选举、复制和安全性机制,是构建可靠分布式系统的基础。
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






