Skip to content

核心概念 - 数据模型 ​

etcd 的数据模型是其核心基础,理解数据模型对于正确使用 etcd 至关重要。本章将深入探讨 etcd 的数据存储结构、版本机制和查询能力。

数据模型概览 ​

etcd 采用简单的键值对模型,但通过丰富的元数据和索引机制提供了强大的查询能力。

基本结构 ​

Key: []byte      // 键,字节数组
Value: []byte    // 值,字节数组
CreateRevision   // 创建时的全局版本号
ModRevision      // 最后修改时的全局版本号
Version          // 键的版本号(从 1 开始)
Lease            // 关联的租约 ID(0 表示无租约)

键空间特点 ​

  1. 扁平命名空间:键是字节数组,没有层级结构
  2. 有序性:键按字典序排序,支持范围查询
  3. 原子性:每个操作都是原子的
  4. 版本化:每次修改都会生成新的版本号

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 key2

etcdctl 事务示例 ​

bash
# 简单事务
etcdctl txn <<EOF
compare:
value("/foo") = "bar"

success requests:
put /foo "new-bar"

failure requests:
get /foo
EOF

Go 客户端事务 ​

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")

性能考虑 ​

读优化 ​

  1. 使用范围查询:减少网络往返
  2. 利用 Revision:实现快照读,避免锁竞争
  3. 合理使用 --count-only:只统计数量,不返回数据

写优化 ​

  1. 批量操作:使用事务合并多个操作
  2. 避免大值:大值会增加网络和存储开销
  3. 控制历史版本:定期压缩,减少存储空间

存储优化 ​

bash
# 定期压缩(删除旧版本)
etcdctl compact 1000

# 碎片整理
etcdctl defrag

总结 ​

etcd 的数据模型虽然简单(键值对),但通过以下机制提供了强大的功能:

  1. Revision 机制:实现 MVCC,支持历史查询
  2. 有序键空间:支持高效的范围查询
  3. 租约机制:实现临时数据,自动过期
  4. 事务支持:保证操作的原子性

理解这些核心概念是有效使用 etcd 的基础。在下一章中,我们将学习 Raft 共识算法,了解 etcd 如何保证分布式一致性。