适用场景

集群管理员准备升级内核、更换节点或缩容节点池,执行 kubectl drain 后命令长时间重试,并反复出现 Cannot evict pod as it would violate the pod's disruption budget。业务当前未必已经中断,但维护窗口正在被耗尽。

本文适用于由 Deployment 或 StatefulSet 管理的多副本工作负载,重点处理 PodDisruptionBudget(PDB)与健康副本不足共同造成的驱逐阻塞。PDB 只约束通过 Eviction API 发起的自愿中断,不能阻止节点故障等非自愿中断,也不能替代副本分散、就绪探针和容量规划。

现象描述

典型操作如下:

kubectl cordon worker-03
kubectl drain worker-03 --ignore-daemonsets --delete-emptydir-data --timeout=20m

cordon 成功后,节点会变为不可调度;drain 则持续尝试驱逐普通 Pod。若某个应用的 PDB 当前不允许更多中断,API Server 会暂时拒绝驱逐,kubectl drain 会继续重试,直到预算恢复或命令超时。

先不要把“节点已不可调度”误判成“排空已完成”。检查节点上还剩哪些 Pod:

kubectl get pods -A --field-selector spec.nodeName=worker-03 \
  -o custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name,OWNER:.metadata.ownerReferences[0].kind,READY:.status.containerStatuses[*].ready,PHASE:.status.phase'

DaemonSet Pod 留在节点上通常符合预期;真正需要关注的是受工作负载控制器管理、但一直无法驱逐的 Pod。

PDB 为什么会阻止驱逐

PDB 用标签选择器圈定一组 Pod,并用以下字段之一表达可接受的自愿中断范围:

  • minAvailable:驱逐后至少还要保留多少个健康 Pod。
  • maxUnavailable:驱逐后最多允许多少个 Pod 不可用。

两者互斥。PDB 把 Ready=True 的 Pod 计为健康 Pod,并在状态中给出当前是否还有预算。以 3 副本、minAvailable: 2 为例,三个副本全健康时通常允许驱逐 1 个;若已经有一个副本未就绪,继续驱逐会让健康副本低于 2,预算就会变成 0。

Kubernetes 官方文档说明,kubectl drain 通过 Eviction API 驱逐 Pod,因此会尊重 PDB;直接删除 Pod 或工作负载并不都受 PDB 保护。不要用绕过驱逐检查的方式处理普通维护,否则只是把“维护卡住”变成“可用性下降”。相关语义可参考 Kubernetes Disruptions 文档PDB API 参考

排查思路

1. 找出预算为零的 PDB

kubectl get pdb -A \
  -o custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name,MIN:.spec.minAvailable,MAX:.spec.maxUnavailable,HEALTHY:.status.currentHealthy,DESIRED:.status.desiredHealthy,ALLOWED:.status.disruptionsAllowed,EXPECTED:.status.expectedPods'

重点看以下字段:

  • HEALTHY:当前 Ready=True 的副本数。
  • DESIRED:PDB 要求的最小健康副本数。
  • ALLOWED:当前还能接受的自愿驱逐数;为 0 时继续 drain 会被拒绝。
  • EXPECTED:PDB 控制器推导出的预期副本数。

再查看命中的标签和控制器观察状态:

kubectl describe pdb checkout-api -n production
kubectl get pdb checkout-api -n production -o yaml

.status.observedGeneration 小于 .metadata.generation,说明状态还没有反映最新配置,应先等待控制器同步,不能依据旧的 disruptionsAllowed 冒险操作。

2. 确认 PDB 选中了正确的 Pod

kubectl get pdb checkout-api -n production \
  -o jsonpath='{.spec.selector.matchLabels}{"\n"}'
kubectl get pods -n production -l app=checkout-api \
  -o custom-columns='NAME:.metadata.name,NODE:.spec.nodeName,READY:.status.conditions[?(@.type=="Ready")].status,PHASE:.status.phase'

PDB 的选择器应与目标工作负载标签准确对应。policy/v1 中,缺失的 selector 不选择 Pod,而显式空对象 selector: {} 会选择命名空间内全部 Pod;生成模板时尤其要防止把空选择器误发布到生产环境。

3. 定位为什么健康副本不足

kubectl get deployment checkout-api -n production -o wide
kubectl describe deployment checkout-api -n production
kubectl get pods -n production -l app=checkout-api -o wide
kubectl describe pod checkout-api-7f6d8b9c4d-abcde -n production
kubectl get events -n production --sort-by=.lastTimestamp

常见根因包括:

  • 副本数本来就是 1,却配置了 minAvailable: 1,等价于不允许任何自愿驱逐。
  • 某个副本因镜像拉取、启动异常、探针失败或依赖故障而未就绪。
  • 替代 Pod 因资源不足、亲和性、污点或存储绑定约束无法调度。
  • 多个维护任务并行 drain,或者滚动发布已经占用了不可用预算。
  • 所有副本集中在同一节点或同一故障域,排空时没有合适的落点。

