K8s 节点故障实战:优雅驱逐 31 秒,硬故障 331 秒,以及那个永远 Pending 的 Pod

先把结论放这儿

下表是这次演练的实测结果速览:

项 实测值
场景 A · 计划内维护 kubectl cordon + drain 一个节点
优雅驱逐 31 秒清空节点,6 个 Pod 全部迁走,业务零中断
场景 B · 计划外故障 停掉 node01 的 kubelet + 容器运行时(模拟宕机)
节点被判 NotReady 26~44 秒 (默认 node-monitor-grace-period = 40 秒)
Pod 开始被驱逐 331 秒后
331 的构成 40 秒(判死)+ 300 秒(默认容忍期)≈ 340 秒
最值钱的发现 被 nodeSelector 钉死的负载,替换 Pod 永远 Pending

31 秒和 331 秒差了 10 倍,但这两个数字都不是重点。

331 秒看一眼参数就能算出来,那只是默认计时的必然结果。真正让这次演练值得写出来的,是那两个已经被驱逐、却永远回不来的 Pod ------ 它们暴露的不是 K8s 的 bug,而是我自己写法上的一个可用性单点。

环境:VMware 上自建的 3 节点实验集群,K8s v1.34.10,单 master,每台 4C/4G。这是 lab,不是生产。(这次演练时集群还在用 cri-dockerd + Docker,后来才迁到 containerd ------ 那是另一篇的事。)

还有一处必须先坦白:我的"硬故障"是停掉节点上的 kubelet 和容器运行时模拟的,机器并没有真的断电。这个差别会实质性影响结论,放到第八节单独说。


一、节点故障,是运维里最考验判断力的场景

因为它压着一个根本矛盾:

你想尽快把故障节点上的 Pod 挪走,但你没法确定这个节点是不是真的死了。

网络抖动 10 秒、交换机重启、控制平面自己卡了 ------ 这些都会让节点"看起来像死了"。如果你在这个状态下立刻把 Pod 迁到别处,原节点"复活"之后就会出现两份同名副本,而如果它们都在写同一个数据库,坏掉的就不只是可用性了。

这 331 秒存在的全部理由就是它。 记住这句话,比记住那个数字有用得多。

节点故障分两类,处理方式完全不同,很多人在这里混:

自愿中断(voluntary) 非自愿中断(involuntary)
触发者 你:drain、升级节点、缩容 节点自己:宕机、断网、kubelet 挂掉
机制 调用 Eviction 子资源,要过 PDB 打 NoExecute 污点,等容忍期到期
能通知应用吗 能(SIGTERM + 优雅关闭) 不能(节点都死了,通知谁)
实测耗时 31 秒 331 秒

这篇把两条路都真走一遍。


二、动手之前,先搞清三件事

2.1 cordon / drain / uncordon 的区别

动作 作用 驱逐已有 Pod?
cordon 标记节点不可调度,只挡新 Pod ❌
drain cordon + 驱逐现有 Pod ✅
uncordon 恢复可调度 ---

我习惯先 cordon 停几分钟观察,确认没有新 Pod 再落上来,然后再 drain。

2.2 节点"死了"是怎么判定的?不是 10 秒

kubectl get nodes 里那行 Ready 只是摘要,真实状态是一组 Condition。判定"心跳断了"靠的是 NodeLease:

参数 默认值 作用
nodeStatusUpdateFrequency 10s kubelet 上报完整 Node 状态的间隔
nodeMonitorGracePeriod 40s 控制平面容忍多久收不到心跳
nodeMonitorPeriod 5s 控制平面检查心跳的间隔

判定 NotReady ≈ 40 秒,不是 10 秒。 10 秒是上报频率,不是阈值 ------ 这两个数答混了,一听就知道没实际调过。

2.3 那为什么 NotReady 之后 Pod 还不走?

因为还有一段"容忍期"。节点被判 NotReady 后,node controller 会给它打上污点:

复制代码
node.kubernetes.io/unreachable:NoExecute

NoExecute 的意思是"不容忍这个污点的 Pod 就得走"。那为什么不是立刻走?因为 apiserver 的 DefaultTolerationSeconds 准入插件会自动给每个 Pod加一段容忍:

yaml 复制代码
tolerations:
- key: node.kubernetes.io/unreachable
  operator: Exists
  effect: NoExecute
  tolerationSeconds: 300   # ← 默认 5 分钟

于是:

