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 restoreetcdctl 挪到了独立的 etcdutl3.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 失效导致级联删除不工作,或者外部系统认为「这是新对象」。

<

相关推荐
牢姐与蒯1 小时前
Linux进程间通信(三).基于匿名管道的进程池的实现
linux·运维·服务器·ubuntu
景煜拾年1 小时前
RHCA-Docker知识点整理
运维·笔记
骇客野人9 小时前
Redis 完整安装部署方案(Linux,分【源码编译】+【Docker】两种,推荐生产用源码,测试快速用 Docker)
运维
蓝速科技12 小时前
会议室门牌公告通知发布选型与落地指南丨蓝速科技
大数据·运维·数据库·人工智能·科技
chicheese13 小时前
Linux 面试速查表:从初级到高级,附高频场景答题模板
linux·运维
Linux-lucky13 小时前
32-38-Linux学习之旅之MySQL综合
linux·运维·学习·mysql·ubuntu
智能运维指南14 小时前
2026年ITSM系统怎么选?四款主流方案与五维评估模型
运维·itsm·嘉为蓝鲸
我命由我1234514 小时前
Git 推送报错:error: src refspec main does not match any
运维·git·gitee·github·运维开发·学习方法·版本控制
骇客野人14 小时前
测试环境MySQL迁移Vastbase(海量数据库)完整改造与落地实施方案
运维·服务器