cordon 和 drain 是两件事,只是大家习惯一口气说完。cordon 给节点打上 spec.unschedulable,让调度器不再选它;drain 则把已经在上面跑的 Pod 请走。前者瞬时完成、随时可逆,后者是一个循环,要逐个 Pod 去跟驱逐 API 打交道。
矛盾在于:drain 是一个带安全护栏的客户端流程。它在删除任何东西之前会先拒绝,而每一个能越过拒绝的参数,都在拿掉一部分护栏。搞清楚哪个参数对应哪一类拒绝,就是维护窗口和线上事故之间的差别。
cordon 关门,drain 清场
kubectl cordon node-1 把 spec.unschedulable 置为 true,调度器随即把该节点从新 Pod 的候选里滤掉。已经在跑的 Pod 照常服务,这正是要先 cordon 的原因:先停止接收新工作,再开始搬走已有的工作。cordon 也是让 drain 有意义的前提——不封门,调度器会把替代副本直接放回你正在清空的那台机器上。kubectl drain 自己也会打这个标记,所以单独先执行一次 cordon 只是稳妥,不是必需。kubectl uncordon 就是反向的同一个补丁,调度器立刻会重新使用这台节点——先确认 kubelet 已经 Ready 再放它回去。
接下来 drain 逐个驱逐 Pod:服务端支持驱逐 API 时走驱逐,不支持时退化为普通的删除。驱逐是一种优雅终止,terminationGracePeriodSeconds 会被尊重,drain 会一直等到它结束。但优雅期是一扇窗口而不是保证——忽略 SIGTERM 的 Pod 到期照样被杀,真正保护数据的是 Pod 自己的关闭逻辑。镜像 Pod(mirror pod)是例外:它们由 kubelet 依据静态清单管理,根本无法通过 API server 删除,所以 drain 直接跳过,而不是在这里失败。
拒绝清单一票否决
drain 在删除任何东西之前先拒绝,而且一个不合规的 Pod 会挡住这台节点上的全部驱逐,而不只是它自己那一次。三类拒绝对应三个不同的参数,kubectl 的报错文案里写得很直白:没有控制器的 Pod 会得到 cannot delete Pods that declare no controller (use --force to override),管理它的资源已经消失的 Pod 也一样;用了 emptyDir 的会得到 Pods with local storage (use --delete-emptydir-data to override);而 DaemonSet 的 Pod 得到 DaemonSet-managed Pods (use --ignore-daemonsets to ignore)。
DaemonSet 这一类是加了参数也不改变结果的那一种。无论有没有 --ignore-daemonsets,drain 都不会删 DaemonSet 的 Pod,因为 DaemonSet 控制器无视不可调度污点,删掉会立刻被重建;这个参数只是让 drain 不再拒绝而已。还有两类 Pod 无论如何都会落到被排空的节点上:容忍 node.kubernetes.io/unschedulable 污点的,以及直接写死 nodeName、绕过调度器的。容忍这个污点的通常就是 DaemonSet,文档因此明确建议除了 DaemonSet 之外不要容忍它。
最终拍板的是预算,不是参数
拒绝清单清空之后,drain 开始提交驱逐请求,此时说话算数的是 PodDisruptionBudget。当某个 PDB 的 disruptionsAllowed 为 0,驱逐请求会返回 429,drain 就带着退避一直重试,直到 --timeout 到期——而这个参数默认是 0,也就是无限。这个重试循环正是卡住的 drain 看起来像“挂起”而不是“失败”的原因:drain 只是在忠实执行你的指令,不报错、不推进,只反复打 evicting pod。
真正绕过预算的参数是 --disable-eviction,它跳过驱逐 API 直接删除 Pod。--force 不做这件事,它只负责那些没有控制器的 Pod——这跟大多数人的直觉正好相反。这两个参数完全不能互换,把它们当成同一类“强制开关”来用,是排空流程里最常见、代价也最高的误用。还有一点要注意:默认的不健康 Pod 驱逐策略 IfHealthyBudget 会让一个卡在 CrashLoopBackOff 的 Pod 把整台节点的排空拖住,直到它变成 Ready。
坏掉的样子
永远不返回的 drain。minAvailable 等于副本数,或者 maxUnavailable: 0,算出来的可中断数就是 0,驱逐请求被永远拒绝,终端就那么停在那里。同时排空多台节点也帮不上忙,它们共用同一个预算。用 kubectl get pdb -A 一条条核对定位:某个 PDB 选中的 Pod 正好落在你要排空的节点上,且 ALLOWED DISRUPTIONS 显示 0,它就是元凶;正确的修法是换一个允许一次驱逐的预算,而不是把超时调大。
把负载一起带走的 drain。面对卡住的排空,有人会去抓 --force 或 --disable-eviction,节点是清空了,被预算保护的那几个 Pod 也一起没了。如果那是一个需要法定多数的服务,这场“维护”就变成了事故。安全的顺序是:先修预算、再耐心等待,把 --force 留给你已经逐一确认过确实无状态的那个 Pod。
快速验证
确认节点已经封门,并看清 drain 会拒绝什么:
kubectl get node node-1 -o jsonpath='{.spec.unschedulable}{"\n"}'
kubectl drain node-1 --dry-run=client --ignore-daemonsets --delete-emptydir-data
kubectl get pods --field-selector spec.nodeName=node-1 -o custom-columns=NAME:.metadata.name,OWNER:.metadata.ownerReferences[0].kind
kubectl get pdb -A -o custom-columns=NAME:.metadata.name,MIN:.spec.minAvailable,MAX:.spec.maxUnavailable,ALLOWED:.status.disruptionsAllowed
第一条在节点被 cordon 之后会打印 true。第二条干跑会列出 drain 打算驱逐的 Pod,以及它按名字拒绝的 Pod。第三条里 OWNER 列为空的 Pod 必须靠 --force 才能删;第四条里,如果某个 PDB 的 Pod 就在这台节点上而 ALLOWED 是 0,那么这次 drain 会一直等下去,而不是报错退出。
来源
- 安全地排空节点,drain 流程及其与 PodDisruptionBudget 的交互,已核验 2026-09-16。
- kubectl drain 参考,精确的拒绝条件与参数说明,已核验 2026-09-16。
- Pod 中断,主动与被动中断的区别以及驱逐重试,已核验 2026-09-16。