数据存储

1. 数据存储格式

etcd的数据存储格式主要是基于键值对的形式。

  • :是一个唯一的字符串,用于标识存储的数据,类似于字典中的键。例如,在一个配置管理系统中,键可能是某个服务的名称加上配置项的名称,如service1.timeout
  • :可以是任意类型的数据,如字符串、整数、JSON 对象等,这取决于具体的应用场景。例如,对于上述service1.timeout这个键,其对应的值可能是一个整数3000,表示服务1的超时时间为 3000 毫秒;也可能是一个JSON 对象,包含更复杂的配置信息。
    etcd在底层将这些键值对数据以B+树的结构进行存储,这种数据结构可以提供高效的查找、插入和删除操作。同时,etcd还使用了Raft 算法来保证数据在分布式环境下的一致性和可靠性,确保每个节点都能保持相同的键值对数据副本。

etcd中的B+树数据既存储在内存中,也存储在磁盘上,采用了内存与磁盘相结合的存储方式:

  • 内存存储:etcd会将一部分经常访问的数据,以B+树的结构缓存在内存中,以加快数据的访问速度。这样,对于频繁读取的键值对,就可以直接从内存中获取,避免了磁盘I/O操作,从而提高系统的性能和响应速度。
  • 磁盘存储:etcd会把数据持久化存储到磁盘上,以保证数据的可靠性和持久性。在磁盘上,数据以一定的格式组织成文件进行存储,其中就包含了B+树结构相关的数据。当etcd启动时,会从磁盘中加载数据到内存中,恢复B+树的状态。

2. 数据读取

etcd获取key(读操作)主要是从内存中完成的,而非直接从磁盘读取。这是etcd高性能读取操作的关键设计。

  1. etcd将所有活跃的键值对数据保存在内存中,通常使用B树或B+树的变种结构
  2. 读操作直接从这些内存数据结构中获取数据,无需访问磁盘
  3. 内存中不仅存储最新版本,还保留了历史版本,这支持了etcd的事务和历史版本查询功能

    [!note]
    由于etcd会在内存中存放所有数据,因此不建议使用etcd作为持久化数据库,存储大量用户数据。建议etcd用于存放元数据或配置类数据。

2.1. 内存控制的办法

etcd的MVCC(多版本并发控制)机制会为每次修改创建新版本,保留旧版本。这会导致:

  1. 内存持续增长:
    • 每次PUT/DELETE操作都会创建新的版本,而不是覆盖旧版本
    • 历史版本会占用额外内存,且随操作频率线性增长
  2. 无限增长问题:
    • 如不进行版本清理,即使是对同一个key反复修改,也会产生大量版本
    • 理论上内存使用量会无限增长,最终导致OOM为解决这个问题,etcd提供了几种控制历史版本的机制:

2.1.1. 压缩(Compaction)

etcd的关键机制,用于清理历史版本,压缩后,指定版本之前的历史记录将被删除
支持两种模式:

  • 基于版本号: etcdctl compact 5(清理revision 5之前的所有版本)
  • 基于时间: 清理特定时间点之前的版本
    配置etcd定期自动执行压缩,常用配置选项:
  • --auto-compaction-retention='1h': 保留最近1小时的历史
  • --auto-compaction-mode='periodic': 基于时间的周期压缩
  • --auto-compaction-mode='revision': 基于版本号的压缩

2.1.2. 租约机制(Lease)

  • 为key设置TTL(生存时间)
  • 过期后自动删除key及其所有版本

3. 磁盘存储格式

默认情况下,etcd将数据存储在/var/lib/etcd目录下(可通过启动参数--data-dir指定其他位置)。etcd的持久化则使用<font color="#ff0000">预写式日志(WAL:Write Ahead Log)进行记录存储。在WAL的体系中,所有的数据在提交之前都会进行日志记录。在etcd的持久化存储目录中,有两个子目录。

  • 一个是WAL,存储着<font color="#ff0000">所有事务的变化记录
  • 另一个则是snapshot,用于存储某一个时刻etcd<font color="#ff0000">所有目录的数据
# tree /home/ubuntu/data/
/home/ubuntu/data/
└── member
    ├── snap
    │   └── db
    └── wal
        ├── 0000000000000000-0000000000000000.wal
        └── 0.tmp

4 directories, 3 files

3.1. WAL

WAL是etcd数据持久化的主要机制,它遵循"先写日志,后应用"的原则。

3.1.1. 主要特点

  1. 追加写入:所有的写操作都以追加的方式写入WAL文件,确保写入效率
  2. 顺序性:记录严格按时间顺序存储
  3. 原子性:每个条目要么完全写入,要么完全不写入

3.1.2. 包含的主要信息

  1. Entry类型记录:包含Raft协议的操作如提案(proposals)
  2. State类型记录:保存Raft状态信息
  3. CRC校验:每个记录都有校验和,确保数据完整性
  4. 元数据:如term, index等Raft相关信息

3.1.3. WAL分段管理

etcd将WAL文件按大小分段(默认64MB),形成一系列有序的WAL文件,命名格式通常为[序号]-[第一个索引].wal

3.2. Snapshot(快照)

随着系统运行,WAL文件会不断增长。为了避免在重启时重放过多的WAL条目,etcd会周期性创建状态快照。

