Kubernetes PDB 的保护范围与实现:驱逐、直接删除和滚动更新
应用有两个 Ready 副本,PDB 配置 minAvailable: 2。节点维护时,驱逐请求被拒绝;但直接执行 kubectl delete pod,Pod 又确实被删除了。Deployment 的滚动更新也能够完成。
三种操作经过不同的控制路径。PDB 的检查发生在 Eviction API 内,普通 Pod DELETE 不经过这项预算检查。理解这个入口差异,才能判断一次删除是否在 PDB 的保护范围内。
PDB 约束的是受保护的驱逐请求
PodDisruptionBudget 描述一组 Pod 在自愿中断时允许失去多少可用副本。它通过 selector 选中 Pod,使用 minAvailable 或 maxUnavailable 表达预算。
对于使用 Eviction API 的操作,API Server 会检查预算。不满足条件时,请求被拒绝,调用方需要等待或处理拒绝。PDB 不会阻止节点突然断电,也不会把普通 DELETE 自动改造成驱逐请求;工作负载的滚动更新策略也需要单独配置。官方 Disruptions 文档明确区分了这些情况。
预算由 disruption controller 计算并写入 PDB status。它统计选中 Pod 的健康状态,同时排除已经删除或已记为待中断的 Pod;在本例的整数 minAvailable 配置下,所需健康数是 2。两个 Pod 都 Ready 时,currentHealthy=2、desiredHealthy=2,所以 disruptionsAllowed=0。计算代码见 v1.35.1 的 disruption controller。
处理受预算约束的健康 Pod 时,Eviction API 检查预算状态是否有效、是否还有允许中断数;允许驱逐后,先减少余量,并把 Pod 记入 disruptedPods,再推进删除。这样,多个并发驱逐不能仅凭同一份旧健康数各自认为仍有余量。实现位于 Pod eviction storage。
这里的健康依据是 Kubernetes 的 Ready 状态。探针若没有反映应用能否服务,预算计算也无法替应用补上这项判断。
用同一组 Pod 比较三种操作
实验通过 SSH 在 zj 上运行,环境为 kind v0.33.0、Kubernetes v1.35.1。dd-pdb namespace 中只有一个两副本 Deployment 和一个 PDB:
| |
等两个 Pod 都 Ready 后再开始操作,避免把尚未启动完成误认为 PDB 的行为。
第一种:Eviction API 被拒绝
脚本向选中的 Pod 发送 policy/v1 Eviction 请求。真实错误中包含:
| |
此时少一个副本就无法满足 minAvailable: 2,所以拒绝符合配置。排查时可同时查看预算和 Pod:
| |
重点看 currentHealthy、desiredHealthy 和 disruptionsAllowed,同时核对 selector 是否选中了预期副本。选错对象时,即使业务实际有其他健康实例,也不会自动计入这份预算。
节点维护工具使用 Eviction API 时,也可能在这里等待。看到 drain 卡住,应该找出哪个 PDB 的预算不足,以及为什么没有余量。仅把超时时间加长,不会让一直为零的预算自行变成正数。
第二种:直接 DELETE 可以删除
保持 PDB 不变,脚本直接删除刚才的 Pod。删除完成后,Deployment 补建一个新 Pod;实测新旧 Pod 名称不同。
这一步验证的是操作路径的差异,不是建议绕过 PDB 完成维护。删除成功并不证明这次中断对应用安全。对于必须同时保留两个成员的系统,绕过预算可能恰好破坏当初设置 PDB 的目的。
更合适的处理可能是先增加能真正提供服务的副本、恢复未就绪成员,或在确认应用允许的前提下调整预算。只增加 spec.replicas、新增 Pod 却一直 Pending,也不会凭空增加健康副本。
第三种:Deployment 仍然可以滚动更新
实验修改 Pod template 的 annotation,触发 Deployment 创建新 ReplicaSet。PDB 保持不变,滚动更新仍然完成。
Deployment 按自己的更新策略推进替换,包括 maxSurge 和 maxUnavailable。PDB 不能代替这些字段。反过来,滚动更新期间的不可用副本可能影响可用数量,使同时发生的另一项驱逐操作更难获得预算。
本次实测可以归纳为:
| 操作 | minAvailable: 2、两个 Ready 副本时的结果 |
|---|---|
| Eviction API | 被预算拒绝 |
| 直接删除一个 Pod | 删除成功,控制器补建 |
| 修改 Deployment template | 滚动更新完成 |
为什么一个不健康副本可能让维护更难
实验还给应用增加 readinessProbe,然后让一个副本的探针失败。等 PDB 的 currentHealthy 变为 1、desiredHealthy 仍为 2 时,对这个未就绪 Pod 发送驱逐请求,默认策略仍然拒绝。
只把 PDB 的 unhealthyPodEvictionPolicy 改成 AlwaysAllow,再次驱逐同一个未就绪 Pod,请求成功,Deployment 补建的新 Pod 随后恢复 Ready。
因此,维护卡住时还要检查对不健康 Pod 的处理策略。AlwaysAllow 允许驱逐符合条件的不健康 Pod,不代表健康 Pod 的预算检查也被关闭。是否采用它,要看应用是否适合通过重新创建恢复,字段定义见不健康 Pod 驱逐策略。
两个维护请求怎样避免同时花掉同一份预算
假设 PDB 当前只允许中断一个健康副本,两台机器同时发起驱逐。如果只读取 disruptionsAllowed: 1 后各自删除一个 Pod,就会超出预算。v1.35.1 的 checkAndDecrement先检查 PDB 状态是否已经处理当前 generation,再减少 disruptionsAllowed,并把目标 Pod 记入 status.disruptedPods,最后通过 UpdateStatus 提交。
这次状态写入受 resourceVersion 乐观并发控制保护。并发请求不能把同一份旧状态都写成功;发生冲突的请求需要重新读取预算再判断。disruptedPods 则覆盖另一个时间差:预算已经扣减,但 Pod 的删除还没有被 PDB 控制器观察到。否则控制器可能仍把这个 Pod 算作可用,把刚扣掉的预算加回来。该记录也有后续清理机制,避免一次没有真正完成的删除永久占住预算。
这不是跨 PDB 与 Pod 的单次数据库事务,而是“先占预算,再推进删除”,由控制器协调中间状态。代码还会在相应路径给 Pod 删除附加 resourceVersion 前置条件,防止按旧的“不健康”状态放行后,目标已经转为健康却仍被无条件删除。
这些实现保护的是经过 Eviction API 的请求。本文并没有注入两个并发驱逐来测量竞争过程;并发控制结论来自上述版本源码,实测覆盖的是预算拒绝、直接 DELETE、滚动更新和不健康 Pod 策略。后两种删除路径不会因为 Eviction handler 内部做得更严格,就自动受到相同入口的约束。
实验代码与使用说明见 k8sdev/deep-dive-pdb。