etcd 学习系列(四):存储模型 —— WAL 和 Snapshot 是如何配合的

前言

前几篇文章我们讲了 etcd 的集群架构和 Raft 协议,明白了数据是怎么在节点之间同步的。但还有一个问题:数据在磁盘上是怎么存的? 宕机了怎么恢复?

这篇文章,我们来拆解 etcd 的存储模型。


一、数据目录结构

当你安装好 etcd 并启动后,数据目录下会有这样的结构:

复制代码
/var/lib/etcd/etcd-cluster/          ← 数据目录(可配置)
  ├── member/
  │   ├── snap/                      ← Snapshot(快照文件)
  │   │   └── 0000000000000001-0000000000000001.snap
  │   └── wal/                       ← WAL(预写日志)
  │       └── 0000000000000001-0000000000000001.wal
  └── ...

两个核心目录

目录 文件名 存什么 类比
snap/ *.snap 某一时刻的全量数据快照 每个月拍一张全家福
wal/ *.wal 所有写操作的日志 每天写日记

文件名的含义

复制代码
0000000000000001-0000000000000001.snap
↑ 当前任期      ↑ 当前日志索引

二、WAL(Write-Ahead Log,预写日志)

WAL 是什么

WAL 是 etcd 的操作日志,记录所有对数据的修改操作(set、delete 等)。

复制代码
etcd 收到写请求:"set timeout=60"
  → 先写入 WAL(写日志,保证不丢)
  → 再更新内存中的 key-value 数据
  → 返回客户端:写入成功

为什么先写 WAL,再更新内存?

复制代码
如果不先写 WAL:
  etcd 更新内存 → 突然宕机 → 内存中的数据丢失
  → 数据丢了 ❌

先写 WAL,再更新内存:
  etcd 写入 WAL → 更新内存 → 突然宕机
  → 重启后,从 WAL 中重放操作,恢复数据
  → 数据不丢 ✅

WAL 是追加写入的

WAL 文件是顺序追加的,不会修改已有的内容。这意味着:

复制代码
第 1 次写入 → WAL 文件末尾追加一条记录
第 2 次写入 → 再追加一条
第 3 次写入 → 再追加一条

WAL 文件会越来越大...

三、Snapshot(快照)

为什么需要 Snapshot?

复制代码
WAL 一直在增长,如果不做处理:
  第 1 次写入 → WAL 1KB
  第 1000 次写入 → WAL 1MB
  第 100000 次写入 → WAL 100MB
  ...
  宕机恢复时,要从头重放所有 WAL → 非常慢 ❌

所以需要定期生成 Snapshot(快照):
  Snapshot = 当前内存中所有数据的全量备份
  有了 Snapshot,就不需要从最早的 WAL 开始重放了

Snapshot 的触发条件

bash 复制代码
# 从 etcd 日志中可以查看
journalctl -u etcd | grep "snapshot count"
# 输出:snapshot count = 100000

snapshot count = 100000 表示:每处理 10 万次写操作,自动生成一次 Snapshot。

Snapshot 生成过程

复制代码
当 WAL 达到 100000 条时:
  1. etcd 把内存中的全量数据保存为 .snap 文件
  2. 删除旧的 .snap 文件(保留最新的)
  3. 删除旧的 WAL 文件
  4. 新建一个空的 WAL 文件,从头开始写

所以任何时候,磁盘上只有:
  1 个 .snap 文件(全量数据)
  1 个 .wal 文件(snapshot 之后的增量)

四、宕机恢复流程

复制代码
etcd 宕机了(比如进程被 kill)

重启 etcd:
  1. 加载最新的 .snap 到内存(恢复全量数据)
  2. 重放 .wal 中 snapshot 之后的日志(恢复增量数据)
  3. etcd 恢复到宕机前的状态 ✅

类比:
  你有一本日记本(WAL),每个月拍一张照片(Snapshot)
  如果你失忆了(宕机):
  1. 看最近的照片(Snapshot)→ 知道上个月的状态
  2. 读照片之后的日记(WAL)→ 知道之后发生了什么
  3. 你就恢复了完整记忆 ✅

五、snapshot save 是什么?

这是 etcd 迁移中最重要的操作。

执行方式

bash 复制代码
export ETCDCTL_API=3
etcdctl --endpoints=http://127.0.0.1:2379 \
  snapshot save /tmp/snapshot-20260903.db

它做了什么?

复制代码
snapshot save 不是复制磁盘上的 .snap 文件
而是从 etcd 进程的内存中,直接导出"当前所有数据"

内存中的数据 = 磁盘上的 .snap 加载后 + .wal 重放后
             = 完整的、最新的数据

所以导出的 .db 文件 = 那个时刻的完整数据 ✅

为什么会叫"save"而不是"export"?

复制代码
虽然叫 snapshot save,但它跟 etcd 内部自动触发的 snapshot 是两回事:

