kubectl drain 卡了 2 小时?找你的 PodDisruptionBudget

场景:节点下线维护,kubectl drain 执行后 10 分钟一个 Pod 都没驱逐——PDB 设了 minAvailable: 100%,allowedDisruptions=0,drain 卡死了 路径:坐标(drain 卡住) → 分层(drain→PDB→eviction 链路) → 路径(诊断命令+一眼判断) → 定位(minAvailable 陷阱) → 标点(修复+逃生口+check-list) 版本:K8s v1.28

上篇讲了 VPA 推荐的坑——Pod 重启过导致历史数据失真。这篇聊 PodDisruptionBudget(PDB——Pod 扰动预算,控制允许同时中断的 Pod 数量)——之前 DaemonSet 那篇提过它用来保护服务,但很多人不知道:PDB 设太保守,节点维护会完全卡死。

节点要下线维护,kubectl drain node-web-1 一执行,等了 10 分钟一个 Pod 都没驱逐——不是 Pod 卡住,不是网络问题,是 PodDisruptionBudget 设成了 100%,allowedDisruptions=0,一个 Pod 都不准走。

K8s 不是没有告诉你问题在哪——Events 已经说了,只是你没看。

【坐标】→ drain 卡住现场

drain 输出停在了 Evicting

kubectl drain 执行后,输出停在 Evicting pod 阶段不动,没有新进展:

drain 命令(PodDisruptionBudget 感知的节点维护工具,负责将节点上的 Pod 安全地驱逐到其他节点)默认行为是:对每个 Pod 发送 Eviction 请求,等 kube-apiserver 验证 PDB 通过后才真正终止 Pod。如果 PDB 拒绝,drain 就卡在 Evicting 状态——它不会跳过,会一直重试。

Events 已经说了原因

kubectl describe pod 看 Events 段,能看到 Eviction 被拒绝的记录——注意上图中 Events 部分的 FailedEviction

关键 Event:

Warning   FailedEviction   pod   resource limits: PDB "web-pdb" prevented eviction of pod

全局 Events:不是单个 Pod 的问题

只查一个 Pod 的 Events,你可能以为只是它运气不好。来全局看看:

5 个匹配同一 PDB 的 Pod 全部被拒,没有一个例外。注意截图中 kubectl get pdb -A 的输出——web-pdb 的 Allowed Disruptions: 0,而 monitoring 命名空间的 prom-pdb 正常(值为 1)。说明不是集群整体异常,就是 web-pdb 这一条 PDB 配死了

PDB(PodDisruptionBudget——Pod 扰动预算,控制允许同时中断的 Pod 数量)的检查是 Eviction 请求流程的一环:kube-apiserver 收到 Eviction 请求后,会查对应 PDB 的 status.allowedDisruptions(当前允许中断数),如果 ≤ 0,直接拒绝。

小结:不是 drain 坏了,是 PDB 不让走

drain 没坏,kubelet 没挂——是 PDB 的 allowedDisruptions 被算成了 0。往下走,看 PDB 是怎么算出这个 0 的。

【分层】→ drain → PDB → Eviction 链路

drain 执行 Eviction 的完整流程

drain 不是直接 kill Pod,它走了一条有安全校验的链路:

这个链路里,kube-apiserver 是 PDB 校验的执行者,不是 PDB controller。PDB controller(运行在 kube-controller-manager 中,负责持续计算 PDB 状态)会定期根据当前匹配的 Pod 数量和 minAvailable/maxUnavailable 计算 allowedDisruptions,结果写入 PDB 的 status 字段。apiserver 在校验 Eviction 时直接读这个 status,不会重新计算。

这里有件反直觉的事:PDB controller 不是实时同步的——默认每 5-10 秒才刷一次 status。这意味着 drain 卡住时,你改完 PDB 配置不会马上生效,要等 controller 下次 sync 才能算出新的 allowedDisruptions。同样,如果你同时 drain 两个节点,第一个成功了但第二个在第 3 秒发起,而 controller 在第 8 秒才重算——第二个 drain 拿到的是过期的 0,依然被拒。

allowedDisruptions 的计算公式

对于 minAvailable(最少可用 Pod 数):

allowedDisruptions = max(0, currentHealthyPods - minAvailable)

对于 replicas=3, minAvailable=3(即 100%):

currentHealthyPods=3 → allowedDisruptions = max(0, 3-3) = 0

这就是卡住的根因:所有 Pod 都在 Running,currentHealthyPods = minAvailable,没有余量给 drain 用。

小结:allowedDisruptions=0 时 drain 必然卡住

PDB 的公式决定了——minAvailable 等于当前健康 Pod 数时,allowedDisruptions=0,任何 Eviction 请求都会被拒绝。不是 bug,是设计。PDB 的职责是保护服务可用性,它不知道你正在做节点维护。

【路径】→ 核心诊断命令

一眼判断标准

遇到 drain 卡住,跑这三个命令,按输出判断:

症状 命令 关键字段
drain 卡在 Evicting kubectl describe pod <卡住的pod> Events 段搜 FailedEviction
PDB 拒绝 kubectl describe pdb <name> Allowed Disruptions: 0
Pod 分布 kubectl get pods -l <selector> Pod 数量和健康状态

只要 Allowed Disruptions: 0,就是 PDB 的锅,不用排查其他原因。

kubectl get pdb 看全局状态

重点关注 Allowed DisruptionsCurrent HealthyDesired Healthy 三个字段。

判断逻辑Allowed Disruptions: 0 + Current Healthy: 3 + Desired Healthy: 3 → minAvailable 等于 total replicas,没有给 drain 留空间。

