
应用有两个 Ready 副本，PDB 配置 `minAvailable: 2`。节点维护时，驱逐请求被拒绝；但直接执行 `kubectl delete pod`，Pod 又确实被删除了。Deployment 的滚动更新也能够完成。

三种操作经过不同的控制路径。PDB 的检查发生在 Eviction API 内，普通 Pod DELETE 不经过这项预算检查。理解这个入口差异，才能判断一次删除是否在 PDB 的保护范围内。

<!--more-->

## PDB 约束的是受保护的驱逐请求

PodDisruptionBudget 描述一组 Pod 在自愿中断时允许失去多少可用副本。它通过 selector 选中 Pod，使用 `minAvailable` 或 `maxUnavailable` 表达预算。

对于使用 Eviction API 的操作，API Server 会检查预算。不满足条件时，请求被拒绝，调用方需要等待或处理拒绝。PDB 不会阻止节点突然断电，也不会把普通 DELETE 自动改造成驱逐请求；工作负载的滚动更新策略也需要单独配置。[官方 Disruptions 文档](https://kubernetes.io/docs/concepts/workloads/pods/disruptions/#pod-disruption-budgets)明确区分了这些情况。

预算由 disruption controller 计算并写入 PDB status。它统计选中 Pod 的健康状态，同时排除已经删除或已记为待中断的 Pod；在本例的整数 `minAvailable` 配置下，所需健康数是 2。两个 Pod 都 Ready 时，`currentHealthy=2`、`desiredHealthy=2`，所以 `disruptionsAllowed=0`。计算代码见 [v1.35.1 的 disruption controller](https://github.com/kubernetes/kubernetes/blob/v1.35.1/pkg/controller/disruption/disruption.go)。

处理受预算约束的健康 Pod 时，Eviction API 检查预算状态是否有效、是否还有允许中断数；允许驱逐后，先减少余量，并把 Pod 记入 `disruptedPods`，再推进删除。这样，多个并发驱逐不能仅凭同一份旧健康数各自认为仍有余量。实现位于 [Pod eviction storage](https://github.com/kubernetes/kubernetes/blob/v1.35.1/pkg/registry/core/pod/storage/eviction.go)。

这里的健康依据是 Kubernetes 的 Ready 状态。探针若没有反映应用能否服务，预算计算也无法替应用补上这项判断。

## 用同一组 Pod 比较三种操作

实验通过 SSH 在 `zj` 上运行，环境为 kind v0.33.0、Kubernetes v1.35.1。`dd-pdb` namespace 中只有一个两副本 Deployment 和一个 PDB：

```yaml
# 两个副本都必须可用；当前没有主动驱逐的余量。
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: keep-two
spec:
  minAvailable: 2 # 这里使用整数，直接表示至少两个可用副本。
  selector:
    matchLabels:
      app: app # 只匹配本实验 Deployment 的 Pod。
```

等两个 Pod 都 Ready 后再开始操作，避免把尚未启动完成误认为 PDB 的行为。

## 第一种：Eviction API 被拒绝

脚本向选中的 Pod 发送 `policy/v1` Eviction 请求。真实错误中包含：

```text
Cannot evict pod as it would violate the pod's disruption budget.
```

此时少一个副本就无法满足 `minAvailable: 2`，所以拒绝符合配置。排查时可同时查看预算和 Pod：

```sh
kubectl --context kind-deep-dive -n dd-pdb get pdb keep-two -o yaml
kubectl --context kind-deep-dive -n dd-pdb get pods -l app=app
```

重点看 `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 驱逐策略](https://kubernetes.io/docs/tasks/run-application/configure-pdb/#unhealthy-pod-eviction-policy)。

## 两个维护请求怎样避免同时花掉同一份预算

假设 PDB 当前只允许中断一个健康副本，两台机器同时发起驱逐。如果只读取 `disruptionsAllowed: 1` 后各自删除一个 Pod，就会超出预算。v1.35.1 的 [`checkAndDecrement`](https://github.com/kubernetes/kubernetes/blob/v1.35.1/pkg/registry/core/pod/storage/eviction.go#L423)先检查 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](https://github.com/jimyag/k8sdev/tree/8f9bfbbd44ac061702310f332ee949fcdf0a213d/deep-dive-pdb)。