内部自动 snapshot(由 snapshot count 触发):
  ├── 目的是控制 WAL 大小
  ├── 生成 .snap 文件,用于宕机恢复
  └── etcd 自己管理,用户不需要关心

手动 snapshot save(由用户触发):
  ├── 目的是备份或迁移数据
  ├── 生成 .db 文件,可以传到其他机器
  └── 用于备份、迁移、恢复

六、snapshot restore 做了什么?

bash 复制代码
etcdctl snapshot restore /tmp/snapshot-20260903.db \
  --name=new-node-1 \
  --initial-cluster="new-node-1=http://10.0.0.1:2380,new-node-2=http://10.0.0.2:2380,new-node-3=http://10.0.0.3:2380" \
  --initial-cluster-token=my-cluster-token \
  --data-dir=/var/lib/etcd/etcd-cluster

恢复过程

复制代码
1. 从 .db 文件中提取数据
2. 生成全新的 member/snap/ 和 member/wal/ 目录
3. 用新的集群配置初始化(name、token、成员列表)
4. 启动后,etcd 加载这个 snapshot 到内存
5. 集群数据恢复 ✅

为什么 snapshot 能跨集群迁移?

复制代码
snapshot 里存的是"数据本身",不包含"集群身份"

就像:
  把一本书(数据)从 A 房间(旧集群)搬到 B 房间(新集群)
  书的内容不变,但书的摆放位置重新设置

  --name=新节点名          ← 新房间的标签
  --initial-cluster-token=  ← 新房间的标识(防止跟旧集群混淆)
  --initial-cluster=...     ← 新房间的成员列表

为什么要改 token?

复制代码
--initial-cluster-token 相当于集群的"身份证号"

旧集群:身份证号 123456
新集群:身份证号 ABCDEF

如果新集群用旧身份证号:
  → 新集群以为自己是旧集群
  → 试图找旧集群的节点
  → 找不到,启动失败 ❌

如果新集群用新身份证号:
  → 新集群知道自己是一个新集群
  → 用 snapshot 恢复的数据启动
  → 启动成功 ✅

七、compaction(数据压缩)

为什么需要 compaction?

复制代码
etcd 每次写操作都会保留历史版本:

/default/myapp/config/db_url/1700123456 → "旧地址"
/default/myapp/config/db_url/1700123457 → "新地址"
/default/myapp/config/db_url/1700123458 → "最新地址"

如果不压缩,历史版本越积越多,磁盘会无限增长
compaction 就是删除旧版本,只保留最近的版本

配置自动 compaction

bash 复制代码
# 在 etcd 启动参数中加
--auto-compaction-retention=1
# 保留最近 1 小时的 revision,自动删除旧版本

手动 compaction

bash 复制代码
export ETCDCTL_API=3
# 查看当前 revision
etcdctl --endpoints=http://127.0.0.1:2379 endpoint status --write-out=fields
# 压缩到当前 revision
etcdctl --endpoints=http://127.0.0.1:2379 compaction <revision号>

八、总结

复制代码
etcd 的存储模型核心就是两样东西:

WAL(预写日志):
  ├── 记录所有写操作
  ├── 顺序追加,高性能
  └── 宕机时重放恢复

Snapshot(快照):
  ├── 定期压缩全量数据
  ├── 控制 WAL 大小,加速恢复
  └── 手动 snapshot save 用于备份迁移

手动 snapshot save 导出的 .db 文件:
  ├── 是内存中的完整数据,不是磁盘上的 .snap
  ├── restore 后不需要重放 WAL,本身就是完整的
  └── 可以跨集群迁移(数据不绑定集群身份)

上一篇:[etcd 学习系列(三):Raft 协议 ------ 分布式一致性就是这么简单](#etcd 学习系列(三):Raft 协议 —— 分布式一致性就是这么简单)

下一篇:[etcd 学习系列(五):Watch 机制 ------ 配置热加载的核心](#etcd 学习系列(五):Watch 机制 —— 配置热加载的核心)

相关推荐
come112342 小时前
Nginx `location` 配置说明(后端开发版)
运维·nginx
SkyWalking中文站9 小时前
SkyWalking 11 与 BanyanDB 0.11:在存储引擎内部实现 Trace 尾部采样
运维·监控·自动化运维
姚不倒9 小时前
etcd 学习系列(二):集群架构 —— 3 节点是如何工作的
运维·架构·etcd
pt104310 小时前
网络自动化Python课程:Cisco PyATS网络自动化测试框架
运维·自动化
小袁拒绝摆烂11 小时前
Jenkins部署经验
运维·jenkins
刚入门的大一新生12 小时前
Linux-进程控制
linux·运维·服务器·c++
上火的金鱼妹12 小时前
K8S基础组件作用和关系整理
linux·运维·服务器·kubernetes
Vcaker12 小时前
Linux学习25-harbor私有仓库部署
linux·运维·学习
醉颜凉13 小时前
网络安全必学:粘性MAC地址(Sticky MAC)原理与应用全解析
运维·服务器·网络·安全·web安全