etcd备份与恢复:删了 etcd.yaml,容器为什么还在跑?——一次 K8s 控制面死锁排查实录

一、背景:我在做什么

目标是在单节点集群(kubeadm 部署,节点名 k8s-master)上做一次完整的 etcd 快照备份与恢复演练,流程如下:

  1. 创建测试命名空间和资源
  2. etcdctl snapshot save 做快照
  3. 删除测试资源模拟故障
  4. 停掉 etcd
  5. etcdctl snapshot restore 恢复
  6. 验证数据回来

演练顺利走到了第 3 步,然后卡死在了第 4 步------etcd 停不下来。


二、现象:三个诡异的问题

现象 1:docker stop 之后,etcd 自己又活了

js 复制代码
docker stop <etcd-container-id>
# 等 30 秒
docker ps | grep etcd   # 又出现了,而且容器 ID 变了

现象 2:把 etcd.yaml 移走,容器依然存活

js 复制代码
mv /etc/kubernetes/manifests/etcd.yaml /etc/kubernetes/manifests/etcd.yaml.bak
sleep 30
docker ps | grep etcd   # 还在跑

按官方文档,静态 Pod 的清单文件删除后,kubelet 应该自动清理对应的 Pod。但这里没有。

现象 3:改用 rm 之后,日志开始疯狂爆炸

js 复制代码
rm /etc/kubernetes/manifests/etcd.yaml
journalctl -u kubelet -f

输出刷屏:

js 复制代码
E1109 06:59:01 kubelet[36304] mirror_client.go:132] 
Failed deleting a mirror pod "etcd-k8s-master_kube-system": 
Delete https://192.168.10.100:6443/api/v1/namespaces/kube-system/pods/etcd-k8s-master: 
read tcp 192.168.10.100:36152->192.168.10.100:6443: use of closed network connection

每隔 10 秒一条,无限循环。

三、逐个击破

问题 1 解释:静态 Pod 的自动重启是设计行为

这个最简单,先排除掉。

静态 Pod 的 RestartPolicy 固定为 Always 且不可修改 。容器非正常退出后,kubelet 的 podSync 循环会检测到"容器状态与期望不符",于是重新 CreateContainer。

所以链路是:

js 复制代码
docker stop → 容器退出 → CRI 上报事件 
→ kubelet podSync 发现容器非预期终止 
→ 查内存 staticPods 里还有 etcd-k8s-master 
→ 重新创建容器

结论:这不是 bug,docker stop 只能"临时"停掉静态 Pod。

验证手段:

js 复制代码
# 看重启计数
docker ps -a | grep etcd

# 看 kubelet 的重建记录
journalctl -u kubelet --since "5 min ago" | grep -i -E "etcd|static"

问题 2 解释:mv 和 rm 触发的文件事件不一样

这是第一个真正的坑。

kubelet 的静态 Pod 管理器通过 fsnotify(inotify) 监听 staticPodPath 目录,代码里处理的事件是:

文件事件 kubelet 动作
fsnotify.Create podAdd
fsnotify.Write podModify
fsnotify.Remove podDelete

而 mv a.yaml a.yaml.bak 触发的是 IN_MOVED_FROM / rename 事件 ,不是 IN_DELETE。在某些版本/环境下,这个事件不会被翻译成 podDelete。

所以 kubelet 根本没意识到"这个 Pod 该被删了",容器自然继续跑。

经验 :想通过清单文件停掉静态 Pod,必须用 rm,不能用 mv 做"备份式移走"。

顺带排查掉的两个伪原因:

js 复制代码
# 1. 监听目录对不对
grep -i -E "staticPodPath|fileCheckFrequency" /var/lib/kubelet/config.yaml

# 2. 检查频率是不是被改大了
systemctl cat kubelet | grep -i "file-check"

第二条命令没有输出 ------但这不是原因。没显式设置意味着使用默认值 --file-check-frequency=20s,等了 30 秒以上早该生效了。

踩坑提醒 :systemctl cat 没输出 ≠ 参数没生效,很可能是走了默认值。别把"没配置"当成"配置有问题"。


问题 3 解释:这才是真正的死锁

rm 之后日志爆炸,说明 kubelet 已经检测到文件删除了 (它进入了 Failed deleting a mirror pod 这一步),但删不掉。

为什么删不掉?因为静态 Pod 在 API Server 里有一个对应的 mirror pod(镜像 Pod) 。kubelet 清理静态 Pod 的完整流程是:

js 复制代码
检测到清单文件删除 
→ 调用 API Server: DELETE /api/v1/namespaces/kube-system/pods/etcd-k8s-master 
→ 等 API Server 确认删除 
→ 才停止本地容器

而错误信息是:

js 复制代码
read tcp 192.168.10.100:36152->192.168.10.100:6443: use of closed network connection

注意是 use of closed network connection,不是 connection refused。区别在于:

  • connection refused:端口没人监听,服务没起来
  • use of closed network connection:连接已经被对端主动关闭,说明 API Server 在响应过程中崩溃/退出了

