快照天天成功,恢复时才发现不行:etcd 备份的验证缺口

四十多兆的快照跑了几个月,真要恢复时才发现容器里的 etcdctl 只有 save,没有 status 和 restore。校验、恢复演练与加密后密钥保管的完整做法。

去年做集群加固的时候,我把「etcd 定时快照」列进了清单。脚本写了,cron 加了,每六小时落一份,保留十四份。跑了几天,看目录里文件一天天多起来,大小稳定在四十多兆,我就打了勾。

真正让我坐不住的是上个月的一次恢复演练。同事问了一句「快照能恢复吗」,我想了想,说「应该能」。然后我们找了个临时环境试了一下,发现恢复不了——不是因为快照坏了,是因为容器里那个 etcdctl 根本没有 restore 子命令。

那一刻的心情很难形容。备份看起来完美:文件在、大小对、cron 没报错、日志没异常,但它是一份无法验证、也无法恢复的备份。

一、「备份成功」的三个假象

现在回头看,当时的「成功」建立在一个很脆的判断上:文件生成了。而文件生成只证明了一件事——写文件的权限没问题。

常见的三个假象,值得逐条对照:

第一,文件存在且大小合理。四十多兆确实像那么回事,但如果 snapshot save 中途被打断,一样会留下一个几十兆的残缺文件,大小只是「看起来对」。

第二,退出码是 0。脚本里写的是 etcdctl snapshot save ... && echo ok,只要命令没报错就算成功。可它没报错,不代表写出来的是完整快照。

第三,日志里没有 ERROR。没有 ERROR 只说明没有异常被打印,不说明内容可用。真正的校验需要拿快照去读,而不是看它写。

判断一个备份是否可信,只有一条标准:它被成功恢复过一次。其他都是间接证据。

二、容器里的 etcdctl 是精简版

我们用的是静态 Pod 方式跑的 etcd,快照脚本通过 kubectl exec 进到 etcd 容器里执行。第一次写脚本时,我直接在容器里敲了 etcdctl --help,看到的子命令列表里只有 snapshot save,没有 status,没有 restore。

原因是官方镜像里带的 etcdctl 是精简编译版本,为了减小镜像体积,砍掉了一部分子命令。它够用来做「存快照」,但不够用来做「校验快照」和「恢复快照」。

这就是最坑的地方:你用来备份的工具,和用来验证的工具,不是同一个。备份脚本能跑通,让你误以为工具链是完整的。

# 在 etcd 容器里看到的
$ kubectl -n kube-system exec etcd-node1 -- etcdctl version
etcdctl version: 3.7.0
API version: 3.5

$ kubectl -n kube-system exec etcd-node1 -- etcdctl snapshot --help
NAME:
   etcdctl snapshot - Manages etcd node snapshots
COMMANDS:
   save   Stores an etcd node backend snapshot to a given file
# 只有 save,没有 status / restore

三、给校验装一个能干活的客户端

修法很简单:在宿主机上装发行版的 etcd 客户端,不依赖容器里的那个。

apt-get install -y etcd-client
/usr/bin/etcdctl version    # 3.5.16,子命令齐全

注意版本要对齐。客户端 3.5.x 可以操作 3.5/3.6 的服务端,但别拿 3.4 的去连 3.6——snapshot status 会报协议不匹配。装完先确认容器里的服务端版本,再选客户端版本。

装好之后,校验分成三步,每步都有明确的判据:

校验项 命令 通过判据 不通过意味着什么
文件完整性 etcdctl snapshot status <f> 能输出 hash、revision、key 数 文件残缺或格式损坏
大小下限 stat -c %s <f> 大于历史均值的 80% 写一半被打断
内容一致性 对比 revision 单调递增 新快照 revision ≥ 旧快照 拿到了旧文件或缓存

snapshot status 的输出长这样,四项信息都很有用:

$ etcdctl snapshot status /backup/etcd/snap-20261112-0600.db -w table
+----------+----------+------------+------------+
|   HASH   | REVISION | TOTAL KEYS | TOTAL SIZE |
+----------+----------+------------+------------+
| 8617e459 |   667491 |       2615 |      43 MB |
+----------+----------+------------+------------+

REVISION 是最有价值的一列。把它和历史值比一下,就能判断「这份快照是不是新的」。我们后来把它写进了监控:如果最新快照的 revision 三小时没变,说明 etcd 没写入,或者脚本其实没跑。

四、恢复演练才算数

校验能证明「文件可读」,但不能证明「能恢复成一个能用的集群」。所以还得真恢复一次。

做法是恢复到一个临时目录,不碰生产数据目录:

