此冬歌咏头像
关注

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 会变。

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

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 备份完必须马上校验,不然等于没备份

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 上敲:

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

然后备份,然后开删:

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 排查

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

# 数据目录确实写进去了
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

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

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

再看 apiserver:

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

数一下还在跑的容器:

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 修法

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
待停止容器数: 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 失效导致级联删除不工作,或者外部系统认为「这是新对象」。

<

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/a969667/article/details/166249259

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--