kubectl get pods -l 看 Pod 分布

3 个 Pod 各跑在不同节点,全部健康。PDB 要求 minAvailable=3,drain 任何一个节点都会试图驱逐 1 个 Pod,但驱逐后只剩 2 个——低于 minAvailable,所以拒绝。

【定位】→ PDB 常见的配置陷阱

大多数人会怎么做:minAvailable 设越大越安全

spec:
  minAvailable: 3   或   minAvailable: 100%

"PDB 是保护服务的,设大点更安全"——这个直觉在滚动更新时是对的,在节点维护时是反的。

minAvailable=100%(等价于 replicas=3 时 minAvailable=3)意味着任何时候都不能有 Pod 少于 3 个。drain 需要驱逐 1 个 → 只剩 2 个 → 低于 3 → 拒绝。PDB 不知道你在做维护,它只知道"当前 3 个,要求 3 个,少一个都不行"。

正确的配置:maxUnavailable 替代 minAvailable

节点维护场景,直觉需求是"每次最多允许 N 个 Pod 不可用"——这正是 maxUnavailable 的语义:

❌ 错误配置:
  spec:
    minAvailable: 3            → allowedDisruptions: 0,drain 卡死

✅ 正确配置(等价语义,留出 drain 余量):
  spec:
    maxUnavailable: 1          → 允许 1 个 Pod 不可用,drain 可以驱逐 1 个

✅ 或者用 minAvailable 留余量:
  spec:
    minAvailable: 2            → allowedDisruptions: max(0, 3-2) = 1

maxUnavailable 在节点维护场景比 minAvailable 更直觉——它直接说"最多允许多少个 Pod 不在线",而不是"最少要留多少个"。

多副本跨 Node 的特殊情况

如果 Pod 分布在 3 个节点上,PDB 设 maxUnavailable: 1,drain 单个节点正常。但批量 drain 两个节点时,第一个 drain 成功后 allowedDisruptions=0,第二个会卡住。

PDB 不感知节点拓扑——它只看匹配的 Pod 总数,不看这些 Pod 分布在哪些节点上。你 drain 的节点数超过 maxUnavailable 时,必定有 drain 卡住。

PDB 不是为了锁死运维操作而存在的——它是在说"可以停,但一次少停点"。

【标点】→ 修复 + 逃生口 + Check-list

修复:改 PDB 配置

将 minAvailable 改为 maxUnavailable,给 drain 留出余量:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: web-pdb
spec:
  maxUnavailable: 1             ← 允许 1 个 Pod 不可用
  selector:
    matchLabels:
      app: web

修复前后对比:一行之差

改动只差一行——从 minAvailable: 3 换成 maxUnavailable: 1。看高亮行:左边第 6 行是 minAvailable: 3,右边对应位置是 maxUnavailable: 1

两个配置在 replicas=3 的场景下语义等价:minAvailable=3 要求至少 3 个 Pod 在线,maxUnavailable=1 要求最多 1 个不可用。但 maxUnavailable 给 drain 留出了操作空间——它允许有 1 个 Pod 不在线,drain 可以驱逐 1 个。

验证:修复后 drain 恢复

改完 PDB 后,重新 drain 同一个节点:

紧急逃生口:--disable-eviction

如果节点必须立刻下线,PDB 来不及改,可以用 --disable-eviction 绕过 PDB 检查:

# ⚠️ 跳过 PDB 检查直接删除 Pod,可能影响服务可用性
kubectl drain node-web-1 --disable-eviction --delete-emptydir-data --force
  • --disable-eviction:绕过 Eviction API,直接发 DELETE Pod 请求,PDB 不起作用
  • --delete-emptydir-data:如果 Pod 使用 emptyDir 卷,需要加这个 flag 允许 drain 删除包含 emptyDir 的 Pod(默认跳过,怕数据丢失)
  • --force:即使 Pod 不由 ReplicaSet/Deployment 管理,也强制删除

风险:这相当于绕过了 PDB 的保护——如果 maxUnavailable=1 但有 3 个 Pod 分布在当前节点上,直接 DELETE 可能导致 3 个 Pod 同时被删除。只在紧急场景(节点硬件故障、内核 panic)使用。

Check-list

步骤 命令 判断标准
1. drain 卡了先看 Pod kubectl describe pod <pod> Events 段有 FailedEviction → PDB 问题
2. 查 PDB 状态 kubectl describe pdb <name> Allowed Disruptions: 0 → 确诊
3. 算公式 currentHealthy - minAvailable ≤ 0 → PDB 太保守,没留余量
4. 修复配置 minAvailablemaxUnavailable: 1 drain 可驱逐 1 个 Pod
5. 紧急绕过 --disable-eviction 仅节点紧急下线时使用

附:完整命令清单

# 诊断
kubectl describe pod <pod> | grep -A 5 FailedEviction
kubectl describe pdb <name>
kubectl get pdb -A
kubectl get pods -l app=<label> -o wide

# 修复
kubectl edit pdb <name>    # minAvailable → maxUnavailable

# 紧急绕过
kubectl drain <node> --disable-eviction --delete-emptydir-data --force

# 验证 drain 是否恢复
kubectl drain <node> --ignore-daemonsets

下篇我们聊 K8s Service 访问不通——从 iptables 到 IPVS 逐层排查。你会发现 Service 不通 90% 的情况用三步就能定位。


📺 公众号「Ai拆代码的曹操」 | 🌟 知识星球「Ai拆代码的曹操」