--- title: etcd 持久化存储文件 weight: 2650 description: 持久化存储格式与文件参考 categories: [概念] upstream_link: "https://github.com/etcd-io/website/blob/824597935df6e95992ef61c07e3222f4f796ca6c/content/en/docs/v3.7/learning/persistent-storage-files.md" aliases: [/etcd/learning/persistent-storage-files/] --- 本文介绍了 etcd 持久化存储格式:命名规则、内容结构以及可供开发者用于检查存储内容的工具。后续应随着存储模型的变更持续扩展本文内容。本文面向 etcd 开发者,旨在帮助其满足数据恢复需求。 ## 先决条件 {#prerequisites} 以下文章为本文提供了有益的背景信息: * [etcd 数据模型概述](/zh/docs/etcd/learning/data_model/) * [Raft 概述](https://raft.github.io/raft.pdf)(特别是“5.3 日志复制”节)。 ## 概述 {#overview} ### 长期存在的文件 {#long-leaving-files}
| 文件名 | 主要用途 |
|---|---|
./member/snap/db |
bbolt b+tree 用于存储所有已应用的数据、成员权限信息及元数据。它知晓最新的已应用 WAL 日志索引("consistent_index")。 |
./member/snap/0000000000000002-0000000000049425.snap ./member/snap/0000000000000002-0000000000061ace.snap |
定期生成的旧版 v2 存储系统快照,包含:
自 etcd v3 起,其内容与 /snap/db 文件的内容重复。 |
/member/snap/000000000007a178.snap.db |
如果副本严重滞后,则从 etcd 领导者下载完整的bbolt 快照。 其内容类型与 该文件用于以下两种场景:
恢复完成后,该文件不会被删除(其全部内容会填充到 ./member/snap/db 文件中)。这些文件会定期(30s)进行清理。
此处同样只保留 |
./member/wal/000000000000000f-00000000000b38c7.wal ./member/wal/000000000000000e-00000000000a7fe3.wal ./member/wal/000000000000000d-000000000009c70c.wal |
Raft 的预写日志,包含 Raft 接受的近期事务、定期快照或 CRC 记录。 保留最近的 如果快照生成频率过低,可能会出现超过 |
./member/wal/0.tmp (or .../1.tmp) |
为下一个预写日志文件预留的空间。 用于避免因 WAL 日志容量不足而导致 Raft 卡住,且无法触发告警的情况。 |
| 文件 | 主要用途 |
|---|---|
./member/snap/0000000000000002-000000000007a178.snap.broken |
当快照文件无法加载时,会被重命名为“broken”。
etcd 启动时会尝试加载最新文件。 或在执行 etcdctl 的备份/迁移命令期间。 |
./member/snap/tmp071677638 (random suffix) |
该临时 bbolt 文件由副本创建,用于响应领导者的 msgSnap 请求,按要求从指定快照恢复存储。
完整内容成功获取后,文件将被重命名为 参见 etcd/issues/12837。已在 etcd 3.5 中修复。 |
/member/snap/db.tmp.071677638 (random suffix) |
在碎片整理过程中,该临时文件用于保存后端数据库内容(/member/snap/db)的副本。整理成功后,文件会重命名为 /member/snap/db,替换原后端数据库。 在 etcd 服务器启动时,这些文件会被清理。 |
| 存储桶 | 键 | 示例值 | 描述 |
|---|---|---|---|
| alarm | rpcpb.Alarm:
{MemberID, Alarm: NONE|NOSPACE|CORRUPT}
|
nil |
表明其中一个成员已诊断出问题。 |
| auth | "authRevision" | ""(空)或 BigEndian.PutUint64 |
角色或用户任何变更都会在事务提交时递增此字段。 该值仅用于授权过程中的乐观锁。 |
| authRoles | [roleName] 为字符串 | authpb.Role 序列化 |
|
| authUsers | [userName] 为字符串 | authpb.User 序列化 |
|
| cluster | "clusterVersion" | "3.5.0"(字符串) |
次要 版本的共识达成的通用存储版本。 |
| "downgrade" | JSON: {
"target-version": "3.4.0"
"enabled": true/false
}
|
保存最近一次 自 v3.5 版本起 |
|
| key |
[revisionId] 使用 bytesToRev{main,sub} 编码 删除的键值对在序列化时会以 't' 结尾,表示“墓碑”(Tombstone) |
mvccpb.KeyValue 序列化 proto(key, create_rev, mod_rev, version, value, lease id) |
|
| lease | leasepb.Lease 序列化后的 proto(ID、TTL、RemainingTTL) |
注意:LeaseCheckpoint 仅扩展 RemainingTTL。TTL 仍来自原始 Grant。 注意 2:我们以秒为单位持久化 TTL(从未定义的“现在”开始计算)。崩溃循环的服务器不会释放租约! |
|
| members | 以十六进制字符串形式表示的 [memberId]:"8e9e05c52164694d" |
以字符串形式序列化的 Member 结构:
{
"id":10276657743932975437,
"peerURLs":[
"http://localhost:2380"],
"name":"default",
"clientURLs": ["http://localhost:2379"]
}
|
已达成一致的集群成员关系信息。 |
| members_removed | 以十六进制字符串形式表示的 [memberId]:"8e9e05c52164694d" |
[]byte("removed") |
所有已移除成员的 ID。用于验证已移除的成员不会以相同 ID 再次添加。 该字段目前(3.4 版本)从 V2 存储系统读取,从不从 V3 读取。详见 https://github.com/etcd-io/etcd/pull/12820 |
| meta | "consistent_index" | uint64 字节(大端序) | 表示最后应用的 WAL 条目在 Bolt DB 存储系统中的偏移量。 |
| scheduledCompactRev | bytesToRev{main,sub} 编码(共 16 字节)。 | 在执行压缩请求后发生崩溃时,用于重新初始化压缩。 | |
| finishedCompactRev | bytesToRev{main,sub} 编码(共 16 字节)。 | 存储系统最近成功完成压缩时的修订版本(https://github.com/etcd-io/etcd/blob/ae7862e8bc8007eb396099db4e0e04ac026c8df5/server/mvcc/kvstore_compaction.go#L54) | |
| "confState" | 自 etcd 3.5 版本起 | ||
| "term" | 自 etcd 3.5 版本起 | ||
| "storage-version" |
64*10^6 字节时,会将其切分并开始写入新文件。
### 逻辑内容 {#logical-content}
预写日志文件在逻辑层包含:
* `Raftpb.Entry: ` 由 Raft 领导者复制的最近提案。其中部分提案被视为“已提交”,其余提案可能被逻辑覆盖。
* `Raftpb.HardState(term,commit,vote): ` 关于日志条目索引的周期性(非常频繁)信息,该索引表示条目已“提交”(复制到多数服务器),因此保证不会被更改或覆盖,并可应用于后端(v2、v3)。该信息还包含“任期”(指示是否存在与选举相关的变更)以及投票信息——当前副本在当前任期中投给的成员。
* `walpb.Snapshot(term, index): ` Raft 状态的周期性快照(不包含数据库内容,仅包含快照日志索引和 Raft 任期)
* v2 存储内容存储在独立的 *.store 文件中。
* v3 存储内容保存在 bbolt 文件中,一旦条目被应用,该文件即成为隐式快照。
* crc32 校验和记录(位于每个文件开头),用于恢复对文件剩余部分的 CRC 检查。
* `etcdserverpb.Metadata(node_id, cluster_id)` —— 用于标识日志所代表的集群与副本。
每个 WAL 日志文件按以下顺序构建:
1. CRC-32 帧(从之前所有文件延续的 CRC;第一个文件为 0)。
2. 元数据帧(集群 ID 与副本 ID)。
3. 仅初始 WAL 文件包含:
* 空快照帧(索引:0,任期:0)。
此帧用于维持一个不变量:所有条目之前均有一个快照。
对于非初始(第 2 个及后续)WAL 文件:
* HardState 帧。
4. 条目、硬状态与快照记录的混合
WAL 日志可能包含同一索引的多个条目。这种情况可能出现在 [Raft 论文](http://web.stanford.edu/~ouster/cgi-bin/papers/raft-atc14.pdf) 图 7 所描述的场景中。etcd 的 WAL 日志仅支持追加写入,因此当新条目以相同索引写入时,原有条目会被覆盖。
特别是在读取 WAL 时,[逻辑会用新条目覆盖旧条目](https://github.com/etcd-io/etcd/blob/release-3.4/wal/wal.go#L448-L462)。因此,仅当条目索引 entry.index <= HardState.commit 时,才可视为最终版本。索引大于 HardState.commit 的条目可能发生变化。
WAL 日志中的“任期”应保持单调递增。
WAL 日志中的“索引”预期满足以下要求:
1. 从某个快照开始
2. 在该任期期间,索引应持续递增
3. 若任期发生变化,索引可能减少,但必须大于最新的 HardState.commit 值
4. 任意索引大于等于 HardState.commit 的新快照都可能发生,从而开启新的索引序列

### 工具 {#tools-1}
#### etcd-dump-logs {#etcd-dump-logs}
etcd 的 WAL 日志可使用 [etcd-dump-logs](https://github.com/etcd-io/etcd/tree/master/tools/etcd-dump-logs) 工具读取:
```
% go install go.etcd.io/etcd/v3/tools/etcd-dump-logs@latest
% go run go.etcd.io/etcd/v3/tools/etcd-dump-logs --start-index=0 aname.etcd
```
请注意:
* 该工具仅显示条目,而不显示 WAL 日志文件中的所有记录(如快照、HardState)。
* 该工具会自动应用“覆盖”规则。若某条目被同一索引下更新的条目覆盖,则工具仅输出最终值。
* 该工具还会输出未提交的条目(来自日志尾部),但不包含 HardState.commitIndex 信息,因此无法判断条目是否为最终值。
## Store V2 快照:**member/snap/{term}-{index}.snap** {#snapshots-of-store-v2-membersnapterm-indexsnap}
### 文件名: {#file-names-1}
**member/snap/{term}-{index}.snap**
文件名在 [此处](https://github.com/etcd-io/etcd/blob/ad5b30297a43daeb5ce7311fa606ce4c1f16618f/server/etcdserver/api/snap/snapshotter.go#L78) `("%016x-%016x.snap") ` 生成,采用 2 个十六进制编码的组合形式:
* `term` -> 快照生成时的 Raft 任期(两次选举之间的时段)
* `index` -> 快照生成时最后一个已应用提案的索引
### 创建 {#creation}
*.snap 文件由 [Snapshotter.SaveSnap](https://github.com/etcd-io/etcd/blob/ad5b30297a43daeb5ce7311fa606ce4c1f16618f/server/etcdserver/api/snap/snapshotter.go#L68) 方法创建。
有两个触发器控制这些文件的创建:
* 每处理大约 --snapshotCount=(默认为 100'000)个已应用提案,就会创建一个新文件。这只是近似值:提案可能分批到达,而系统只会在一批处理结束时考虑生成快照,最终的快照过程还会异步调度。
标志名 --snapshotCount 容易产生误解:它控制[最后一次快照索引与最后一次已应用提案索引之间的索引差](https://github.com/etcd-io/etcd/blob/ad5b30297a43daeb5ce7311fa606ce4c1f16618f/server/etcdserver/server.go#L1266)。
* Raft 请求副本从快照恢复。副本通过网络接收快照(msgSnap 消息)时,也会将其以轻量方式记录到 WAL 日志中。这可确保 WAL 日志尾部始终存在一个后接日志条目的有效快照,从而避免 WAL 日志中可能出现的不连续。
目前,这些文件大致[^3]与 WAL 日志中的 Snapshot 条目一一对应。随着 v2 存储系统停用,预计将彻底停止写入这些文件(3.5.x 可选启用,3.6.x 强制执行)。
### 内容 {#content}
该文件包含序列化后的 [snapdb.snapshot proto](https://github.com/etcd-io/etcd/blob/ad5b30297a43daeb5ce7311fa606ce4c1f16618f/server/etcdserver/api/snap/snappb/snap.proto#L11) `(uint32 crc, bytes data)`,
其中 `data` 字段包含 [Raftpb.Snapshot](https://github.com/etcd-io/etcd/blob/ad5b30297a43daeb5ce7311fa606ce4c1f16618f/raft/raftpb/raft.proto#L31):
(字节数据,SnapshotMetadata{index, term, [conf](https://github.com/etcd-io/etcd/blob/ad5b30297a43daeb5ce7311fa606ce4c1f16618f/raft/raftpb/raft.proto#L99)} 元数据),
最后,嵌套数据包含序列化为 JSON 格式的 [存储 v2 内容](#exemplar-json-serialized-store-v2-content-in-etcd-34-snap-files)。
特别是存在:
* 任期
* 索引
* 成员数据:
* /0/members/8e9e05c52164694d/attributes -> {\"name\":\"default\",\"clientURLs\":[\"http://localhost:2379\"]}
* /0/members/8e9e05c52164694d/RaftAttributes -> "{\"peerURLs\":[\"http://localhost:2380\"]}"
* 存储版本:/0/version -> 3.5.0
### 工具 {#tools-2}
#### protoc {#protoc}
以下命令可在 etcd 根目录下执行,用于查看文件内容:
```
cat default.etcd/member/snap/0000000000000002-0000000000049425.snap |
protoc --decode=snappb.snapshot \
server/etcdserver/api/snap/snappb/snap.proto \
-I $(go list -f '{{.Dir}}' github.com/gogo/protobuf/proto)/.. \
-I . \
-I $(go list -m -f '{{.Dir}}' github.com/gogo/protobuf)/protobuf
```
类似地,可以提取 `data` 字段,并将其解码为 [Raftpb.Snapshot](https://github.com/etcd-io/etcd/blob/ad5b30297a43daeb5ce7311fa606ce4c1f16618f/raft/raftpb/raft.proto#L31)。
### etcd 3.4 *.snap 文件中示例 JSON 序列化存储 v2 内容: {#exemplar-json-serialized-store-v2-content-in-etcd-34-snap-files}
```json
{
"Root":{
"Path":"/",
"CreatedIndex":0,
"ModifiedIndex":0,
"ExpireTime":"0001-01-01T00:00:00Z",
"Value":"",
"Children":{
"0":{
"Path":"/0",
"CreatedIndex":0,
"ModifiedIndex":0,
"ExpireTime":"0001-01-01T00:00:00Z",
"Value":"",
"Children":{
"members":{
"Path":"/0/members",
"CreatedIndex":1,
"ModifiedIndex":1,
"ExpireTime":"0001-01-01T00:00:00Z",
"Value":"",
"Children":{
"8e9e05c52164694d":{
"Path":"/0/members/8e9e05c52164694d",
"CreatedIndex":1,
"ModifiedIndex":1,
"ExpireTime":"0001-01-01T00:00:00Z",
"Value":"",
"Children":{
"attributes":{
"Path":"/0/members/8e9e05c52164694d/attributes",
"CreatedIndex":2,
"ModifiedIndex":2,
"ExpireTime":"0001-01-01T00:00:00Z",
"Value":"{\"name\":\"default\",\"clientURLs\":[\"http://localhost:2379\"]}",
"Children":null
},
"RaftAttributes":{
"Path":"/0/members/8e9e05c52164694d/RaftAttributes",
"CreatedIndex":1,
"ModifiedIndex":1,
"ExpireTime":"0001-01-01T00:00:00Z",
"Value":"{\"peerURLs\":[\"http://localhost:2380\"]}",
"Children":null
}
}
}
}
},
"version":{
"Path":"/0/version",
"CreatedIndex":3,
"ModifiedIndex":3,
"ExpireTime":"0001-01-01T00:00:00Z",
"Value":"3.5.0",
"Children":null
}
}
},
"1":{
"Path":"/1",
"CreatedIndex":0,
"ModifiedIndex":0,
"ExpireTime":"0001-01-01T00:00:00Z",
"Value":"",
"Children":{
}
}
}
},
"WatcherHub":{
"EventHistory":{
"Queue":{
"Events":[
{
"action":"create",
"node":{
"key":"/0/members/8e9e05c52164694d/RaftAttributes",
"value":"{\"peerURLs\":[\"http://localhost:2380\"]}",
"modifiedIndex":1,
"createdIndex":1
}
},
{
"action":"set",
"node":{
"key":"/0/members/8e9e05c52164694d/attributes",
"value":"{\"name\":\"default\",\"clientURLs\":[\"http://localhost:2379\"]}",
"modifiedIndex":2,
"createdIndex":2
}
},
{
"action":"set",
"node":{
"key":"/0/version",
"value":"3.5.0",
"modifiedIndex":3,
"createdIndex":3
}
}
]
}
}
}
}
```
## 变更记录 {#changes}
本节用于描述不同 etcd 版本之间引入的文件格式变更。
[^1]:
bbolt 文件开头的元数据页会原地修改。
[^2]:
不一致,因为大多数 uint 的写入格式为大端字节序
[^3]:
WAL 日志开头的初始快照(索引: 0)不与 *.snap 文件关联。此外,旧的 *.snap 文件(或 WAL 日志)可能已被清除。