若替代 Pod 是 Pending,继续修改 PDB 通常不是根治;应先处理调度容量或约束。若 Pod 在 CrashLoopBackOff,应查看脱敏后的事件、容器退出码和应用日志,而不是只盯着 drain 输出。

4. 区分健康 Pod 与异常 Pod 的驱逐策略

未设置 unhealthyPodEvictionPolicy 时,默认行为对应 IfHealthyBudget:当应用已经低于健康预算时,处于 Running 但未就绪的 Pod 也可能阻塞排空。对于希望节点维护能移走异常 Pod 的工作负载,可以评估 AlwaysAllow

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: checkout-api
  namespace: production
spec:
  minAvailable: 2
  unhealthyPodEvictionPolicy: AlwaysAllow
  selector:
    matchLabels:
      app: checkout-api

AlwaysAllow 只放宽对 Running 但未健康 Pod 的驱逐,健康 Pod 仍受预算约束。它可能让一个尚有机会恢复的异常副本更快被替换,因此需要结合应用启动时间、故障恢复方式和维护目标评估。该字段的当前行为及版本说明见 配置 PDB 官方任务文档

修复方案

方案一:先恢复健康副本,再继续排空

这是风险最低的默认方案。修复镜像、配置、探针或资源问题,确认健康副本和预算恢复:

kubectl rollout status deployment/checkout-api -n production --timeout=10m
kubectl get pdb checkout-api -n production -w

disruptionsAllowed 大于 0 后,在原维护终端重新执行带明确超时的 drain。一次只维护一个故障域内的节点,避免多个排空任务争抢同一份预算。

方案二:临时扩容并确认新副本可调度

如果应用支持水平扩展且集群有余量,可先增加副本:

kubectl scale deployment checkout-api -n production --replicas=4
kubectl rollout status deployment/checkout-api -n production --timeout=10m
kubectl get pods -n production -l app=checkout-api -o wide
kubectl get pdb checkout-api -n production

只有新副本真正变为 Ready=True,预算才可能增加。维护完成后应按容量基线缩回,并确认没有把临时扩容永久留在配置漂移中;使用 GitOps 时应在声明式配置中完成变更。

方案三:修正不合理的预算

对无状态 3 副本服务,下面配置允许一次自愿中断,同时保留两个健康副本:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: checkout-api
  namespace: production
spec:
  maxUnavailable: 1
  unhealthyPodEvictionPolicy: AlwaysAllow
  selector:
    matchLabels:
      app: checkout-api

选择 minAvailable 还是 maxUnavailable,应由服务容量和一致性要求决定。法定人数系统不能照抄无状态 Web 服务的预算;单副本服务也无法同时实现“维护时零中断”和“允许驱逐”,应先增加冗余或接受明确维护窗口。

修改后先检查差异,再应用并观察状态:

kubectl diff -f checkout-api-pdb.yaml
kubectl apply -f checkout-api-pdb.yaml
kubectl get pdb checkout-api -n production -w

不要在没有业务负责人确认的情况下把 minAvailable 改为 0、删除 PDB,或使用 kubectl drain --disable-eviction 绕过保护。该选项会跳过 PDB 检查,只应在已明确接受中断风险的应急流程中使用,并保留审批和回滚记录。

验证与收尾

排空完成后确认节点上只剩预期 Pod,并检查业务副本分布与服务指标:

kubectl get pods -A --field-selector spec.nodeName=worker-03
kubectl get pods -n production -l app=checkout-api -o wide
kubectl get pdb checkout-api -n production
kubectl get node worker-03

维护结束并确认节点健康后恢复调度:

kubectl uncordon worker-03

验收不应只看 drain 的退出码,还要确认应用可用副本、错误率、延迟、重启次数和请求成功率回到基线。如果临时扩容或调整过 PDB,应核对声明式配置和集群实际状态一致。

预防措施

  • 在预发布环境定期演练 cordondrain,把“预算为零”和“替代 Pod 无法调度”纳入发布门禁。
  • kube_poddisruptionbudget_status_pod_disruptions_allowed == 0 建立有上下文的告警,但结合维护事件和健康副本判断,避免长期噪声。
  • 用 topology spread constraints 或反亲和性分散副本,避免全部副本落在同一节点或可用区。
  • 节点升级、集群缩容和工作负载滚动发布错峰执行,防止同时消耗预算。
  • 为单副本或法定人数服务维护专门的操作手册,明确可用性目标、数据一致性边界和回滚条件。
  • 审查 PDB 选择器,确保没有漏选目标 Pod,也没有因空选择器误选整个命名空间。

总结

PDB 阻止 drain 通常不是 Kubernetes “卡死”,而是在提示当前健康副本或调度容量不足以安全承受下一次自愿中断。正确处理顺序是:确认剩余 Pod,读取 PDB 的健康数与允许中断数,定位未就绪或不可调度的根因,优先恢复或扩容,再按业务可用性目标修正预算。只有把 PDB、探针、调度容量和副本分布一起治理,节点维护才能既按时完成,也不靠绕过保护碰运气。