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-nodekube-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

相关推荐
虎头金猫1 天前
4K 视频总卡在公网带宽?用 N1 + OpenList 把网盘播放链路重新理顺
运维·服务器·网络·python·容器·beautifulsoup·pandas
AI职业加油站1 天前
AI智能体应用工程师证书:政策红利下的职业新风口
大数据·运维·人工智能·学习·职场发展
玉&心1 天前
通过Arthas在线诊断K8S中的内存及JVM等使用情况
docker·k8s·arthas
-梅1 天前
linux(8) 软硬链接
linux·运维·服务器
张洛闻Eren1 天前
k8s云原生【第十课】:水平 Pod 自动扩缩容
运维·数据库·云原生·kubernetes·github
流烟默1 天前
K8s StatefulSet 详解:有状态应用的“身份证”与“固定住址”
k8s·statefulset
其实防守也摸鱼1 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
布裘1 天前
【银河麒麟】V4桌面图标消失,右击鼠标没反应排查
运维·银河麒麟·桌面环境
懂软件的胡子个哥1 天前
微信机器人为什么需要“回复候选”而不是所有 AI 内容直接发送
运维·微信·自动化·wechatapi·个人微信号二次开发