2. etcd 架构与数据模型:Raft 协议简述、MVCC 机制、key-value 存储结构、watch 机制

etcd 是一个强一致性的分布式键值存储系统,其核心架构围绕 Raft 共识协议、MVCC(多版本并发控制)、有序的 key-value 存储以及高效的 Watch 机制构建。理解这些底层原理,是优化备份 RPO(Recovery Point Objective)与镜像拉取流程的前提。

2.1 Raft 协议简述

Raft 是 etcd 实现数据一致性的基石。它将分布式共识问题分解为 Leader 选举、日志复制和安全性三个子问题。在 etcd 集群中,所有写操作必须由 Leader 节点处理,Leader 将操作封装为日志条目并复制到 Follower 节点,当多数节点(Quorum)确认写入后,该操作才算提交成功。

RPO 关联: 备份操作通常从 Follower 节点读取快照,避免对 Leader 的写入性能造成影响。理解 Raft 的日志复制机制,有助于设计“无锁”或“低锁”的备份策略。
Raft 日志复制流程 Raft 日志复制流程 Leader 节点 Follower 节点 A Follower 节点 B 日志条目 (Term 3) 日志条目 (Term 3) 日志条目 (Term 3) 提交确认 (ACK) 提交确认 (ACK) Leader 接收写请求 → 追加日志 → 并行复制到 Follower → 等待多数 ACK → 提交并应用到状态机

2.2 MVCC 机制

etcd 使用 MVCC 来支持并发读写,而不会阻塞。每个 key 的每次修改都会生成一个新的版本号(revision),旧版本数据不会被立即删除,而是保留一段时间(通过压缩机制清理)。

  • 版本号(revision): 全局递增的整数,用于标识每一次事务操作。
  • 子版本号(sub-revision): 在同一事务内,对多个 key 的修改会共享同一个 revision,但通过 sub-revision 区分。
  • 压缩(Compaction): 定期清理历史版本,释放存储空间。压缩频率直接影响 RPO 和存储成本。
RPO 陷阱: 如果备份操作在压缩之后进行,将无法恢复被压缩的历史版本。因此,备份策略必须与压缩窗口对齐,确保在压缩前完成快照或增量备份。

2.3 key-value 存储结构

etcd 的底层存储基于 BoltDB(etcd v3 使用 bbolt),这是一个嵌入式、持久化的 key-value 数据库。数据以 有序的字节数组 形式存储,支持范围查询和前缀扫描。

概念 说明 备份影响
Bucket 逻辑命名空间,类似表。etcd 主要使用 key 和 meta 两个 bucket。 备份需要遍历所有 bucket,确保数据完整性。
Key 用户存储的键,以原始字节形式存储,支持前缀匹配。 范围扫描备份时,key 的有序性可提高 I/O 效率。
Value 用户存储的值,与 key 关联。 大 value 会影响备份速度和网络传输。
Revision 全局版本号,作为 MVCC 的索引。 增量备份依赖 revision 来识别变更。
etcd 存储结构 etcd 存储结构(BoltDB) BoltDB 文件 (db) Bucket: key /registry/pods/default/my-pod /registry/deployments/default/nginx ... Bucket: meta revision: 1024 txnID: 512 compactedRev: 1000

2.4 Watch 机制

Watch 是 etcd 提供的一种事件驱动的订阅机制,客户端可以监听某个 key 或 key 前缀的变化。当被监听的 key 发生创建、更新、删除操作时,etcd 会向客户端推送事件。

  • 事件类型: PUT(创建/更新)、DELETE(删除)。
  • 版本控制: 客户端可以指定从某个 revision 开始监听,实现“断点续传”式的增量同步。
  • 流式推送: 基于 gRPC 双向流,保持长连接,减少轮询开销。
RPO 优化点: 在镜像拉取流程中,可以利用 Watch 机制实时感知 etcd 数据变更,实现“准实时”增量备份,将 RPO 从分钟级降低到秒级。

Watch 的实现依赖于 MVCC 的 revision 序列。etcd 在内存中维护一个 事件历史缓冲区,当客户端 Watch 的 revision 落后于当前最新 revision 时,etcd 会从历史记录中查找并推送事件;如果历史记录已被压缩,则返回错误,客户端需要重新进行全量同步。

// 示例:使用 etcdctl 进行 Watch
etcdctl watch /registry/pods/default/ --prefix --rev=1000
// 输出示例:
// PUT /registry/pods/default/my-pod
// {"metadata": {"uid": "abc-123"}, "spec": {...}}
// DELETE /registry/pods/default/old-pod