3. etcd 数据模型:key-value 存储结构、版本号与 MVCC、租约(Lease)与 watch 机制

etcd 作为分布式键值存储系统,其数据模型直接决定了灾备场景下的恢复粒度与缓存一致性策略。理解 key-value 存储结构、多版本并发控制(MVCC)以及租约与 watch 机制,是优化灾备 RPO 的理论基础。

3.1 分层 key-value 存储结构

etcd 将所有数据组织为扁平的 key-value 对,但内部采用 分层目录结构 的逻辑视图。Key 被设计为类似文件系统的路径格式,例如 /coredns/config 或 /calico/ipam/v2/host/。这种设计便于按前缀进行范围查询与 watch。

概念 说明 灾备影响
Key 字节数组,最大 1.5 MB;推荐使用可读字符串路径 备份时需保留完整 key 路径以恢复精确状态
Value 字节数组,最大 1.5 MB;存储序列化后的配置或状态 增量备份需对比 value 变更,减少传输量
Key-Value 对 每个 key 对应一个当前版本的值,以及历史版本链 MVCC 支持时间点恢复(PITR),降低 RPO
目录(逻辑) 通过 key 前缀模拟目录,etcd 本身不存储空目录 前缀扫描可快速定位业务模块数据
工程提示: 在灾备场景中,建议对关键业务 key 使用统一前缀(如 /production/redis/),以便在恢复时通过 etcdctl get --prefix 精准拉取子集数据,避免全量恢复带来的带宽浪费。

3.2 版本号与 MVCC 机制

etcd 采用 多版本并发控制(MVCC) 来管理 key 的每一次变更。每个 key 的每次写操作都会生成一个新的版本号,旧版本数据不会被立即删除,而是保留在历史版本链中。这一机制是时间点恢复(PITR)和增量备份的核心支撑。

3.2.1 版本号体系

etcd 维护三个层次的版本号:

  • 全局版本号(revision): 单调递增的整数,每次事务提交时递增。用于标识整个存储的全局时序。
  • Key 子版本号(mod_revision): 该 key 最近一次被修改时的全局 revision。
  • Key 创建版本号(create_revision): 该 key 被创建时的全局 revision。
# 查看 key 的版本信息
etcdctl get /app/config -w json
{
  "header": {
    "revision": 1024,          # 当前全局版本号
    "raft_term": 5
  },
  "kvs": [
    {
      "key": "L2FwcC9jb25maWc=",
      "create_revision": 512,   # 创建时的 revision
      "mod_revision": 1024,     # 最近修改时的 revision
      "version": 3,             # 该 key 被修改的次数
      "value": "e30="
    }
  ]
}

3.2.2 MVCC 与历史版本压缩

etcd 通过 压缩(Compaction) 机制定期清理历史版本,防止存储无限增长。压缩操作会删除指定 revision 之前的所有历史版本,释放磁盘空间。

操作 命令示例 对灾备的影响
手动压缩 etcdctl compaction 2048 删除 revision ≤ 2048 的历史版本,PITR 无法恢复到该点之前
自动压缩 --auto-compaction-retention=1h 保留最近 1 小时的历史版本,RPO 受限于保留窗口
取消压缩 etcdctl compaction --physical 物理回收磁盘空间,但历史版本不可恢复
关键风险: 如果灾备方案依赖 etcd 的 MVCC 做增量备份或 PITR,必须确保自动压缩保留窗口大于备份周期。例如,每小时备份一次,则压缩保留窗口应 ≥ 2 小时,避免备份窗口与压缩窗口重叠导致数据空洞。

3.2.3 MVCC 在灾备中的价值

MVCC 允许备份工具通过 指定 revision 进行一致性快照读取,无需加锁。例如,etcdctl snapshot save 在内部使用 MVCC 读取当前 revision 的完整快照,保证备份数据的一致性。

