Raft
深度重写 已把 PreVote、ReadIndex、ConfChange V2
等工程补丁钉到 go.etcd.io/raft/v3 源码精度;Raft
实现拆解 走过 raft.Node 的消息循环。但 etcd
服务器如何把 gRPC 请求接到 Raft、Follower
收到写请求后如何转发、Learner 为何不参与
quorum——仍需要单独一篇,否则排障时会把「Raft
库行为」与「etcdserver 路由策略」混为一谈。
本文钉三件事:整个集群只有一个 Raft
组;EtcdServer 与 raft.Node
的职责边界;Leader / Follower / Learner
三种角色下的 request 路由。与 distributed/13
的分工口令:13 讲 Raft 算法与库;本篇讲 etcd
如何嵌入该库。
本篇在系列中的位置
篇目 核心内容 第 1 篇 · 生产全景 缺口、五轴、16 篇路线 第 2 篇 · 单 Raft 组与服务器角色 Leader/Follower/Learner;request 路由 第 3 篇 · WAL 与快照 WAL 段、commit vs applied
版本锚定:etcd v3.5.33(源码 tag
v3.5.33)。EtcdServer定义见server/etcdserver/server.go;Raft 嵌入见server/etcdserver/raft.go;成员 Learner 标记见server/etcdserver/api/membership/member.go的RaftAttributes.IsLearner。无真实集群则不粘贴伪造etcdctl endpoint status的 Raft 字段。
一、单 Raft 组:与 Multi-Raft 的分叉点
etcd 集群中所有 key-value
数据由同一个 Raft
实例管理。无论集群是 3 节点还是 5
节点,日志条目顺序全局唯一;MVCC 的 main
revision 递增也建立在这份全局日志之上。
这与 TiKV 的 Multi-Raft(每个 Region 独立 Raft 组)形成鲜明对比——也是 分布式 KV 对比 里 etcd 写吞吐有硬顶的根本原因:写路径无法按 key 分片并行复制。
flowchart TB
subgraph cluster ["Single Raft Group"]
L["Leader"]
F1["Follower"]
F2["Follower"]
LR["Learner optional"]
end
C["gRPC clients"] --> L
C --> F1
C --> F2
L -->|"MsgApp replicate"| F1
L -->|"MsgApp replicate"| F2
L -->|"replicate no vote"| LR
工程后果:
| 属性 | 单 Raft 组收益 | 代价 |
|---|---|---|
| 全局 Revision 全序 | Watch、Txn、resourceVersion 语义简单 | 写吞吐不随节点数线性扩展 |
| 运维模型 | member add/remove 一套流程 | 大 keyspace 仍共享一份 WAL |
| 故障域 | quorum 丢失即整集群不可写 | 不能按租户隔离 Raft 组 |
Li et al.(;login: 2017)把 etcd 定位为配置与服务发现的小规模 KV——单 Raft 组是刻意的设计选择,不是「还没做分片」。当 quota 与单写者(bbolt)同时触顶时,应评估迁 Multi-Raft 系统(TiKV)或拆分 etcd 集群,而不是继续加 Follower 期望写性能提升。
二、EtcdServer
与 go.etcd.io/raft/v3 的边界
EtcdServer 是 etcd
进程的核心状态机宿主。v3.5.33 中其关键字段包括:
// etcd-io/etcd v3.5.33, server/etcdserver/server.go(节选)
type EtcdServer struct {
appliedIndex uint64 // MVCC 已应用到的 Raft index
committedIndex uint64 // Raft 已提交的 index
term uint64
lead uint64 // 当前 Leader 的 member ID
r raftNode
kv mvcc.WatchableKV
lessor lease.Lessor
be backend.Backend
// ...
}分工表——排障时先判断问题落在哪一侧:
| 层级 | 包/组件 | 职责 | 典型失败 |
|---|---|---|---|
| gRPC/API | server/etcdserver/v3_server.go 等 |
鉴权、请求校验、路由 | 请求过大、auth 拒绝 |
| 共识 | go.etcd.io/raft/v3 via
raftNode |
选举、日志复制、ReadIndex | no leader、term 跳变 |
| 持久化 | server/wal、snap.Snapshotter |
WAL 段、快照 | corrupt WAL(见第 3 篇) |
| 状态机 | applyV3、server/mvcc |
把已提交 Entry 应用到 KV | apply lag、quota |
| 成员 | membership.RaftCluster |
member 元数据、Learner 标记 | join 卡住、promote 失败 |
raftNode(server/etcdserver/raft.go)是
etcdserver 与 raft.Node 之间的适配层:它消费
Ready() 中的
Entries、HardState、Snapshot,写入
WAL,经 rafthttp 与 peer 交换消息,再把可 apply
的条目交给 EtcdServer。
边界口令:
- Raft 库不知道 MVCC:它只看到
[]byte的Entry.Data。 - EtcdServer
不实现选举细节:PreVote、随机超时、MsgApp
流水线都在
raft包内(distributed/13 已展开)。 - committedIndex 由 Raft 侧推进,appliedIndex 由 apply 协程推进——二者分叉是 apply 轴与 WAL 轴的分界(第 3、6 篇展开)。
Ongaro & Ousterhout(2014)的复制状态机模型在这里落地:Raft 保证日志一致;etcd 的 MVCC store 是状态机实现。
三、Leader、Follower、Learner 三种角色
3.1 Leader
Leader 是唯一接受写 propose 的节点(成员变更 ConfChange 亦然)。客户端 gRPC 连到 Leader 时,写路径为:
KV/Txn/Compaction request
→ EtcdServer 序列化为 raftpb.Entry
→ raftNode.Propose
→ 复制到 quorum
→ committedIndex 推进
→ apply 协程 → MVCC/bbolt
Leader 还负责 ReadIndex 线性一致读(第 8 篇):在返回读结果前,需确认自己仍是 Leader 且已应用至 ReadIndex 指定的 commit 点。
EtcdServer.lead 字段(atomic)保存当前
Leader 的 member ID;term 保存当前 Raft
term。etcdctl endpoint status
在有真实环境时可对照
Raft Term、Raft Index、Leader
字段——本篇不粘贴伪造输出。
3.2 Follower
Follower 接收 Leader 的 MsgApp,持久化到
WAL,回复 ACK。它不直接 propose
客户端写请求。
当 gRPC 写请求落到 Follower 时,etcdserver
会转发给 Leader(经 rafthttp
或 gRPC
内部路径,取决于请求类型与版本配置)。客户端可能感知到额外
RTT,但不会破坏线性一致性。
Follower 可读:
- Serializable read:直接读本地 MVCC 状态——可能 stale(第 8 篇)。
- Linearizable read:走 ReadIndex 流程,仍由 Leader 协调。
3.3 Learner
Learner 是 non-voting member(Raft
论文扩展的工程化)。v3.5.33 在
membership.RaftAttributes 中标记:
// server/etcdserver/api/membership/member.go(节选)
type RaftAttributes struct {
PeerURLs types.URLs
IsLearner bool `json:"isLearner,omitempty"`
}| 能力 | Voting member | Learner |
|---|---|---|
| 接收日志复制 | 是 | 是 |
| 参与 quorum 计数 | 是 | 否 |
| 参与 Leader 选举投票 | 是 | 否 |
| 典型用途 | 生产副本 | 新节点 catch-up、跨地域观测副本 |
Learner 的设计动机是 member add 不扰动 quorum:先把新节点以 Learner 身份拉入并复制完全量日志,再 promote 为 voting member——避免 Ongaro 论文 §6 成员变更中「临时双 majority」的经典坑(ConfChange V2 在 distributed/13 已述)。
排障提示:Learner 节点
etcdctl endpoint status 可能显示较高
Raft Applied Index lag 但仍不参与选举;把它当
Follower 做 quorum 计算会误判集群健康。
四、Request 路由:写、读、Watch、Lease
sequenceDiagram
participant C as Client
participant S as EtcdServer
participant R as raftNode
participant MVCC as MVCC store
C->>S: Put / Txn
alt local is Leader
S->>R: Propose entry
R->>R: replicate to quorum
R->>S: committed entries
S->>MVCC: apply
else local is Follower
S->>S: forward to Leader
end
C->>S: Watch
S->>MVCC: watchableStore subscribe
Note over MVCC: no Raft for stream setup
C->>S: Lease KeepAlive
S->>S: lessor renew local path
Note over S: TTL path see part 10
4.1 写路径(Put / Txn / Delete / Compaction)
所有 mutating v3 请求最终变成 一条或多条 Raft Entry(Txn 是单个 Entry 内多个 op)。必须经过 Leader + quorum commit,再 apply。
Follower 收到写请求 → 转发 Leader → 与直连 Leader 等价,只是多一跳。
4.2 读路径(Get / Range)
读请求默认不走 Raft propose(除非触发
ReadIndex 线性一致读)。Serializable 读直接查本地
WatchableKV——在 Follower
上可能读到未追平的旧状态。
这是「apiserver 读 etcd 是否 stale」争论的工程根源(第 8、13 篇展开)。
4.3 Watch
Watch 建立 gRPC 流后,由 watchableStore 在
apply 之后推送事件。historical revision
追赶从 bbolt 读历史——不经过 Raft,但依赖 MVCC 尚未 compact
的版本保留。
4.4 Lease
Grant/Revoke 等会走
Raft;KeepAlive 在 Leader 本地 lessor
续期,不经 Raft 提交——Leader 切换时 TTL 语义会变化(第 10
篇)。这是 Lease 轴独立于 Raft 轴的关键原因。
五、与 distributed/13 的分工与开放问题
| 问题 | 去看 |
|---|---|
| PreVote、选举风暴、term 跳变 | distributed/13 §5 |
| ReadIndex / LeaseRead 实现 | distributed/13 + 本系列 08 |
| ConfChange V2、joint consensus | distributed/13 + 本系列 14 |
| 单组拓扑、Learner promote、request 转发 | 本篇 + 14 |
开放问题(有文献或源码依据):
- Learner 跨地域部署:复制 lag 不影响 quorum,但 promote 时仍需要一次 ConfChange——跨洋带宽下 promote 窗口多长才安全,官方文档给运维指南(B 级),无统一公式。
- Follower 读默认可用性:Serializable 读降低 Leader 负载,但 K8s 控制面是否应允许 follower 读 etcd 在工程上仍有争议——默认 apiserver 连 Leader 仍是常见部署。
- 单 Raft 组扩展性上限:Li et al. 2017 与 etcd 官方 Hardware guidelines 都指向磁盘 fsync 与 bbolt 单写者,而非 CPU——Multi-Raft 是架构分叉,不是调参能弥合的 gap。
六、本篇不写什么
- Raft 论文证明、PreVote 状态机全集(外链 distributed/13)。
- WAL 段布局与 crash recovery(见 第 3 篇)。
- MVCC Revision 与 keyIndex(见 第 4 篇)。
- ReadIndex 完整流程(见第 8 篇)。
- 伪造
etcdctl endpoint status输出。
参考资料
规范 / 官方文档 / 源码(A)
- etcd v3.5.33,tag
v3.5.33 server/etcdserver/server.go—EtcdServer结构server/etcdserver/raft.go—raftNode适配server/etcdserver/api/membership/member.go—IsLearnergo.etcd.io/etcd/raft/v3README @ tagv3.5.33
论文(A)
- Ongaro & Ousterhout, In Search of an Understandable Consensus Algorithm, USENIX ATC 2014 — §5 成员变更
- Li et al., etcd: A Distributed Key-Value Store, ;login: 2017 — 单 Raft 组架构
站内对照(A/B)
实验台账
- 本篇无集群实测;Raft 字段语义来自源码与官方文档,无伪造输出。
上一篇:生产全景
下一篇:WAL 与快照
读完这篇,下一步读什么
优先读同系列或同问题的下一篇,把单篇消费变成主题集群。
【etcd】生产全景:缺口、五轴坐标系与 16 篇路线
相对 distributed/50、39、13 钉清 etcd 生产内核缺口;定义 Raft/WAL/MVCC/Watch/Lease 五轴排障坐标系,给出 16 篇阅读路线与 K8s 控制面耦合指针。版本锚定 v3.5.33。
【etcd】运维与升级:member change、backup/restore 与 3.5→3.6/3.7 门
member add/remove/replace 与 learner promote 检查单;snapshot backup/restore 及 K8s 语境 revision bump;3.5.33 补丁线与 3.6/3.7 官方升级门禁项,不超前写未钉 tag 能力。
【etcd】排障五轴:Raft/WAL/MVCC/Watch/Lease 口令表
按 Raft、WAL、MVCC、Watch、Lease 五轴做症状否证;解释 etcdctl endpoint status/health 字段语义;并对照 K8s 控制面 apiserver 超时与 Node Lease 漂移。
【etcd】WAL 与快照:预写日志、crash recovery 与 commit vs applied
拆解 etcd v3.5.33 的 WAL 段文件、Raft snapshot 触发与重启 recovery 顺序;钉清 committed index 与 applied index 分叉对排障与 K8s 控制面的含义。