K8s etcd 备份与恢复实战:误删命名空间后 37 秒恢复,5 个对象 UID 完全一致

先把结论放这儿

项 实测值
灾难 kubectl delete ns prod-app(手滑删掉整个业务命名空间)
恢复耗时 37 秒 (从停控制平面到 /readyz 变绿)
数据完整性 5/5 对象 UID 完全一致,Secret 明文正确
快照大小 5.7 MB(etcd DB 6.0 MB / 721 keys)
备份耗时 51 ms,对线上无感

但这 37 秒不是我第一次就做到的。我第一次「成功」只花了 6 秒 ------ 而那是假的。

环境:VMware 上自建的 3 节点实验集群,K8s v1.34.10,etcd v3.6.5,单 master。这是 lab,不是生产,所以后面我会明确说清哪些结论不能照搬。


一、为什么 etcd 值得单独写一篇

K8s 里能重建的东西很多:kubelet 能重启,Pod 能重建,节点能重装。

etcd 不一样,它是唯一不能重建的东西。

你的 Deployment、Service、ConfigMap、Secret、RBAC、CRD 定义、CA 证书、ServiceAccount token、甚至 Leader 选举租约,全都只存在 etcd 里。它一丢,集群不是「坏了」,是失去了记忆。

而现实里最常见的 etcd 灾难不是硬盘坏 ------ 是人:

  • kubectl delete ns 敲快了
  • kubectl delete crd 以为只删定义,结果该 CRD 下所有实例一起没
  • apply 了一份写错的 RBAC,把自己锁在门外
  • 有人「清理磁盘」把 /var/lib/etcd 删了

我审计自己这个实验集群的时候,发现一个备份都没有:没有 crontab,没有备份目录,什么都没有。这就是这个演练的起点。


二、备份:snapshot save 到底存了什么

先说一个容易搞错的点。

kubectl get -A -o yaml > backup.yaml 不是集群备份。它只是把你导出的那几类对象存成了文本,里面没有 CRD 定义、没有租约、没有 CA,而且 UID 会变。

真正的集群备份是物理快照:

bash 复制代码
etcdctl snapshot save /var/backups/etcd/etcd-$(date +%Y%m%d-%H%M%S).db

它通过 etcd 的 Maintenance.Snapshot gRPC 接口,让 etcd 在某个一致性点上把整个 bbolt 数据库流给你。注意「一致性点」这四个字 ------ 它不是边写边拷,那样会拷出一个撕裂的库。

两者对比:

物理快照 YAML 导出
存什么 bbolt 数据库文件 一堆 YAML 文本
含 UID / resourceVersion / Secret / CRD / 租约 全都含 基本不含
恢复后 UID 保持一致 全部重新生成
适合场景 灾难恢复 变更审计、迁移

我的结论是两个都要:快照用来救命,Git 仓库(GitOps)用来审计「谁在什么时候改了什么」。

2.1 备份完必须马上校验,不然等于没备份

bash 复制代码
etcdutl snapshot status /var/backups/etcd/etcd-20260913-164401.db -w json
# {"hash":"f9dffe70...","revision":139650,"totalKey":721,"totalSize":5967872,"version":"3.6.0"}

如果 revision 是 0,或者字段是空的,这个快照就是废的。

我在备份脚本里把这条写死了:校验不通过就删掉文件、返回非 0 退出。

理由很直白:一个「备份了但恢复不了」的快照,比没有备份更危险 ------ 它会给你虚假的安全感,让你在真出事那天以为身后有退路。

2.2 这里有个坑,而且是静默的

etcd 3.5 起 把 snapshot status / snapshot restore 从 etcdctl 挪到了独立的 etcdutl;3.6 直接移除。

问题在于,如果你在 etcd 3.6 上敲:

bash 复制代码
etcdctl snapshot status /var/backups/etcd/etcd-20260913-164401.db

它不报错。 它只是打印一屏 help 文本,然后 exit 0。

任何「只看退出码」的脚本都会认为校验通过了。我就是这么被坑的 ------ 备份脚本跑了很久,校验其实一次都没真正执行过。

修法是脚本里探测 etcd 版本,优先用 etcdutl,老版本回退 etcdctl。

这条的通用教训比 etcd 本身更值钱:「没报错」不等于「执行成功」。校验脚本必须检查输出内容,不能只看退出码。


三、灾难现场

先造数据。一个模拟业务的命名空间 prod-app:3 副本 Deployment + Service + ConfigMap + Secret。