复制代码
   0s ───────────── 40s ───────────────────────── 340s
    │               │                            │
 节点失联        判 NotReady                  Pod 被驱逐
                (grace period)            (300s 容忍期到期)

331 秒 = 40 秒 + 300 秒,再减掉一点调度抖动。 实测落在预期区间内。


三、场景 A:优雅驱逐(计划内维护)

3.1 还没开始就踩了一个坑:反亲和性不是越硬越好

先部署测试负载:一个 4 副本的 Deployment,要求 Pod 尽量分散到不同节点。

第一版我用的是硬反亲和 (requiredDuringScheduling...)------ 每台机器最多放一个同类 Pod。而我的集群只有 2 个可调度节点(master 带 control-plane 污点),4 个副本必然有无家可归的:

复制代码
FailedScheduling: 0/3 nodes are available:
  1 node(s) had untolerated taint(s),
  2 node(s) didn't match pod anti-affinity rules

硬反亲和 + 副本数 > 可调度节点数 = Pod 永久 Pending。 这是个很实在的设计陷阱。

改成软反亲和(preferred...,"尽量分散"而不是"必须分散")之后就正常了:2 个副本落在 node01,2 个落在 node02。

3.2 drain 实录

bash 复制代码
kubectl cordon k8s-node01
kubectl drain k8s-node01 --ignore-daemonsets --delete-emptydir-data
复制代码
node/k8s-node01 cordoned
Warning: ignoring DaemonSet-managed Pods:
kube-system/calico-node-pkv78, kube-system/kube-proxy-jfv68
evicting pod prod-app/web-5c4bbcf55-2j56z
evicting pod default/rc-demo-tppqg
evicting pod default/my-app-6b6fb547ff-lwl5s
evicting pod default/rc-demo-p9q2n
evicting pod drill-ns/web-6745984cb5-wx5nh
evicting pod drill-ns/web-6745984cb5-hfjxm
pod/web-6745984cb5-hfjxm evicted
pod/web-6745984cb5-wx5nh evicted
pod/web-5c4bbcf55-2j56z evicted
pod/my-app-6b6fb547ff-lwl5s evicted
pod/rc-demo-tppqg evicted
pod/rc-demo-p9q2n evicted
node/k8s-node01 drained
★ 驱逐耗时: 31 秒

结果:

观测项 结果
驱逐 Pod 数 6 个
驱逐耗时 31 秒
Pod 去向 全部在 node02 重建
DaemonSet 保留 (calico-node、kube-proxy 没被动)
业务中断 无

3.3 三个必须知道的细节

① 为什么必须加 --ignore-daemonsets。 DaemonSet 的语义是"每个节点必须有且仅有一个"。赶走 calico-node,这个节点的 Pod 网络就瘫了;赶走 kube-proxy,Service 转发就失效。所以 drain 默认拒绝执行,必须你显式声明"我知道,忽略它们"。

② 31 秒花在哪。 几乎全在"等每个 Pod 走完优雅关闭":发 SIGTERM → 等进程退出 → 超时则 SIGKILL。我的应用是 nginx,对 SIGTERM 响应很快,所以 6 个 Pod 加起来才 31 秒。如果应用不处理 SIGTERM,每个 Pod 都要等满 terminationGracePeriodSeconds(默认 30 秒),时间会成倍涨。

③ uncordon 不会把 Pod 送回原节点。 这是最常见的一厢情愿:Pod 早就在新节点重建完了,uncordon 只是允许未来的新 Pod 调度上来。


四、场景 B:硬故障(计划外)

4.1 怎么"杀死"一个节点

我在 node01 上直接停掉三件套:

bash 复制代码
systemctl stop kubelet cri-docker docker
# 停止前:10 个容器运行中 → 停止后:0 个

4.2 实测时序

复制代码
17:28:50  停止 node01 的 kubelet / cri-docker / docker
    │
    │ 约 26~44 秒(默认 40 秒阈值,详见 4.3「计时起点」)
    ▼
17:29:34  节点变 NotReady(status=Unknown)
          Conditions 全部转为 Unknown:
            MemoryPressure / DiskPressure / PIDPressure / Ready
          自动打上污点:
            node.kubernetes.io/unreachable:NoExecute
            node.kubernetes.io/unreachable:NoSchedule
          ⚠️ 此刻 Pod 仍然是 Running ------ 并没有被驱逐
    │
    │ 331 秒
    ▼