# 基于指定 revision 读取历史数据(PITR 查询)
etcdctl get /app/config --rev=800
优化 RPO 实践: 利用 MVCC 的 revision 机制,可以实现增量备份:记录上次备份的 revision,下次只拉取 revision 大于该值的变更事件。结合 watch 机制,可将 RPO 从分钟级降低到秒级。

3.3 租约(Lease)与 watch 机制

租约和 watch 是 etcd 实现分布式协调与实时感知的核心机制,直接影响缓存命中率与灾备切换速度。

3.3.1 租约(Lease)

租约是一个 带 TTL 的轻量级对象,用于管理 key 的自动过期。租约可以关联多个 key,当租约过期或被撤销时,所有关联的 key 自动删除。

属性 说明 灾备场景应用
TTL(秒) 租约存活时间,到期自动失效 服务注册:节点宕机后 TTL 到期自动摘除
KeepAlive 客户端定期续约,维持租约存活 灾备切换:主节点续约失败,备节点接管
关联 key 一个租约可绑定多个 key 批量清理:租约过期时关联配置一并删除
# 创建 10 秒 TTL 的租约
etcdctl lease grant 10
lease 694d7b2f8a1f

# 将 key 绑定到租约
etcdctl put /service/leader --lease=694d7b2f8a1f

# 续约
etcdctl lease keep-alive 694d7b2f8a1f

3.3.2 watch 机制

watch 允许客户端监听 key 或前缀的变更事件(创建、更新、删除)。etcd 保证事件按 revision 顺序 投递,且支持从指定 revision 开始回溯。

  • 事件类型: PUT(创建/更新)、DELETE(删除)
  • 监听粒度: 单 key、前缀、范围
  • 回溯能力: 指定 --rev 参数从历史 revision 开始监听
# 监听 /app/config 的变更,从当前 revision 开始
etcdctl watch /app/config

# 监听 /app/ 前缀下的所有变更,从 revision 1000 开始回溯
etcdctl watch /app/ --prefix --rev=1000

3.3.3 租约与 watch 在缓存命中中的协同

在灾备缓存场景中,租约用于控制缓存数据的有效期,watch 用于实时刷新缓存,两者结合可大幅提升缓存命中率。

// 伪代码:缓存刷新逻辑
func watchAndRefreshCache(client *etcd.Client, key string) {
    // 1. 初始加载当前值
    resp, _ := client.Get(ctx, key)
    cache.Set(key, resp.Kvs[0].Value)
    
    // 2. 从当前 revision 开始 watch
    watchChan := client.Watch(ctx, key, clientv3.WithRev(resp.Header.Revision+1))
    
    // 3. 持续监听变更
    for wresp := range watchChan {
        for _, ev := range wresp.Events {
            switch ev.Type {
            case mvccpb.PUT:
                cache.Set(key, string(ev.Kv.Value))
            case mvccpb.DELETE:
                cache.Delete(key)
            }
        }
    }
}
优化 RPO 技巧: 在灾备切换时,备集群可以通过 watch 从主集群的最后一个已知 revision 开始回溯,确保不丢失切换期间的任何变更。配合租约的 TTL 检测,可实现秒级故障感知与缓存刷新。

3.4 数据模型与灾备 RPO 的关系总结

下表梳理了 etcd 数据模型各组件对灾备 RPO 的直接影响:

组件 对 RPO 的贡献 优化方向
MVCC + revision 支持增量备份与 PITR,RPO 可达秒级 合理设置压缩保留窗口,避免数据空洞
Watch 机制 实时推送变更事件,减少轮询延迟 使用前缀 watch 批量监听,降低连接数
租约(Lease) 自动过期清理,保障灾备切换时数据一致性 TTL 设置不宜过长,避免脏数据残留
Key 前缀结构 支持按业务模块粒度备份与恢复 设计统一前缀规范,便于灾备脚本处理
核心结论: 理解 etcd 的 MVCC 版本链和 watch 事件流,是构建低 RPO 灾备方案的前提。通过 revision 驱动的增量备份 + watch 驱动的缓存实时刷新,可以将理论 RPO 压缩到秒级甚至亚秒级。