数据存储
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高性能读取操作的关键设计。
- etcd将所有活跃的键值对数据保存在内存中,通常使用B树或B+树的变种结构
- 读操作直接从这些内存数据结构中获取数据,无需访问磁盘
- 内存中不仅存储最新版本,还保留了历史版本,这支持了etcd的事务和历史版本查询功能
[!note]
由于etcd会在内存中存放所有数据,因此不建议使用etcd作为持久化数据库,存储大量用户数据。建议etcd用于存放元数据或配置类数据。
2.1. 内存控制的办法
etcd的MVCC(多版本并发控制)机制会为每次修改创建新版本,保留旧版本。这会导致:
- 内存持续增长:
- 每次PUT/DELETE操作都会创建新的版本,而不是覆盖旧版本
- 历史版本会占用额外内存,且随操作频率线性增长
- 无限增长问题:
- 如不进行版本清理,即使是对同一个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. 主要特点
- 追加写入:所有的写操作都以追加的方式写入WAL文件,确保写入效率
- 顺序性:记录严格按时间顺序存储
- 原子性:每个条目要么完全写入,要么完全不写入
3.1.2. 包含的主要信息
- Entry类型记录:包含Raft协议的操作如提案(proposals)
- State类型记录:保存Raft状态信息
- CRC校验:每个记录都有校验和,确保数据完整性
- 元数据:如term, index等Raft相关信息
3.1.3. WAL分段管理
etcd将WAL文件按大小分段(默认64MB),形成一系列有序的WAL文件,命名格式通常为[序号]-[第一个索引].wal。
3.2. Snapshot(快照)
随着系统运行,WAL文件会不断增长。为了避免在重启时重放过多的WAL条目,etcd会周期性创建状态快照。
3.2.1. 特点
- 完整状态:包含某一时刻etcd的完整状态
- 压缩格式:通常是经过压缩的,减少存储空间
- 定期触发:可基于时间或WAL大小触发
3.2.2. Snapshot内容
- 键值数据:存储区中的所有键值对
- Raft元数据:如最后应用的日志索引等
- 版本信息: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节点重启时:
- 首先加载最新的快照文件,恢复到快照时刻的状态
- 然后从快照的末尾索引开始,重放后续WAL记录
- 完成状态恢复后,etcd开始正常服务
3.3.5. 协作示例
假设etcd具有以下文件:
0000000000000000-0000000000000000.wal0000000000000001-0000000000010000.wal0000000000000002-0000000000020000.walsnapshot.db(包含索引15000的状态)
重启时,etcd会:
- 加载
snapshot.db恢复到索引15000 - 从索引15001开始重放第二个WAL文件的后半部分和第三个WAL文件
- 重建完整的内存状态
3.3.6. 特殊设计考量
- 预写缓冲区:WAL写入前会使用内存缓冲区提高性能
- fsync策略:可配置同步策略,平衡性能和可靠性
- 压缩算法:快照通常使用压缩算法减少磁盘使用
- 版本控制:与MVCC机制结合,支持多版本并发控制
这种设计使etcd能够在保证数据安全的同时,提供较高的性能,适合作为分布式系统的协调服务和配置中心。
4. 数据版本
etcd支持数据的版本控制,每个键值对都有一个对应的版本号。当数据发生更新时,版本号会自动递增,这使得用户可以方便地进行数据的回溯和查看历史版本。
[!note]
etcd使用的是64位整数(int64)来存储全局版本号。对于int64类型,其最大值约为9.2×10^18。假设etcd每秒处理1百万个写操作(这已经是非常高的吞吐量),理论上需要大约292,471年才会发生溢出,因此在实际应用中这个问题几乎不需要担心。
4.1. 原理
在 etcd 中,create_revision、mod_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