mkdir -p /tmp/etcd-restore
etcdctl snapshot restore /backup/etcd/snap-20261112-0600.db \
  --data-dir=/tmp/etcd-restore/member \
  --name=restore-test \
  --initial-cluster=restore-test=http://127.0.0.1:2380 \
  --initial-advertise-peer-urls=http://127.0.0.1:2380

ls /tmp/etcd-restore/member
# snap  wal

出现 snap 和 wal 两个目录,说明快照被正确展开。这一步做完,还可以进一步用这个数据目录起一个单节点 etcd,读一下 key 数量对不对:

etcdctl --endpoints=http://127.0.0.1:2379 get / --prefix --keys-only | wc -l

数字和 snapshot status 里的 TOTAL KEYS 接近,就说明内容完整。演练完记得把临时目录删掉,别让它占着磁盘。

我们把这件事做成了每季度一次的固定动作,写在值班手册里。理由很简单:恢复流程里有很多「只在真恢复时才会暴露」的问题——数据目录权限、启动参数、证书路径、版本差异,光看文档是发现不了的。

五、加密带来的新约束

后来给 Secret 开了静态加密。这件事和备份叠加,产生了一个很容易被忽略的约束。

开了加密之后,写进 etcd 的 Secret 是密文,快照里自然也是密文。恢复的时候,如果新集群没有同一把解密密钥,那么恢复出来的六十多个 Secret 全是乱码——集群能起来,但所有依赖 Secret 的工作负载都读不到配置。

所以密钥必须满足两个条件:

第一,和快照分开存放,不能放在同一个目录、同一台机器、同一个备份包里。否则丢一次就两个都丢了。

第二,异地也要有一份。快照通常会有异地副本,密钥如果没有,异地的快照就是一堆废纸。

还有一个时间点问题:加密是某天开启的,那么开启之前生成的快照里,Secret 仍然是明文。这些旧快照要么删掉,要么按「含明文」的级别单独保管。我们保留了那两份旧快照,但在文件名和清单里都标注了「加密前产物,含明文」。

六、脚本要自带校验

最后的脚本改成这样,把「保存」和「校验」绑在一起,任何一个环节不过就告警:

#!/usr/bin/env bash
set -euo pipefail

DEST=/backup/etcd
FILE="$DEST/snap-$(date +%Y%m%d-%H%M).db"
mkdir -p "$DEST"

ETCDCTL_API=3 etcdctl \
  --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key \
  snapshot save "$FILE"

# 1) 结构校验
etcdctl snapshot status "$FILE" -w json > "$FILE.status.json"

# 2) 大小下限(历史均值约 43MB,低于 30MB 视为异常)
SIZE=$(stat -c %s "$FILE")
[ "$SIZE" -gt 31457280 ] || { echo "snapshot too small: $SIZE"; exit 1; }

# 3) 摘要留档
sha256sum "$FILE" > "$FILE.sha256"

# 4) 保留最近 14 份
ls -1t "$DEST"/snap-*.db | tail -n +15 | xargs -r rm -f

# 5) 顺带同步密钥副本(与快照同机但不同目录,异地另备份)
cp -a /etc/kubernetes/enc/encryption-config.yaml "$DEST/.encryption-key.yaml"
chmod 600 "$DEST/.encryption-key.yaml"

再配一条定时任务,把 status.json 里的 revision 推到监控系统,加一条「两小时未更新」的告警。备份这件事,最怕的不是失败,而是静默地成功但没用。

小结

  • 「文件生成了」不等于「备份成功」。唯一可信的判据是它被恢复过一次。
  • 官方镜像里的 etcdctl 可能是精简版,只有 snapshot save,没有 status 和 restore。备份工具和验证工具要分开准备。
  • snapshot status 输出的 revision 最有用,拿它做「快照是否新鲜」的监控指标。
  • 恢复演练要恢复到临时目录,验证 snap/wal 生成和 key 数量,别直接动生产数据目录。
  • 开了 Secret 静态加密之后,快照里是密文,密钥必须与快照分开、异地各存一份,否则恢复出来全是乱码。
  • 加密之前生成的旧快照仍含明文,要单独标注保管或清理。
  • 备份脚本要自带结构校验、大小下限、摘要留档和保留策略,缺一项就会留下「看起来成功」的坑。
查看「容器与编排」全部文章 →

同分类文章

查看 0 评论 0

评论

正在加载评论…

留言提交后自动审核:正常内容立即显示,含敏感信息的会转人工复核。

快照天天成功,恢复时才发现不行:etcd 备份的验证缺口 - 起航社区