3.2.1. 特点

  1. 完整状态:包含某一时刻etcd的完整状态
  2. 压缩格式:通常是经过压缩的,减少存储空间
  3. 定期触发:可基于时间或WAL大小触发

3.2.2. Snapshot内容

  1. 键值数据:存储区中的所有键值对
  2. Raft元数据:如最后应用的日志索引等
  3. 版本信息:MVCC版本数据

3.3. WAL与Snapshot的协作机制

WAL和Snapshot协同工作,共同保障etcd的数据一致性和性能:

3.3.1. 写入流程

  • 所有修改操作首先写入WAL
  • 成功写入WAL后,修改应用到内存中的状态机

3.3.2. 快照触发

  • 当WAL累积到一定大小或经过一定时间后
  • 将当前状态序列化为快照文件
  • 记录快照对应的最后应用的日志索引

3.3.3. WAL清理

  • 创建快照后,系统清理已被快照包含的WAL段
  • 保留最近的几个快照作为冗余备份

3.3.4. 恢复流程

当etcd节点重启时:

  1. 首先加载最新的快照文件,恢复到快照时刻的状态
  2. 然后从快照的末尾索引开始,重放后续WAL记录
  3. 完成状态恢复后,etcd开始正常服务

3.3.5. 协作示例

假设etcd具有以下文件:

  • 0000000000000000-0000000000000000.wal
  • 0000000000000001-0000000000010000.wal
  • 0000000000000002-0000000000020000.wal
  • snapshot.db (包含索引15000的状态)
    重启时,etcd会:
  1. 加载snapshot.db恢复到索引15000
  2. 从索引15001开始重放第二个WAL文件的后半部分和第三个WAL文件
  3. 重建完整的内存状态

3.3.6. 特殊设计考量

  1. 预写缓冲区:WAL写入前会使用内存缓冲区提高性能
  2. fsync策略:可配置同步策略,平衡性能和可靠性
  3. 压缩算法:快照通常使用压缩算法减少磁盘使用
  4. 版本控制:与MVCC机制结合,支持多版本并发控制
    这种设计使etcd能够在保证数据安全的同时,提供较高的性能,适合作为分布式系统的协调服务和配置中心。

4. 数据版本

etcd支持数据的版本控制,每个键值对都有一个对应的版本号。当数据发生更新时,版本号会自动递增,这使得用户可以方便地进行数据的回溯和查看历史版本。

[!note]
etcd使用的是64位整数(int64)来存储全局版本号。对于int64类型,其最大值约为9.2×10^18。假设etcd每秒处理1百万个写操作(这已经是非常高的吞吐量),理论上需要大约292,471年才会发生溢出,因此在实际应用中这个问题几乎不需要担心。

4.1. 原理

在 etcd 中,create_revisionmod_revision 和 version 是与键值对数据相关的元数据字段,用于记录数据的版本和修改历史信息。

  • create_revision:表示键值对创建时的修订版本号。每当一个新的键值对被创建时,etcd 会为其分配一个唯一的修订版本号,这个版本号会随着 etcd 中事务操作的执行而单调递增。通过 create_revision 可以准确地知道某个键值对是在 etcd 的哪个修订版本中被创建的。
  • mod_revision:代表键值对最后一次修改时的修订版本号。当键值对的 value 被更新或者键被删除时,mod_revision 的值会更新为当前的修订版本号。所以 mod_revision 总是反映键值对最新的修改状态对应的修订版本。
  • version:表示键值对的版本号。它记录了该键值对被修改的次数,从创建开始计数,每次对键值对进行修改(包括更新值或删除后重新创建),version 都会递增。与 create_revision 和 mod_revision 不同,version 是一个相对的、针对特定键的版本计数,而不是 etcd 全局的修订版本号。

    [!note]
    创建一个键值对 /app/config/timeout,其 create_revision 可能是100,mod_revision 也是100,version 为 1。如果对该键值对进行了一次更新操作,那么 mod_revision 会更新为一个更大的修订版本号,比如 105,version 会递增为 2,而 create_revision 保持不变仍为100。这些字段有助于实现 etcd 的多版本控制功能,方便用户查询和管理数据的不同版本,以及进行事务处理和一致性检查等操作。

4.2. 示例

etcdctl put /app/name myapp1
# OK
etcdctl get /app/name -w=json
# {"header":{"cluster_id":14841639068965178418,"member_id":10276657743932975437,"revision":36,"raft_term":2},"kvs":[{"key":"L2FwcC9uYW1l","create_revision":36,"mod_revision":36,"version":1,"value":"bXlhcHAx"}],"count":1}
etcdctl put /app/name myapp2
# OK
etcdctl get /app/name -w=json
{"header":{"cluster_id":14841639068965178418,"member_id":10276657743932975437,"revision":37,"raft_term":2},"kvs":[{"key":"L2FwcC9uYW1l","create_revision":36,"mod_revision":37,"version":2,"value":"bXlhcHAy"}],"count":1}
etcdctl get /app/name --rev=37
# /app/name
# myapp2
etcdctl get /app/name --rev=36 # 查看之前版本的数据
# /app/name
# myapp1
etcdctl del /app/name
# 1
etcdctl get /app/name --rev=36  # 查看删除前的数据
# /app/name
# myapp1