17:35:05  TaintManagerEviction 触发:
          "Marking for deletion Pod drill-ns/pinned-xxx"
          3 个 Pod 进入 Terminating,ReplicaSet 开始创建替换 Pod
    │
    ▼
          ★ 替换 Pod 全部 Pending ------ 调度不上去

NotReady 那一瞬间,节点上的业务 Pod 状态是这样的:

复制代码
NAME                    READY   STATUS    AGE    IP              NODE
pinned-6c798fc8d7-7hhhm  1/1    Running   53s    10.244.85.232   k8s-node01
pinned-6c798fc8d7-9z4fr  1/1    Running   53s    10.244.85.231   k8s-node01
pinned-6c798fc8d7-fm888  1/1    Running   53s    10.244.85.230   k8s-node01
web-6745984cb5-gcxxh    1/1    Running   2m35s  10.244.58.227   k8s-node02
web-6745984cb5-jgqk7    1/1    Running   2m14s  10.244.58.232   k8s-node02

节点已经"失联"了,Pod 却还显示 Running。 因为 apiserver 手上只有最后一次上报的快照,它无从判断这些容器现在是死是活。这也算是"为什么要有容忍期"的另一个视角:控制平面看到的信息,本来就是过期的。

4.3 一个关于"计时起点"的坑

这次演练里,NotReady 我先后得到过两个数:从脚本启动那一刻算是 26 秒,从真正停 kubelet 那一刻算是 44 秒。差的 20 秒,是因为观察脚本比故障注入晚了 20 秒才启动。

两个数都没错,但报出去的结论会差近一倍。做这类计时,第一步是先定"时间原点",并且写清楚原点是什么。我现在的习惯是:把故障注入命令和计时器写在一起。


五、★ 最值钱的发现:我用 nodeSelector 把可用性钉成了单点

驱逐本身完全正常。问题出在 331 秒之后。

为了模拟"钉死在某个节点上"的负载(GPU 节点、特定硬件、本地数据盘这类),我建了 3 个 Pod,加了约束:

yaml 复制代码
nodeSelector:
  kubernetes.io/hostname: k8s-node01

节点失联 → 331 秒后 Pod 被正常驱逐 → ReplicaSet 照常创建了 3 个替换 Pod。然后:

复制代码
pinned-6c798fc8d7-znct4  0/1  Pending     ← 永远调度不上去
pinned-6c798fc8d7-k4zkr  0/1  Pending  
pinned-6c798fc8d7-4bjbn  0/1  Pending  

事件里只有一句:

复制代码
0/3 nodes are available:
  1 node(s) didn't match Pod's node affinity/selector,
  2 node(s) had untolerated taint...

这个 Deployment 会永远 Pending 下去。ReplicaSet 没有超时机制 ------ 它会一直重试到天荒地老。

对比一下更刺眼:同一时刻、同一命名空间里没有节点约束 的那 4 个 w

相关推荐
吴声子夜歌8 小时前
Docker入门与实战——实战案例(编程开发)(一)
运维·docker·容器
空空dream8 小时前
04 | 用 Jenkins 打通 CI自动构建镜像并推送到本地 Harbor
运维·ci/cd·jenkins
海宇AI8 小时前
零信任架构实战:基于海宇运营商近3个月平均账单构建自动化免押租赁授信网关
运维·人工智能·架构·自动化
此生决int8 小时前
Linux系列(06)——进程概念(上)
linux·运维·服务器
海宇服务8 小时前
零信任架构实战:基于海宇股权穿透构建自动化招投标供应商关联排查网关
运维·人工智能·架构·自动化
wdfk_prog9 小时前
LWIP教程 13:从 `dhcp_start()` 到 `DHCP_STATE_BOUND`——DHCPv4、ACD、Timer 与真实抓包
运维·网络协议
星云低代码开发平台9 小时前
AI 连接器运行健康度:指标、异常恢复和联调清单
大数据·运维·网络·数据库·人工智能·mcp
Sayai9 小时前
Hadoop 集群安全加固实战:YARN 未授权访问漏洞修复,firewalld 白名单 + Nginx 反向代理 Basic Auth 全流程
运维·hadoop·nginx·安全·网络安全
云飞云共享云桌面10 小时前
智能装备数字化转型:云飞云云桌面助力苏州工厂共享 SolidWorks 算力
运维·服务器·3d·自动化·制造
yichengerp10 小时前
中小电子厂用什么ERP?电子厂ERP管理软件实施要点与易呈方案推荐
大数据·运维·人工智能·云计算·制造