为了让后面能证明「恢复的是原对象」而不是「重建了同名对象」,我先把 5 个对象的 UID 记了下来:

复制代码
Namespace   prod-app       d51f9126-c2c8-4a5e-937b-b226f98d3834
Deployment  web            6a00afee-75ca-4faf-a0fc-6b8c2713a4b9
Service     web-svc        a29da680-baac-4502-a344-64ef59ec6e63
ConfigMap   app-config     e0b5a150-5719-4594-babf-f45bab9704a2
Secret      db-credential  bfa04906-2f4b-494b-945f-555752b11590

然后备份,然后开删:

bash 复制代码
kubectl delete ns prod-app
# namespace "prod-app" deleted    ← 10 秒清完

10 秒,一个业务命名空间没了。


四、第一次恢复:6 秒「成功」,其实是假的

第一版恢复流程长这样:

复制代码
[1/5] 停 kubelet          → kubelet: inactive
[2/5] 保全现场            → /var/lib/etcd → /var/lib/etcd.bak-20260913-164456
[3/5] etcdutl snapshot restore → 数据目录已重建
[4/5] 启动 kubelet        → API Server 就绪(耗时 6 秒)

6 秒。我当时还以为自己发现了什么不得了的东西。

结果 kubectl get ns 一看 ------ prod-app 根本不存在。

4.1 排查

第一反应是「快照坏了」,查下去发现不是:

bash 复制代码
# 数据目录确实写进去了
ls -la /var/lib/etcd/member/snap/
# -rw-------. 1 root root 5967872 Sep 13 16:44 db    ← 16:44 的新数据,5.9 MB
旧数据也还在
ls -d /var/lib/etcd.bak-*
/var/lib/etcd.bak-20260913-164456

数据和退路都对。那问题在哪?去看容器启动时间:

bash 复制代码
docker ps --filter name=etcd --format 'table {{.Names}}\t{{.Status}}'
# k8s_etcd_etcd-k8s-master01_...  Up 49 minutes    ← 49 分钟没重启过

再看 apiserver:

bash 复制代码
ss -lntp | grep 6443
# LISTEN ... users:(("kube-apiserver",pid=2705,fd=3))  ← 还是 1 小时前的 PID

数一下还在跑的容器:

bash 复制代码
docker ps --filter name=k8s_ -q | wc -l
# 18

18 个 k8s 容器,一个都没停。

4.2 根因

systemctl stop kubelet 不会杀掉它管理的容器。

这不是 bug,是 kubelet 的正确设计 ------ 重启 kubelet 不应该导致业务容器中断。但对 etcd 恢复来说这是致命的:

复制代码
① mv /var/lib/etcd (旧数据)
    ↓
   运行中的 etcd 仍持有旧文件的 inode 句柄
   → 它继续用【旧数据】对外服务,完全不受影响
② etcdutl snapshot restore → /var/lib/etcd (新数据)
↓
新数据老老实实落到磁盘了
但没有任何进程会去读它
③ systemctl start kubelet
↓
kubelet 一看「容器已经在运行」→ 什么都不做
→ etcd 仍在用旧数据 → 恢复等于没做

那为什么 /readyz 是绿的?因为 apiserver 从头到尾就没停过,它一直连着那个还活着的旧 etcd。绿色的 healthz 只说明「apiserver 能连上 etcd」,不说明「你恢复成功了」。

4.3 修法

bash 复制代码
systemctl stop kubelet
sleep 3
关键:kubelet 停了容器还在,必须手工停
docker ps --filter "name=k8s_" -q | xargs -r docker stop -t 20
必须确认端口真释放了
ss -lntp | grep -E ':6443|:2379'    # 应该没有任何输出
systemctl start kubelet            # 从已恢复的数据目录重建静态 Pod
bash 复制代码
待停止容器数: 18
剩余运行容器: 0
✅ 6443 已释放(apiserver 确实停了)
✅ 2379 已释放(etcd 确实停了)
✅ API Server 就绪(本轮耗时 32 秒)

这一步的教训一句话:恢复流程里必须有「端口释放校验」,不能只看 systemctl is-active kubelet。


五、完整恢复流程(5 步)

