mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1331 字
4 分钟
Raft 共识算法:从选举到日志复制
2024-12-19

etcd 是 Kubernetes 集群状态存储的核心组件,支撑着数以万计的集群稳定运行。etcd 背后正是 Raft 算法——一个以”易理解”为设计目标的共识算法。2012 年 Diego Ongaro 和 John Ousterhout 在论文《In Search of an Understandable Consensus Algorithm》中提出 Raft,旨在解决 Paxos 难以理解和实现的问题。

一、Raft 要解决什么问题#

分布式共识问题的核心:一组节点需要对某个值达成一致,即使部分节点故障或网络中断,已决定的值也不能被推翻。

Raft 通过”复制状态机”(Replicated State Machine)模型实现共识:所有节点从相同初始状态出发,按相同顺序执行相同命令,最终状态必然一致。

graph TB subgraph "复制状态机" A[Client] --> C[共识模块] C --> D1[节点1] C --> D2[节点2] C --> D3[节点3] end

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

  1. Leader 选举:选出一个 Leader 负责所有决策
  2. 日志复制:Leader 将操作日志复制到多数节点
  3. 安全性:如果某个日志条目在某一任期被提交,则不会在后续任期被覆盖

二、Leader 选举#

2.1 三种角色#

Raft 节点在任意时刻处于以下三种角色之一:

角色职责转换条件
Follower(跟随者)被动接收 Leader 的日志和心跳选举超时后转为 Candidate
Candidate(候选者)发起选举,争取多数票获得多数票转为 Leader
Leader(领导者)处理所有客户端请求,复制日志发现更高任期时转为 Follower

2.2 选举流程#

sequenceDiagram participant F as Follower participant C as Candidate participant L as Leader F->>F: 选举超时 F->>C: 成为 Candidate C->>C: 递增当前任期 C->>C: 投自己一票 C->>C: 向其他节点发送 RequestVote C->>+L: RequestVote RPC L-->>-C: 投票响应 Note over C: 获得多数票成为 Leader C->>L: 成为 Leader L->>F: AppendEntries 心跳

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 复制流程#

sequenceDiagram participant C as Client participant L as Leader participant F as Follower C->>L: 请求 L->>L: 写入本地日志 L->>F: AppendEntries RPC F->>F: 写入日志 F-->>L: 成功响应 L->>L: 多数节点确认后提交 L->>C: 返回结果

Leader 收到客户端请求后:

  1. 将操作追加到本地日志
  2. 并行向所有 Follower 发送 AppendEntries RPC
  3. 等待多数节点确认
  4. 提交日志条目,应用到状态机
  5. 响应客户端

3.3 日志一致性#

Raft 维护以下日志匹配性质:如果两个日志条目具有相同的索引和任期,则它们存储的命令相同,且之前所有条目也相同。

当 Follower 的日志与 Leader 不一致时,Leader 通过递减 nextIndex 找到两者最后一致的点,然后从该点开始覆盖 Follower 的冲突条目。

四、成员变更#

集群成员变更不能简单地从旧配置直接切换到新配置,因为不同节点切换时机不同,可能出现两个多数派重叠,导致两个 Leader 同时当选。

Raft 采用”联合共识”(Joint Consensus)方案分两步完成变更:

  1. 切换到联合配置 C_old + C_new,任何决策都需要新旧配置各自的多数派同意
  2. 确认联合配置生效后,切换到新配置 C_new
graph LR A[旧配置 1,2,3] -->|添加节点4| B[联合共识 1,2,3,4] B -->|确认生效| C[新配置 2,3,4]

五、快照与日志压缩#

日志无限增长会耗尽存储,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 对比#

维度RaftPaxos
理解难度低,分阶段阐述高,证明复杂
Leader强 Leader,所有请求经 LeaderMulti-Paxos 可选 Leader
工程实现etcd、Consul、CockroachDBChubby、Google Spanner
日志结构连续索引,易于理解日志条目可空洞
成员变更联合共识,两阶段更复杂

Raft 通过强 Leader 简化了共识问题,是工程实践中广泛使用的共识算法。理解 Raft 的选举、复制和安全性机制,是构建可靠分布式系统的基础。

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

Raft 共识算法:从选举到日志复制
https://blog.souloss.com/posts/distributed/distributed-raft-algorithm/
作者
Souloss
发布于
2024-12-19
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时