你是否遇到过 kubectl drain 卡住半天不动,报错"Cannot evict pod as it would violate the pod's disruption budget"?或者更惨------集群升级失败,就因为某个 PDB 配置得太死,节点排空一直过不去?
我在这上面栽过不止一次。今天就聊聊 PodDisruptionBudget(PDB)这个看似简单、用起来却容易翻车的东西。
PDB 是什么?一句话说清楚
PDB 的全称是 Pod Disruption Budget,中文叫"Pod 中断预算"。它的核心功能就一个:在自愿中断(主动驱逐)操作期间,保证你的 Pod 不会一次性挂掉太多。
用最简单的话说------你要维护节点、升级集群、做滚动更新的时候,PDB 会拦住你,说"兄弟,不能再杀了,再杀 SLA 就崩了"。
关键区分:
- 自愿中断 :管理员主动排空节点(
kubectl drain)、升级集群、删除 Deployment 等(PDB 管) - 非自愿中断:节点宕机、硬件故障、网络分区(PDB 管不了)
PDB 是通过 API 驱逐(Eviction API)来实现保护的。驱逐动作发起时,K8s 会校验目标 Pod 所属的 PDB 约束,只有满足条件才允许执行。
PDB 在 K8s v1.4 以 Alpha 形态首次出现,v1.6 转为 Beta,v1.21 正式 GA(稳定版)。
前置条件
- 用的是 K8s v1.21 及以上(PDB 在
policy/v1中稳定可用) - 有 kubectl 访问集群的权限
- 集群里至少有一个 Deployment/StatefulSet,且副本数 ≥ 2(单副本配 PDB 是个坑,后面细说)
版本确认:
kubectl version --short
# 确保 Server Version 是 v1.21+
原理速览:两个参数,互斥使用
PDB 的核心在 spec 里的两个字段,且只能二选一:
|------------------|-----------------------|--------------|
| 字段 | 含义 | 示例 |
| minAvailable | 驱逐后至少还有多少个 Pod 可用 | 3 或者 50% |
| maxUnavailable | 驱逐后最多有多少个 Pod 不可用 | 1 或者 30% |
举例说明:一个 5 副本的 Deployment,minAvailable: 4 等价于 maxUnavailable: 1。但写法只能用其中一个。
3 个常用示例(可直接复制)
示例1:绝对数量模式
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: nginx-pdb
namespace: default
spec:
minAvailable: 2
selector:
matchLabels:
app: nginx
示例2:百分比模式(推荐)
百分比模式的优点是------副本数变了,PDB 自动跟着算,不用改配置。
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: api-pdb
namespace: production
spec:
maxUnavailable: 30%
selector:
matchLabels:
app: api-gateway
示例3:StatefulSet(etcd/ZooKeeper 场景)
有状态应用通常需要保证仲裁数量(quorum)。比如 5 节点的 etcd,至少需要 3 个健康成员才能正常工作:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: etcd-pdb
namespace: kube-system
spec:
minAvailable: 3
selector:
matchLabels:
app: etcd
重要提醒 :PDB 只通过 selector 匹配 Pod,不区分 Deployment 还是 StatefulSet。所以只要 selector 写对就行。
一图说清工作原理
(概念性理解就能上手,就不画图了)
当执行 kubectl drain 时,节点排空过程是串行驱逐 Pod 的。每驱逐一个 Pod,K8s API Server 会检查这个 Pod 对应的 PDB:
- 统计当前健康 Pod 数量(以 Ready 状态为标准)
- 如果驱逐完后还能满足 PDB 约束(
minAvailable达标或maxUnavailable未超),允许驱逐 - 否则拒绝驱逐,并返回
Cannot evict pod as it would violate the pod's disruption budget
kubectl drain 会持续重试,直到所有 Pod 都排空或超时。
kubectl drain 实操(带 PDB)
假设有一个 3 副本的 nginx,配了 maxUnavailable: 1 的 PDB。
# 先封锁节点(不让新 Pod 调度上来)
kubectl cordon node-1
# 排空节点(忽略 DaemonSet 管理的 Pod)
kubectl drain node-1 --ignore-daemonsets
# 预期行为:
# - 第一个 Pod 成功驱逐
# - 第二个 Pod 被 PDB 阻塞(replica 只剩 2 个健康 Pod,再驱逐 1 个会违反 maxUnavailable: 1)
# - drain 会卡住并报错
这也是很多人遇到的问题:drain 到一半卡死,因为 PDB 卡得太死,新 Pod 还没启动完成,旧 Pod 不让走。
怎么解?后面"验证方法"部分会详细说。
⚠️ 坑1:单副本应用配 PDB
这是最大也是最常见的坑。
你用 minAvailable: 1 保护一个单副本的 Deployment,看起来合理对吧?但实际情况是:排空节点时,只有一个 Pod 可用,驱逐它会让可用数变为 0,不满足 PDB 约束,drain 永远卡住。
错误场景:
- 副本数 = 1,PDB =
minAvailable: 1或maxUnavailable: 0 - 任何时候主动驱逐都会被 PDB 完全阻止
- 节点无法正常排空
最佳实践:副本数 < 2 时,不要配置 PDB,或者直接不用。云厂商比如 IBM 的文档也明确写了,PDB 只在副本数 ≥ 2 时才有意义。
⚠️ 坑2:健康检查未就绪导致 PDB 误判
PDB 计数时,只统计健康 的 Pod。健康的标准是 status.conditions 中 type="Ready" 且 status="True"。
如果 Pod 因为启动慢、配置错误、CrashLoopBackOff 等原因一直没 Ready 就绪:
- PDB 不会把它计入
currentHealthy - 集群健康的 Pod 数可能低于 PDB 的
desiredHealthy - 所有驱逐请求都会被拒绝,节点排空卡死
⚠️ 坑3:一个 Pod 被多个 PDB 覆盖
K8s 的驱逐子资源不支持一个 Pod 同时属于多个 PDB。如果出现了,API Server 会直接拒绝驱逐请求。
排查命令 :识别 Pod 的标签,再看看哪些 PDB 的 selector 匹配了这些标签。
# 查看 Pod 的标签
kubectl get pod <pod-name> -n <namespace> --show-labels
# 查看命名空间下所有 PDB
kubectl get pdb -n <namespace> -o yaml | grep -A5 selector
彩蛋:K8s 1.26+ 的不健康 Pod 驱逐策略(解决节点排空死锁)
这是我觉得近两年最有用的 PDB 增强,但知道的人还不多。
spec.unhealthyPodEvictionPolicy 有两个选项:
|-----------------------|--------------------------------------|-------------------|
| 策略 | 行为 | 适用场景 |
| IfHealthyBudget(默认) | 只有应用的健康 Pod 数满足 PDB 要求时,才允许驱逐不健康 Pod | 数据安全优先,容忍维护困难 |
| AlwaysAllow | 始终允许驱逐不健康的 Pod(无论 PDB 是否满足) | 集群运维优先,尤其适合节点排空场景 |
配置示例(K8s 1.26+,且需要先开启 PDBUnhealthyPodEvictionPolicy 特性门控):
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: always-evict-app
namespace: default
spec:
minAvailable: 2
unhealthyPodEvictionPolicy: AlwaysAllow
selector:
matchLabels:
app: myapp
我的建议 :生产环境默认用 AlwaysAllow。除非你的应用有严格的数据安全要求、不能容忍不健康 Pod 被意外驱逐------但这种情况说实话比较少见。AlwaysAllow 能让运维好过很多,节点排空不会因为一个 CrashLoopBackOff 的 Pod 卡半天。
这个功能在 K8s 1.26 中是 Alpha,现在已经稳定了。如果你还在用老版本,该考虑升级了。
验证方法:检查 PDB 状态和驱逐预算
# 查看当前 PDB 状态
kubectl get pdb -n <namespace>
# 输出示例:
# NAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS AGE
# nginx-pdb 2 N/A 0 10m
# api-pdb N/A 1 1 5m
# 查看详细信息
kubectl describe pdb <pdb-name> -n <namespace>
ALLOWED DISRUPTIONS 这个字段很关键:
- 大于 0:表示当前可以驱逐多少个 Pod(不违反 PDB)
- 等于 0:任何驱逐都会违反 PDB,所有驱逐请求都会被拒绝
故障排查:drain 卡住怎么办?
报错原文:
error when evicting pod (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget
三步排查法:
步骤1:找到是哪个 PDB 在拦你
kubectl describe pdb -A | grep -A10 "Allowed disruptions: 0"
找到 ALLOWED DISRUPTIONS = 0 的那个 PDB。
步骤2:看副本数 vs PDB 约束
可能的情况:
- 副本数 = minAvailable(驱逐 1 个就不满足)
- Pod 全都不健康(PG 健康数为 0,但 minAvailable > 0)
- 副本正在滚动更新(新旧并存,但 Ready 的 Pod 不够)
步骤3:选择解决方案
方案A(推荐) :等新 Pod 启动完成。如果是因为滚动更新导致的,新 Pod 拉镜像慢、启动慢,等一会儿就行。
方案B:临时删除 PDB,排完空再重建。
kubectl delete poddisruptionbudget <pdb-name> -n <namespace>
kubectl drain <node>
kubectl apply -f <pdb-file>
这个方法粗暴但有效。云厂商的文档里也推荐过这个应急方案。
方案C:临时扩容,创造驱逐空间(适用于有弹性容量的场景)。
# 从 3 副本扩到 4,驱逐时就有缓冲了
kubectl scale deployment <name> --replicas=4
方案D (K8s 1.26+):给 PDB 加上 unhealthyPodEvictionPolicy: AlwaysAllow,然后重试 drain。
不支持 & 限制说明
- PDB 不保护非自愿中断:节点直接挂了,PDB 不会帮你恢复 Pod。这时候要靠 ReplicaSet 控制器重新调度。
- 滚动更新有自己的逻辑 :Deployment 的 RollingUpdate 策略(
maxSurge/maxUnavailable)和 PDB 独立工作,但建议两者协调配置,避免互相冲突------比如 Deployment 的maxUnavailable: 50%,PDB 的maxUnavailable: 10%,那滚动更新时会被 PDB 卡住。 - 一个 Pod 不能有多个 PDB :这是 API 层面的限制。设计 PDB 时确保不同 PDB 之间的
selector不会重叠。
总结一下
- 副本数 ≥ 2 再考虑配 PDB,单副本配 PDB 等于自掘坟墓。
- 优先用百分比 (
maxUnavailable: 25%),副本数变了不用改配置。 - K8s 1.26+ 强烈建议配置
unhealthyPodEvictionPolicy: AlwaysAllow,能省很多运维麻烦。 - PDB 不能保证 100% 可用,它只管自愿中断。节点直接宕机它管不了。
- drain 卡住时先查
ALLOWED DISRUPTIONS,等于 0 就是 PDB 在拦你,想办法给它腾出空间。
你踩过 PDB 的什么坑?有没有更优雅的解法?评论区见。如果觉得有用,欢迎分享给同样在 K8s 运维上挣扎的朋友。