快照天天成功,恢复时才发现不行: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 静态加密之后,快照里是密文,密钥必须与快照分开、异地各存一份,否则恢复出来全是乱码。
- 加密之前生成的旧快照仍含明文,要单独标注保管或清理。
- 备份脚本要自带结构校验、大小下限、摘要留档和保留策略,缺一项就会留下「看起来成功」的坑。
正在加载评论…