简介
etcd 是一个分布式一致性键值存储,由 CoreOS 发起、现由 CNCF 托管,使用 Go 编写。它基于 Raft 一致性算法管理高可用复制日志,广泛应用于 Kubernetes 集群元数据存储、服务发现与配置中心等场景。
核心特性:
- 简单:基于 gRPC 的清晰 API(KV、Watch、Lease、Auth 等)
- 安全:支持 TLS 与客户端证书认证
- 可靠:通过 Raft 在多数节点间复制数据,保证强一致性
- 高性能:v3 API 采用 MVCC 多版本并发控制,读写分离,适合读多写少场景
本文从整体架构、核心组件、数据模型与请求流程等方面介绍 etcd 的架构设计。
整体架构
etcd 从逻辑上可分为 API 层、Raft 层和存储层:客户端请求经 gRPC 进入,经 Raft 达成一致后,再在存储层持久化并对外提供查询。
flowchart TB
subgraph Client["客户端"]
etcdctl
SDK
end
subgraph API["API 层"]
KV[KV Server]
Watch[Watch Server]
Lease[Lease Server]
Auth[Auth Server]
end
subgraph Raft["Raft 层"]
RaftNode[Raft 模块]
Apply[Apply 模块]
end
subgraph Storage["存储层"]
WAL[WAL 预写日志]
MVCC[MVCC 存储]
BoltDB[(BoltDB)]
end
etcdctl --> KV
SDK --> KV
KV --> RaftNode
RaftNode --> Apply
Apply --> WAL
Apply --> MVCC
MVCC --> BoltDB
- API 层:对外暴露 gRPC 接口(KV、Watch、Lease、Auth 等),将请求转为 Raft 提案或直接读本地状态机。
- Raft 层:负责 Leader 选举、日志复制与提交;提交后的日志由 Apply 模块应用到本地状态机。
- 存储层:WAL 保证崩溃可恢复;MVCC + BoltDB 提供多版本键值存储与持久化。
代码:API 层 Put 到 Raft 提案
gRPC 收到 Put 后,封装为 InternalRaftRequest,再通过 raftRequest → processInternalRaftRequestOnce 序列化并提交给 Raft(server/etcdserver/v3_server.go):
1 | // Put 入口:写请求统一走 Raft |
核心组件
Raft 模块
etcd 使用 Raft 实现分布式共识,保证多副本间数据一致。
- 节点角色:Leader(处理写请求、复制日志)、Follower(接收复制、参与投票)、Candidate(参与选举)。
- 任期(Term):单调递增,用于区分不同选举轮次。
- 日志复制:写请求先落盘为 Raft 日志,由 Leader 复制到多数节点,提交后再应用到各节点状态机。
- Leader 选举:Follower 超时未收到 Leader 心跳则变为 Candidate,发起投票;获得多数票的节点成为新 Leader。
stateDiagram-v2
[*] --> Follower
Follower --> Candidate: 选举超时
Candidate --> Leader: 获得多数票
Candidate --> Follower: 发现更大 Term 或选举超时
Leader --> Follower: 发现更大 Term
代码:Raft Ready 与 WAL 持久化
Raft 层在 start() 循环中处理 Ready():先写快照(若有)、再 Save(HardState, Entries) 到 WAL,最后把已提交条目通过 applyc 发给 Apply 模块(server/etcdserver/raft.go):
1 | // toApply:已提交条目与快照,交给 Apply 消费 |
存储层:WAL 与 BoltDB
WAL(Write Ahead Log)
- 作用:在修改内存/磁盘状态前,先把 Raft 状态与日志条目写入 WAL,保证崩溃后可按顺序重放,恢复状态。
- 文件:通常位于
member/wal/,命名如0000000000000000-0000000000000000.wal(seq-index.wal)。 - 内容:Raft 硬状态(如 term、vote)、日志条目、快照元数据等,记录 8 字节对齐,带 CRC 校验。
BoltDB(持久化后端)
- 作用:etcd 使用 bbolt(B+ 树)作为持久化 KV 引擎,存放已应用到状态机的数据、成员与鉴权信息、以及
consistent_index(最后应用的 WAL 位置)等。 - 文件:主库为
member/snap/db;从 Leader 拉取的完整快照为member/snap/*.snap.db,用于落后节点追赶或恢复。
MVCC 多版本存储
v2 使用全局大锁,并发与 Watch 能力受限。v3 引入 MVCC,实现无锁读与完整历史版本查询。
- Revision(版本号):全局单调递增的 64 位整数,每次事务性写(Put/Delete 等)产生新 Revision。
- treeIndex:内存中的 B-tree,维护 用户 key → revision 的索引;每个 key 对应一个 keyIndex,记录其多代(generations)的 revision 历史。
- Backend(BoltDB):实际存储键值,key 为
revision编码,value 为具体数据;通过 treeIndex 从用户 key 查到 revision,再在 Backend 中取 value。
因此:
- 读:根据 treeIndex 找到当前(或指定 revision)的 revision,再读 Backend,无需写锁。
- 写:分配新 revision,更新 treeIndex 和 Backend,与读并发互不阻塞。
- Watch:可按 revision 订阅,避免事件丢失,并支持历史事件回放。
代码:Apply 解析 Raft 条目并应用
Apply 模块从 Raft 拿到已提交的 Entry,反序列化为 InternalRaftRequest,再交给具体 Applier 执行(如 KV Put)(server/etcdserver/apply/apply.go):
1 | func Apply(lg *zap.Logger, e *raftpb.Entry, uberApply UberApplier, w wait.Wait, shouldApplyV3 membership.ShouldApplyV3) (ar *Result, id uint64) { |
代码:MVCC store 与 treeIndex
MVCC 存储由 store 内嵌 kvindex(treeIndex)和 Backend 组成;Revision 由 currentRev 维护(server/storage/mvcc/kvstore.go、index.go):
1 | // store:读视图 + 写视图 + 索引 + 后端 |
数据模型
Revision
- 每次写事务提交时,全局 Revision +1。
- 一次事务中多个 key 的修改共享同一 Revision。
- 客户端可通过 revision 做快照读、历史查询和 Watch 起点。
keyIndex 与 Generations
每个用户 key 在 treeIndex 中对应一个 keyIndex,包含:
- key:用户可见的 key。
- modified revision:该 key 最后一次修改对应的 revision。
- generations:该 key 的“代”列表;每次从无到有创建为一代开始,删除为一代结束,用于压缩与历史清理。
这样可支持“按 key 查历史版本”“按 revision 查全局快照”以及 Compact 等运维能力。
代码:keyIndex 与 generation
每个用户 key 对应一个 keyIndex,内部按“代”(generation)组织版本;Put 追加版本,Tombstone 结束当前代并开启新的空代(server/storage/mvcc/key_index.go):
1 | // keyIndex:key 的多代版本信息 |
请求流程
写请求(Put/Delete 等)
- 客户端通过 gRPC 调用 KV API。
- KV Server 将请求封装为 Raft 提案,提交给 Raft 模块。
- Leader 将提案写入本地 WAL,并复制到多数 Follower。
- 多数节点持久化并确认后,Leader 将日志标记为已提交。
- Apply 模块将已提交日志应用到 MVCC:更新 treeIndex 与 BoltDB。
- 返回成功给客户端。
sequenceDiagram
participant C as 客户端
participant KV as KV Server
participant R as Raft
participant A as Apply
participant M as MVCC
C->>KV: Put(key, value)
KV->>R: Propose(entry)
R->>R: 写 WAL、复制日志
R->>R: 多数确认、提交
R->>A: 已提交日志
A->>M: Apply(treeIndex + BoltDB)
A->>KV: 完成
KV->>C: 成功
读请求(Range)
- 串行读(Serializable):走 Raft 读,保证线性一致:先确认当前 Leader 与已提交进度,再读状态机,保证能看到此前全部已提交写。
- 本地读:仅读本节点状态机,延迟低但不保证线性一致,适合对一致性要求稍弱的场景。
代码:Range 与线性一致读
当 RangeRequest.Serializable == false 时,先通过 linearizableReadNotify 与 Raft 达成“当前已提交进度”,再在本地 KV 上执行 Range,从而得到线性一致读(server/etcdserver/v3_server.go):
1 | func (s *EtcdServer) Range(ctx context.Context, r *pb.RangeRequest) (*pb.RangeResponse, error) { |
集群与高可用
- 节点数量:建议 3、5、7 等奇数,便于多数派决策(如 3 节点容忍 1 故障,5 节点容忍 2 故障)。
- 数据一致性:写仅在多数节点持久化并提交后才返回,读可选用串行读以获线性一致性。
- 故障恢复:节点重启后从 WAL 重放;落后过多的节点可从 Leader 拉取快照(
snap.db)再追 WAL,加快恢复。
小结
| 层次 | 组件 | 职责简述 |
|---|---|---|
| API 层 | gRPC Server | 暴露 KV/Watch/Lease/Auth 等接口 |
| Raft 层 | Raft + Apply | 共识、日志复制、应用到状态机 |
| 存储层 | WAL | 持久化 Raft 日志,保证可恢复 |
| 存储层 | MVCC + BoltDB | 多版本索引与键值持久化 |
etcd 通过 Raft 保证多副本一致,通过 WAL + BoltDB 保证持久化与恢复,通过 MVCC 实现高并发读与历史版本,共同构成其高可用、强一致的分布式键值存储架构。配合 etcdctl 命令行交互 可完成日常运维与排障。