API Server 为什么挂? 因为 API Server 强依赖 etcd 存储数据。前面反复 docker stop etcd、mv 清单文件,导致 etcd 状态异常,API Server 连不上 etcd 就崩了。

于是形成死锁闭环:

js 复制代码
  
删 etcd.yaml 
→ kubelet 要删 mirror pod 
→ 需要调 API Server 
→ API Server 因为 etcd 异常已崩溃 
→ 删除失败,10 秒后重试 
→ 日志风暴,容器停不下来

核心结论:不是 inotify 坏了,不是 kubelet 没感知,而是控制面组件互相依赖导致的循环阻塞。

四、破局方法

死锁的破局思路只有一条:先恢复通信,再执行删除。

第一步:放回清单文件,解开 kubelet 的删除执念

js 复制代码
mv /etc/kubernetes/manifests/etcd.yaml.bak /etc/kubernetes/manifests/etcd.yaml

kubelet 发现文件又在了,立刻停止重试删除,日志风暴停止。

第二步:重启 kubelet,重建状态机

js 复制代码
systemctl restart kubelet

补充一个之前踩过的坑:如果重启后看到 pods "etcd-k8s-master" already exists,说明 API Server 里残留着旧的 mirror pod 对象,而磁盘上清单文件当时不存在。这条日志反而证明 kubelet 已经不再认为 etcd 该存在,方向是对的。

第三步:验证控制面恢复

js 复制代码
sleep 30
kubectl get ns
kubectl get pods -n kube-system | grep -E "etcd|apiserver"

能正常返回,说明死锁已破。

附:本次涉及的排查命令速查

js 复制代码
# 静态 Pod 清单目录与检查频率
grep -i -E "staticPodPath|fileCheckFrequency" /var/lib/kubelet/config.yaml
systemctl cat kubelet | grep -i -E "ExecStart|file-check"

# inotify 资源与 watch 状态
cat /proc/sys/fs/inotify/max_user_watches
cat /proc/sys/inotify/max_user_instances
ls -l /proc/$(pgrep -x kubelet)/fd 2>/dev/null | grep -i manifest

# 容器与重启计数
docker ps -a | grep etcd

# kubelet 日志定向过滤
journalctl -u kubelet --since "10 min ago" | grep -i etcd | tail -30

# API Server 健康度
docker ps -a | grep apiserver
docker logs $(docker ps -aqf name=apiserver) --tail 20

总结原因:

  1. mv etcd.yaml etcd.yaml.bak容器还在跑,触发的是 IN_MOVED_FROM / rename 事件 ,不是 IN_DELETE。在某些版本/环境下,这个事件不会被翻译成 podDelete。 所以 kubelet 根本没意识到"这个 Pod 该被删了",容器自然继续跑

  2. 删了 rm etcd.yaml 容器还在跑,不是因为 kubelet 没看见,而是因为它看见了、想删、但 API Server 挂了删不掉------这是控制面循环依赖导致的死锁,破局的关键是"先恢复通信,再执行删除"

总结解决方案:

本次是在尝试rm etcd.yaml后,出现apiServer日志爆炸(文中有描述),然后又立马做了mv /backup/etcd/etcd.yaml /etc/kubernetes/manifests/etcd.yaml处理,之后日志爆炸停止,待检查集群状态都正常后,再次直接使用docker stop <container_id>,最终停止了etcd。

js 复制代码
# 解决`docker stop`直接停止etcd,而不是再去折腾文件
使用 `docker stop etcd_container_id` 代替 `mv etcd.yaml etcd.yanl.bak`
使用 `docker stop etcd_container_id` 代替 `rm etcd.yaml`
相关推荐
weirong3 小时前
Operator 的测试该写到哪一层?139 个用例之后我划的三条线
kubernetes
逐流人7 小时前
手工建集群到全自动发布:k8s集群部署与DevOps流水线整合实战
ci/cd·云原生·kubernetes·云计算·devops
我不会起名字3227 小时前
K8s Pod 反复重启 CrashLoopBackOff:从 exit code 到根因的 5 步排查
云原生·容器·kubernetes·k8s
我不会起名字3228 小时前
K8s 节点磁盘打满导致 Pod 被驱逐:4 类占用与回收顺序
运维·kubernetes·k8s·diskpressure·evicted
wzq11_6661 天前
Kubernetes集群——存储管理
云原生·容器·kubernetes
闲云自留地2 天前
K8s 的记忆大脑!ETCD 集群部署、etcdctl 命令与备份恢复全通关
容器·kubernetes·etcd
weirong2 天前
Operator 上生产前补的 6 件事,和 5 件我明确不做的
kubernetes
孙克旭_2 天前
k8s基础组件与创建资源
运维·docker·容器·kubernetes
吴声子夜歌3 天前
Kubernetes权威指南——网络原理(一)
网络·容器·kubernetes