核心概念 - 数据模型
etcd 的数据模型是其核心基础,理解数据模型对于正确使用 etcd 至关重要。本章将深入探讨 etcd 的数据存储结构、版本机制和查询能力。
数据模型概览
etcd 采用简单的键值对模型,但通过丰富的元数据和索引机制提供了强大的查询能力。
基本结构
Key: []byte // 键,字节数组
Value: []byte // 值,字节数组
CreateRevision // 创建时的全局版本号
ModRevision // 最后修改时的全局版本号
Version // 键的版本号(从 1 开始)
Lease // 关联的租约 ID(0 表示无租约)键空间特点
- 扁平命名空间:键是字节数组,没有层级结构
- 有序性:键按字典序排序,支持范围查询
- 原子性:每个操作都是原子的
- 版本化:每次修改都会生成新的版本号
Revision(修订版本)
Revision 是 etcd 中最重要的概念之一,它是全局递增的 64 位整数。
全局版本号
时间线:
─────────────────────────────────────────────────────>
Revision 1 Revision 2 Revision 3 Revision 4
Put A=1 Put A=2 Put B=1 Del A- 每次修改操作(Put、Delete、Txn)都会增加 revision
- 所有键共享同一个 revision 序列
- 用于实现 MVCC(多版本并发控制)
键的版本信息
go
type KeyValue struct {
Key []byte // 键
Value []byte // 值
CreateRevision int64 // 创建时的 revision
ModRevision int64 // 最后修改时的 revision
Version int64 // 该键的修改次数
Lease int64 // 租约 ID
}示例演进:
操作序列:
1. Put /foo = "bar1" → Revision=1, CreateRev=1, ModRev=1, Version=1
2. Put /foo = "bar2" → Revision=2, CreateRev=1, ModRev=2, Version=2
3. Put /baz = "qux" → Revision=3, CreateRev=3, ModRev=3, Version=1
4. Put /foo = "bar3" → Revision=4, CreateRev=1, ModRev=4, Version=3
5. Del /foo → Revision=5, 键被删除历史查询
使用 revision 可以查询历史版本:
bash
# 查询 revision 2 时的值
etcdctl get /foo --rev=2
# 查询所有历史版本
etcdctl get /foo --write-out="json" --rev=1数据存储
WAL(预写日志)
Write-Ahead Log 保证数据持久性和崩溃恢复。
┌──────────────────────────────────────┐
│ WAL 日志 │
│ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │Entry1│ │Entry2│ │Entry3│ ... │
│ └──────┘ └──────┘ └──────┘ │
│ 每个修改操作都先写入 WAL │
└──────────────────────────────────────┘特点:
- 所有修改操作先写入 WAL,再应用到内存
- 崩溃后可以通过 WAL 恢复数据
- 顺序写入,性能高
BBolt 数据库
etcd 使用 BBolt 存储持久化数据。
内存索引 BBolt 存储
┌──────────────┐ ┌──────────────┐
│ Key → Revision│────>│ Revision → │
│ (B+ Tree) │ │ KeyValue │
└──────────────┘ └──────────────┘存储结构:
- 键索引:内存中的 B+ 树,键 → revision
- 数据存储:BBolt 存储,revision → KeyValue
快照(Snapshot)
定期创建快照,压缩历史数据。
bash
# 手动创建快照
etcdctl snapshot save backup.db
# 查看快照状态
etcdctl snapshot status backup.db查询能力
精确查询
bash
# 查询单个键
etcdctl get /foo
# 查询并显示详细信息
etcdctl get /foo --write-out=json范围查询
利用键的有序性进行范围查询:
bash
# 查询 [key, end_key) 范围
etcdctl get /foo /foo100
# 查询指定前缀
etcdctl get /foo --prefix
# 查询所有键
etcdctl get "" --prefix
# 限制返回数量
etcdctl get /foo --prefix --limit=10
# 排序(默认升序)
etcdctl get /foo --prefix --sort-by=KEY
etcdctl get /foo --prefix --sort-by=VERSION --order=DESCEND版本查询
bash
# 查询指定 revision 的数据
etcdctl get /foo --rev=2
# 查询历史版本(仅获取 modRevision)
etcdctl get /foo --write-out=json | jq '.kvs[0].mod_revision'计数查询
bash
# 统计键数量
etcdctl get /foo --prefix --count-only数据操作
写操作
bash
# 基本写入
etcdctl put /foo "bar"
# 带租约写入
LEASE_ID=$(etcdctl lease grant 60 | awk '{print $2}')
etcdctl put --lease=$LEASE_ID /foo "bar"
# 带先前值检查(CAS)
etcdctl put /foo "new" --prev-value="old"删除操作
bash
# 删除单个键
etcdctl del /foo
# 删除前缀
etcdctl del /foo --prefix
# 范围删除
etcdctl del /foo /foo100
# 返回被删除的键数量
etcdctl del /foo --prefix
# 输出: 3租约操作
bash
# 创建租约(TTL 秒)
LEASE_ID=$(etcdctl lease grant 60 | awk '{print $2}')
# 输出: lease 7581594885984314185 granted with TTL(60s)
# 续租
etcdctl lease keep-alive $LEASE_ID
# 查询租约信息
etcdctl lease timetolive $LEASE_ID
# 撤销租约
etcdctl lease revoke $LEASE_ID
# 查询租约关联的键
etcdctl lease list-keys $LEASE_ID事务
etcd 提供原子的事务操作,支持条件判断。
事务结构
Txn
├── Compare (条件判断)
│ ├── key1.value == "expected1"
│ ├── key2.createRevision > 0
│ └── key3.version < 10
├── Success (条件满足时执行)
│ ├── Put key1 "new_value"
│ ├── Put key2 "another_value"
│ └── Get key3
└── Failure (条件不满足时执行)
├── Get key1
└── Get key2etcdctl 事务示例
bash
# 简单事务
etcdctl txn <<EOF
compare:
value("/foo") = "bar"
success requests:
put /foo "new-bar"
failure requests:
get /foo
EOFGo 客户端事务
go
_, err := cli.Txn(ctx).
If(
clientv3.Compare(clientv3.Value("/foo"), "=", "bar"),
).
Then(
clientv3.OpPut("/foo", "new-bar"),
).
Else(
clientv3.OpGet("/foo"),
).
Commit()数据模型设计最佳实践
键命名规范
推荐:
/services/userservice/instance1
/services/userservice/instance2
/config/database/host
/config/database/port
不推荐:
userservice-instance1
database.host使用前缀组织数据
/services/
├── /userservice/
│ ├── instance1
│ └── instance2
└── /orderservice/
├── instance1
└── instance2控制单个键的值大小
- 单个值建议不超过 1MB
- 大数据应存储在对象存储中,etcd 只存引用
合理使用租约
go
// 临时数据使用租约(如服务注册)
lease, _ := cli.Grant(ctx, 60) // 60秒租约
cli.Put(ctx, "/services/app/instance1", addr, clientv3.WithLease(lease.ID))
// 永久数据不使用租约
cli.Put(ctx, "/config/database/host", "localhost")性能考虑
读优化
- 使用范围查询:减少网络往返
- 利用 Revision:实现快照读,避免锁竞争
- 合理使用 --count-only:只统计数量,不返回数据
写优化
- 批量操作:使用事务合并多个操作
- 避免大值:大值会增加网络和存储开销
- 控制历史版本:定期压缩,减少存储空间
存储优化
bash
# 定期压缩(删除旧版本)
etcdctl compact 1000
# 碎片整理
etcdctl defrag总结
etcd 的数据模型虽然简单(键值对),但通过以下机制提供了强大的功能:
- Revision 机制:实现 MVCC,支持历史查询
- 有序键空间:支持高效的范围查询
- 租约机制:实现临时数据,自动过期
- 事务支持:保证操作的原子性
理解这些核心概念是有效使用 etcd 的基础。在下一章中,我们将学习 Raft 共识算法,了解 etcd 如何保证分布式一致性。