核心概念 - Raft 共识
etcd 的分布式一致性依赖于 Raft 共识算法。本章将深入探讨 Raft 算法的原理及其在 etcd 中的实现。
分布式一致性问题
问题背景
在分布式系统中,多个节点需要就数据达成一致,面临以下挑战:
- 网络分区:节点间通信可能中断
- 节点故障:节点可能随时宕机
- 消息延迟/丢失:网络不可靠
CAP 定理
分布式系统最多只能同时满足以下三项中的两项:
- Consistency(一致性):所有节点看到相同的数据
- Availability(可用性):每个请求都能得到响应
- Partition Tolerance(分区容错):网络分区时系统仍能运行
etcd 选择 CP:强一致性 + 分区容错。
Raft 算法概述
Raft 是一种易于理解的分布式共识算法,由 Diego Ongaro 和 John Ousterhout 在 2014 年提出。
核心目标
- Leader 选举:选出一个 Leader 负责管理日志复制
- 日志复制:Leader 接收客户端请求并复制到其他节点
- 安全性:保证所有已提交的日志条目不会被覆盖
节点角色
┌─────────────────────────────────────────────────────┐
│ │
│ ┌──────────┐ │
│ │ Leader │ ◄─── 接收客户端请求 │
│ └────┬─────┘ │
│ │ 心跳/日志复制 │
│ ┌────┴────┐ │
│ ▼ ▼ │
│ ┌────────┐ ┌────────┐ │
│ │Follower│ │Follower│ │
│ └────────┘ └────────┘ │
│ │
│ 可能存在 Candidate(候选者)参与选举 │
└─────────────────────────────────────────────────────┘- Follower(跟随者):被动响应 Leader 和 Candidate 的请求
- Candidate(候选者):参与选举,尝试成为 Leader
- Leader(领导者):处理所有客户端请求,复制日志到 Follower
Leader 选举
选举过程
时间线:
┌─────────────────────────────────────────────────────>
│ │ │
选举超时 选举超时 选举超时
│ │ │
▼ ▼ ▼
Follower Follower Follower
│ │ │
│ 超时无心跳 │ │
▼ │ │
Candidate ────────────>│ 发起投票 │
│ RequestVote │ │
│ ▼ │
│ 投票给 Candidate │
│ │ │
│◄─────────────────┘ │
│ 获得多数票 │
▼ │
Leader ────────────────>心跳─────────────>│选举超时
- 每个节点有随机选举超时时间(如 150-300ms)
- 超时期间未收到 Leader 心跳,转变为 Candidate
- 随机化避免同时发起选举(减少选票瓜分)
选举规则
- 每个 term 最多投一票,先到先得
- Candidate 需要获得多数节点投票才能成为 Leader
- 新 Leader 立即发送心跳确立领导地位
etcd 中的实现
go
// 选举超时配置
type RaftConfig struct {
// 选举超时 = base + rand(0, electionTick)
ElectionTick int // 默认 10
HeartbeatTick int // 默认 1
}bash
# 查看当前 Leader
etcdctl endpoint status --cluster -w table
# 输出示例
+------------------------+------------------+---------+---------+-----------+------------+
| ENDPOINT | ID | VERSION | DB SIZE | IS LEADER | RAFT INDEX |
+------------------------+------------------+---------+---------+-----------+------------+
| http://localhost:2379 | 8e9e05c52164694d | 3.5.17 | 20 kB | false | 10|
| http://localhost:22379| fd422379fda50e48 | 3.5.17 | 20 kB | false | 10|
| http://localhost:32379| 7a61a32f8669483a | 3.5.17 | 20 kB | true | 10|
+------------------------+------------------+---------+---------+-----------+------------+日志复制
日志结构
日志条目(Log Entry):
┌──────────────────────────────────────────┐
│ Index | Term | Command │
├───────┼──────┼───────────────────────────┤
│ 1 | 1 | Set /foo = "bar1" │
│ 2 | 1 | Set /baz = "qux" │
│ 3 | 2 | Set /foo = "bar2" │
│ 4 | 2 | Delete /baz │
└───────┴──────┴───────────────────────────┘- Index:日志索引,递增
- Term:任期号,用于检测过时 Leader
- Command:具体操作(Put、Delete 等)
复制流程
Client Leader Follower
│ │ │
│ Put /foo = "bar" │ │
│────────────────────>│ │
│ │ │
│ │ AppendEntries RPC │
│ │───────────────────>│
│ │ │ 写入本地日志
│ │ AppendEntries Resp │
│ │<───────────────────│ (success)
│ │ │
│ │ 应用到状态机 │
│ │ (Commit Index) │
│ │ │
│ Response │ │
│<────────────────────│ │提交(Commit)
- 已提交(Committed):日志被复制到多数节点
- 已应用(Applied):已提交的日志应用到状态机
- Leader 更新 commitIndex,通知 Follower 应用日志
一致性保证
- 日志匹配特性:如果两个日志在相同索引有相同 term,则之前的日志都相同
- Leader 完整性:Leader 包含所有已提交的日志
- 状态机安全性:所有节点按相同顺序应用相同日志
安全性规则
Term 机制
- Term 是逻辑时钟,每次选举递增
- 节点发现更高 term 时,立即转为 Follower
- 保证同一时间最多一个 Leader
投票限制
Candidate 请求投票时,携带自己的日志信息:
RequestVote RPC:
- term: Candidate 的 term
- candidateId: Candidate ID
- lastLogIndex: 最后一条日志的索引
- lastLogTerm: 最后一条日志的 term投票者只投票给日志至少和自己一样新的 Candidate:
go
// 日志比较逻辑
func isUpToDate(lastLogTerm, lastLogIndex int, myLastLogTerm, myLastLogIndex int) bool {
if lastLogTerm != myLastLogTerm {
return lastLogTerm > myLastLogTerm
}
return lastLogIndex >= myLastLogIndex
}防止脑裂
网络分区场景:
┌─────────────┐ ┌─────────────┐
│ Partition A │ │ Partition B │
│ │ │ │
│ Leader │ ─ ─ ─ ─ │ 旧 Leader │
│ (Term 3) │ 网络 │ (Term 2) │
│ Follower │ 分区 │ │
└─────────────┘ └─────────────┘
3节点 2节点
多数派 少数派
Partition A 可以继续工作(3/5 = 多数)
Partition B 旧 Leader 的写操作会失败(无法获得多数确认)etcd 中的 Raft 实现
关键组件
┌────────────────────────────────────────────────────┐
│ etcd Server │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Raft Node │ │ Storage │ │
│ │ │ │ (WAL+BBolt) │ │
│ └──────┬───────┘ └──────────────┘ │
│ │ │
│ ┌──────┴───────┐ ┌──────────────┐ │
│ │ Transport │ │ Apply │ │
│ │ (gRPC) │ │ Channel │ │
│ └──────────────┘ └──────────────┘ │
└────────────────────────────────────────────────────┘状态持久化
持久化状态(写入 WAL):
- currentTerm:当前任期
- votedFor:投票给谁
- log[]:日志条目
易失状态(内存):
- commitIndex:已知的最高已提交日志索引
- lastApplied:最后应用到状态机的日志索引
- nextIndex[]:对每个节点,下一个要发送的日志索引
- matchIndex[]:对每个节点,已复制的最高日志索引ReadIndex 读优化
为保证线性一致性读,Leader 需要:
1. 记录当前 commitIndex 为 readIndex
2. 向多数节点发送心跳确认自己是 Leader
3. 等待状态机应用到 readIndex
4. 执行读操作bash
# etcd 线性一致性读(默认)
etcdctl get /foo
# 串行化读(性能更高,可能读到旧数据)
etcdctl get /foo --endpoints=http://localhost:2379 --consistency=sLease 机制
etcd 的租约与 Raft 紧密结合:
1. 客户端创建租约,指定 TTL
2. Leader 维护租约到期时间
3. Leader 崩溃时,新 Leader 从日志恢复租约状态
4. 租约到期后,Leader 删除关联的键故障处理
Leader 故障
Leader 故障恢复:
1. Follower 选举超时
2. 发起选举,获得多数票
3. 新 Leader 上任
4. 新 Leader 发送心跳,同步日志Follower 故障
Follower 故障恢复:
1. Leader 持续重试 AppendEntries
2. Follower 重启后,从 Leader 同步缺失日志
3. 日志匹配后继续正常工作网络分区
网络分区处理:
1. 少数派分区的 Leader 无法提交日志(无多数确认)
2. 多数派选举新 Leader
3. 网络恢复后,旧 Leader 发现更高 term
4. 旧 Leader 转为 Follower,回滚未提交日志性能优化
批量处理
- Leader 批量发送日志给 Follower
- 减少网络往返次数
管道化
- Leader 不等待 Follower 确认就发送下一批日志
- 提高吞吐量
写入优化
go
// etcd 配置
type Config struct {
// 每批最大条目数
MaxBatchSize int
// 批量等待时间
MaxBatchWait time.Duration
}监控指标
bash
# 查看集群健康状态
etcdctl endpoint health --cluster
# 查看 Raft 状态指标
curl http://localhost:2379/metrics | grep etcd_server_has_leader
curl http://localhost:2379/metrics | grep etcd_server_leader_changes_seen_total
# 关键指标
# - etcd_server_has_leader: 是否有 Leader
# - etcd_server_leader_changes_seen_total: Leader 变更次数
# - etcd_raft_commit_index: 已提交日志索引
# - etcd_raft_applied_index: 已应用日志索引总结
Raft 算法通过以下机制保证分布式一致性:
- Leader 选举:选出一个 Leader 协调日志复制
- 日志复制:Leader 将客户端操作复制到多数节点
- 安全性规则:保证已提交日志不被覆盖
- Term 机制:检测过时 Leader,防止脑裂
etcd 基于 Raft 实现了:
- 强一致性读写
- 高可用性(容忍少数节点故障)
- 线性一致性保证
理解 Raft 对于正确使用和运维 etcd 至关重要。在下一章中,我们将学习如何搭建和管理 etcd 集群。