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 本身不存储空目录 | 前缀扫描可快速定位业务模块数据 |
/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 |
物理回收磁盘空间,但历史版本不可恢复 |
3.2.3 MVCC 在灾备中的价值
MVCC 允许备份工具通过 指定 revision 进行一致性快照读取,无需加锁。例如,etcdctl snapshot save 在内部使用 MVCC 读取当前 revision 的完整快照,保证备份数据的一致性。
# 基于指定 revision 读取历史数据(PITR 查询)
etcdctl get /app/config --rev=800
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)
}
}
}
}
3.4 数据模型与灾备 RPO 的关系总结
下表梳理了 etcd 数据模型各组件对灾备 RPO 的直接影响:
| 组件 | 对 RPO 的贡献 | 优化方向 |
|---|---|---|
| MVCC + revision | 支持增量备份与 PITR,RPO 可达秒级 | 合理设置压缩保留窗口,避免数据空洞 |
| Watch 机制 | 实时推送变更事件,减少轮询延迟 | 使用前缀 watch 批量监听,降低连接数 |
| 租约(Lease) | 自动过期清理,保障灾备切换时数据一致性 | TTL 设置不宜过长,避免脏数据残留 |
| Key 前缀结构 | 支持按业务模块粒度备份与恢复 | 设计统一前缀规范,便于灾备脚本处理 |