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 机制 —— 配置热加载的核心)

相关推荐
一个天蝎座 白勺 程序猿11 小时前
KingbaseES数据库归档重做日志文件管理:从配置到运维的实战指南
运维·数据库·oracle·kingbasees·金仓数据库
分布式存储与RustFS14 小时前
镜像仓库的层别堆本地盘:用 RustFS 给 Docker Registry 当 S3 后端
运维·云原生·开源·对象存储·分布式存储·s3·性能基准
陈希12315 小时前
Linux 防火墙配置实战:firewalld、iptables 与云安全组
linux·运维·服务器
@兽血沸腾16 小时前
Windows系统安装Jenkins部署应用至远程Windows服务器操作指南
运维·服务器·jenkins
闲云自留地21 小时前
K8s 的记忆大脑!ETCD 集群部署、etcdctl 命令与备份恢复全通关
容器·kubernetes·etcd
讯捷蓝达21 小时前
戴尔R720xd服务器卡死在 “Configuring Memory...Done“,换了内存还是卡?问题根本不在内存
运维·服务器
爱和冰阔落1 天前
【Linux网络】TCP 服务器如何并发处理多个客户端?从 fork、僵尸进程到双重 fork
linux·运维·服务器·网络
lisanmengmeng1 天前
alertmanger 标签 rules 打上分组标签
linux·运维·服务器
Android系统攻城狮1 天前
Linux FFmpeg深度解析之av_write_frame调用流程与实战(三十三)
linux·运维·服务器·ffmpeg·音视频进阶·ffmpeg音视频
上火的金鱼妹1 天前
k8s生产实际问题排查(网关,数据库读写等)
运维·服务器