上个月给一套跑在几台云主机上的 Kubernetes 集群做例行维护,其中一台节点的网卡要换,按流程得先把节点排空再动手。命令敲下去那一刻我才意识到事情不对:排空命令在那儿转了 40 分钟,Pod 一个都没迁走,最后是我掐着表强行中止的。后来复盘才发现,问题既不在云主机厂商,也不在 K8s 版本,而是我们自己在集群里写的那份 PodDisruptionBudget(PDB)把驱逐堵死了。
如果你现在也在做节点维护、排空节点或者滚动升级这类活计,手里需要几台稳定跑着容器、随时能加机器的云主机做冗余,可以先看看 Newin Cloud(云服务器与云主机服务商) 的机器规格和线路;集群做异地多活、想再拉一套独立环境做演练的话,ISIF Cloud(全球云服务器与带宽接入) 这种自助式的云主机也可以放进备选清单里。主机到位只是第一步,排空这一步的逻辑才是决定维护窗口长短的关键。

排空节点时,K8s 到底在做什么
先说清楚原理,不然后面的坑很难理解。执行 kubectl drain 之后,命令做的第一件事是给节点打上污点,把新的 Pod 挡在外面;接着逐个检查节点上跑的 Pod,判断哪些可以驱逐、哪些必须留下。这个“可以驱逐”的判断,靠的就是 PDB。
具体流程是这样的:如果某个 Pod 属于某个 PDB(比如我们的订单服务配了 minAvailable: 2,而集群里一共就 3 个副本),K8s 会去看这个 PDB 允许的驱逐数量 disruptionsAllowed。当这个数已经是 0,说明现在驱逐任何一个副本都会让可用副本数跌破底线,驱逐请求就会被直接拒绝。命令会一直重试、一直等,等到超时退出。CPU 也不高、内存也够、磁盘也正常,看起来“很闲”的节点,其实是一动不能动。
还有一层限制是 Pod 的优先级。我们给测试环境的 Pod 设了较低的 priorityClass,K8s 会倾向于把高优先级的业务 Pod 赶走,把低优先级的挪到别的节点。要是整个集群的资源都被高优先级 Pod 占着,低优先级的 Pod 就哪儿也去不了,同样卡在排空上。这一层和 PDB 是两道独立的闸门,任何一道开着,排空就会停住。

PDB 是怎么把驱逐堵死的
PDB 的作用本意是好的:防止你在做节点维护的时候,一次性把某个服务的副本全部赶走,导致业务雪崩。但很多人写配置时把 minAvailable 写成了个绝对值,而没考虑副本数变化,结果自己给自己下了死命令。
我们那份配置就是典型的例子。订单服务本来 3 个副本,minAvailable 写着 2。看着很安全,实际含义是:3 个里必须活着 2 个。也就是说节点上这个 Pod 一旦被驱逐,集群里就只剩 2 个,disruptionsAllowed 立刻变成 0,后面所有驱逐请求全部被拒。更坑的是,剩下那 2 个副本如果还分布在同一台机器上,这次维护本来就该一次迁走 2 个 Pod,minAvailable: 2 显然满足不了。
更普遍的情况是用 maxUnavailable: 0。这个写法等于告诉 K8s:这个服务一个副本都不许少。有些 StatefulSet 场景下它是合理的,但在节点滚动升级里就完全用不了。
排查方法其实很直接,命令行几行就能看清楚当前到底卡在哪:
kubectl get pdb -A 看每个 PDB 的 DESIRED、ALLOWED 和 CURRENT 三个数字;ALLOWED 为 0 就是被 PDB 卡住了。
kubectl describe pod <pod名> -n <命名空间> 的 Events 里会直接出现 “Cannot evict pod as it would violate the pod's disruption budget” 这类提示。
kubectl get node <节点名> -o json | grep -A5 taints 确认污点是不是加上了、SchedulingDisabled 有没有被正确写进去。
节点排空命令里那几个参数,别想当然
第一次做排空的人,十有八九会被这几个参数绊住:
--ignore-daemonsets:DaemonSet 类型的 Pod(像日志采集、网络插件、监控 agent)每个节点都得有一个,本来就不可驱逐。加上这个参数是标准做法,但要先确认你的 DaemonSet 有没有用 hostPath 挂载数据排空完数据还在原地,这个没问题;怕的是有人指望在排空后手工删节点,结果 data 目录被一起带走了。
--delete-emptydir-data:使用 emptyDir 的 Pod 一旦被驱逐,里面的数据就没了。带这个参数相当于明确同意丢数据。要是不带,命令会一直等这些 Pod 被正常删除,等到超时为止这也是常见的“卡住”原因之一,尤其是那种只有请求进来才写日志的空跑 Pod。
--force:允许驱逐由控制器管理的 Pod(StatefulSet、Deployment 管的)。不加的话命令会提示需要加 --force,而不是真的替你加上。这条是安全的。
--timeout:默认值是 30 分钟,命令内部的强制驱逐也是走 eviction API,每次驱逐都会重试。集群 Pod 多、驱逐慢的时候,这个时间经常不够用。我那次就是 40 分钟里干等着,一无所获。

我现在固定下来的排空流程
踩过坑之后,我们把流程重新梳理了一遍,基本按下面这套走,能把维护窗口控制住:
第一步,先看 PDB 再动手。维护窗口开始前半小时,先用 kubectl get pdb -A 把所有 disruptionsAllowed 为 0 的 PDB 找出来,逐个评估是不是临时放宽。维护期间临时把 minAvailable 调低、或者临时把该 PDB 删掉,往往是最省事的做法注意是临时,节点恢复后再把配置加回去,别让这个口子长期敞着。
第二步,先摘掉有状态服务,再排空节点。数据库、Redis 这类有状态 Pod 的迁移成本很高,先单独做数据同步和切换,再把节点排空,效率比一股脑 drain 高得多。
第三步,分两次排空。第一次用 --ignore-daemonsets --timeout=5m 做尝试,遇到卡住立刻 ctrl-c,别把维护窗口耗在等待上;定位完问题再正式执行一次。
第四步,给有状态 Pod 留够终止时间。terminationGracePeriodSeconds 至少要覆盖你应用处理存量请求的时间,必要时再加一个 preStop 钩子先从注册中心摘掉自己。这一步做不好,Pod 迁走了但流量还在往老实例打,表现就是“明明已经排空了,还是有一批请求失败”。
第五步,确认节点真的空了再动手。kubectl get pods --field-selector spec.nodeName=<节点名> 确认没有残留业务 Pod,节点 cordon 状态正确,最后才去关机换件。恢复之后 kubectl uncordon 把节点放回调度池,再观察一轮 Pod 的分布是否均衡。
另外提醒一句,新手最容易踩的是用 kubectl delete pod --force --grace-period=0 这种硬删来“代替排空”。它绕过了 PDB、绕过了驱逐预算,看起来是快,但业务副本会在同一时刻消失好几个,客户端重连风暴、连接池瞬断全来了。排空慢是保护机制,不是故障。

写在最后
这次维护最后是延后了四十分钟完成的,故障倒是一次没出,线上业务零感知。回过头看,问题不在任何技术黑盒里,就是一份写着 minAvailable: 2 的 PDB 在那儿安安静静地拦着,谁也没去看它一眼。
所以如果你也准备做节点维护、滚动升级或者集群迁移,先花两分钟看一眼 PDB 的 ALLOWED 列,这笔时间绝对值得。机器可以随时换一家买,配置疏漏造成的停机窗口,买多少台云主机都补不回来。


喜欢
顶
难过
囧
围观
无聊