复制代码
① 停控制平面
   mv /etc/kubernetes/manifests/*.yaml /tmp/hold/    # 逼 kubelet 删掉静态 Pod
   显式停掉所有 k8s_* 容器
   验证 6443 / 2379 已释放                          # ★ 不验证会白忙
② 保全现场
mv /var/lib/etcd /var/lib/etcd.bak-20260913-164456  # 别 rm!这是回滚退路
③ 从快照恢复
etcdutl snapshot restore /var/backups/etcd/etcd-20260913-164401.db 
--name k8s-master01
--initial-cluster "k8s-master01=https://192.168.16.11:2380"
--initial-advertise-peer-urls "https://192.168.16.11:2380"
--data-dir /var/lib/etcd
④ 恢复控制平面
mv /tmp/hold/*.yaml /etc/kubernetes/manifests/
systemctl start kubelet
轮询 kubectl get --raw=/readyz 直到就绪
⑤ 验证
比对 UID、排查异常 Pod、记录 RTO

③ 那三个参数(--name / --initial-cluster / --initial-advertise-peer-urls)不是可选的,原因是:snapshot restore 不是「把数据塞回正在运行的 etcd」 ,而是在你指定的 --data-dir 里重建一个全新的 etcd 数据目录。既然是个新成员,它就得知道「我叫什么、属于哪个集群、peer 地址是什么」。

也正因为如此,多节点 etcd 的恢复会比单节点麻烦得多:每个成员都要用相同的 --initial-cluster-token ,各自的 --name 和 --initial-cluster 一起恢复,才能重建 Raft 关系。


六、验证:UID 才是决定性证据

恢复完:

复制代码
【命名空间】  prod-app  Active ✅
【业务对象】
  pod/web-5c4bbcf55-2j56z  1/1  Running    ← Pod 名字与删除前完全相同
  pod/web-5c4bbcf55-5hs2k  1/1  Running
  pod/web-5c4bbcf55-95pwn  1/1  Running
  service/web-svc  ClusterIP  10.0.24.193  ← ClusterIP 完全相同
  deployment.apps/web  3/3
  secret/db-credential  Opaque
【UID 比对】★ 决定性证据
✅ NS_UID     一致  d51f9126-c2c8-4a5e-937b-b226f98d3834
✅ DEPLOY_UID 一致  6a00afee-75ca-4faf-a0fc-6b8c2713a4b9
✅ SVC_UID    一致  a29da680-baac-4502-a344-64ef59ec6e63
✅ CM_UID     一致  e0b5a150-5719-4594-babf-f45bab9704a2
✅ SECRET_UID 一致  bfa04906-2f4b-494b-945f-555752b11590
【Secret 明文】✅ SuperSecret2026  (演练用的假密码)

为什么一定要比 UID,只比名字不行?

因为「名字对得上」完全可以被伪造成「重新创建了一批同名对象」。而 UID 是存在 etcd value 里的普通字段,物理快照把它原样存下来、原样恢复出来,apiserver 读到的就是原来的 UID。

这件事在生产上很要命:很多系统拿 UID 当外部引用 ------ OwnerReference、Event 的 involvedObject.uid、审计日志、外部 CMDB、备份工具。UID 一变这些引用全断,典型表现是 OwnerReference 失效导致级联删除不工作,或者外部系统认为「这是新对象」。

<

相关推荐
KIDULT°2 小时前
MySQL系列 (1):数据库原理
运维·数据库·mysql·云计算
wewowilling4 小时前
认识微服务
java·运维·微服务·springcloud
玖石书5 小时前
Ubuntu 24.04 UFW 全命令攻略:从入门到端口开放实战
linux·运维·ubuntu
weifont6 小时前
自动化抓取和上架商品:能不动手,就少动手
运维·前端·自动化
拂拉氏7 小时前
【知识讲解】 Linux之动静态库的了解
linux·运维·服务器·动静态库
Mikko77 小时前
Linux 重启后日志没了?进程被 OOM 杀了去哪看?logrotate 之后日志还往旧文件写?三台真机实测日志与性能排查
linux·运维·centos
光依旧8 小时前
发版就掉请求?Spring Boot优雅停机的四步
运维·spring boot·nginx·高可用·优雅停机·mcp server
yichengerp9 小时前
电子行业常用erp系统有哪些?推荐适合中小电子厂的易呈erp软件,操作简单,易落地
大数据·运维·人工智能·云计算·制造
天国梦9 小时前
国密门禁品牌怎么选?2026年最新3个避坑要点
运维·服务器·数据库·python
信可维10 小时前
自动化操作多平台账号:登录态、双会话与 User Data 的 3 个坑
运维·自动化