K8s PodDisruptionBudget 实战

一、什么是 PDB

Pod 中断分两种:
- 非自愿中断(Involuntary):节点宕机、内核 panic、OOMKill,无法提前预知
- 自愿中断(Voluntary)kubectl drain、滚动更新、集群升级,人为触发

PDB 只对自愿中断生效,告诉 K8s:"在任何主动操作期间,我的这组 Pod 最多/最少允许多少个同时不可用。"

没有 PDB,kubectl drain 会不管不顾地驱逐所有 Pod;有了 PDB,驱逐操作会被迫等待,直到副本数量满足可用性下限。

二、两种配置方式(二选一)

minAvailable——最少保持可用

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: payment-service-pdb
  namespace: production
spec:
  minAvailable: 2        # 始终保持至少2个Pod可用
  selector:
    matchLabels:
      app: payment-service

maxUnavailable——最多允许不可用

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: order-service-pdb
  namespace: production
spec:
  maxUnavailable: 1      # 最多允许1个Pod同时不可用
  selector:
    matchLabels:
      app: order-service

两者都支持百分比(副本数动态变化时更实用):

spec:
  minAvailable: "80%"    # 始终保持80%的副本可用
  # 或
  maxUnavailable: "20%"  # 最多允许20%不可用

注意:百分比会向下取整。3 副本 + minAvailable: "80%" = 2.4 → 取整为 2。

三、PDB 与 Drain / 滚动更新的配合

场景一:kubectl drain 节点维护

drain 过程中,K8s 的 eviction API 会检查目标 Pod 对应的 PDB:

检查 PDB → 驱逐 pod-A 是否会违反 minAvailable?
  当前可用: 3/3
  驱逐后剩余: 2/3
  minAvailable: 2 → 满足 → 允许驱逐

等待 pod-A 在其他节点就绪后,再尝试驱逐 pod-B...

如果 2 个节点同时被 drain,而服务只有 2 副本且 minAvailable: 2,第二个节点的驱逐会被无限阻塞

场景二:Deployment 滚动更新

滚动更新本身有 maxUnavailablemaxSurge 控制,PDB 是额外的保护层。两者同时存在时,PDB 的约束更严格——每一步都必须同时满足 Deployment 策略和 PDB 约束。

四、生产配置模式

模式一:高可用服务(3副本+)

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: api-gateway-pdb
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: api-gateway
      tier: frontend

模式二:数据库 StatefulSet(严格保护)

spec:
  maxUnavailable: 1
  selector:
    matchLabels:
      app: mysql
      role: replica  # 只保护 replica,primary 单独配置

模式三:批处理 Job(允许较多中断)

spec:
  maxUnavailable: "30%"
  selector:
    matchLabels:
      app: batch-worker

五、四大配置陷阱

陷阱一:单副本服务配 minAvailable: 1

# ❌ 会导致节点永远无法被 drain!
spec:
  replicas: 1
---
spec:
  minAvailable: 1  # 只有1个副本,drain时永远无法满足

drain 会永远卡住,直到手动 --force 或修改 PDB。单副本服务要么接受中断,要么升级到 2 副本。

陷阱二:minAvailable 等于副本总数

# ❌ 等同于"不允许任何中断"
spec:
  replicas: 3
---
spec:
  minAvailable: 3  # 节点维护永远无法进行

正确做法:minAvailable: replicas - 1 或用百分比留出余量。

陷阱三:忘记配 selector

# ❌ selector 为空会匹配所有 Pod
spec:
  minAvailable: 2
  selector: {}  # 误保护命名空间内所有 Pod

陷阱四:PDB 和 HPA 配合时的计算失误

HPA 动态调整副本数,PDB 百分比需考虑最小副本场景:

# HPA: minReplicas: 2, maxReplicas: 10
# PDB: maxUnavailable: "50%"
# 当副本是2时,50%=1,可能对高可用服务太激进

# 更安全的做法:用绝对数
spec:
  minAvailable: 1  # 无论 HPA 如何扩缩,至少保证1个可用

六、验证 PDB 是否生效

# 查看 PDB 状态
kubectl get pdb -n production
# NAME                   MIN AVAILABLE   MAX UNAVAILABLE   ALLOWED DISRUPTIONS   AGE
# payment-service-pdb    2               N/A               1                     3d

# 检查 drain 被阻塞的原因
kubectl get events -n production --field-selector reason=DisruptionTarget

# 调用 eviction API 测试(被 PDB 阻止会返回 429 Too Many Requests)

ALLOWED DISRUPTIONS 是当前允许中断的 Pod 数量,取决于当前可用副本数和 PDB 配置。

七、完整生产示例(支付服务)

# Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: payment-service
  namespace: production
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 0      # 先扩再缩,零停机
      maxSurge: 1
  template:
    spec:
      terminationGracePeriodSeconds: 60
      containers:
      - name: payment
        image: payment-service:v2.3.1
        readinessProbe:
          httpGet:
            path: /health/ready
            port: 8080
          initialDelaySeconds: 10
          periodSeconds: 5
---
# PDB
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: payment-service-pdb
  namespace: production
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: payment-service

maxUnavailable: 0 + minAvailable: 2 组合效果:
- 滚动更新时先启动新 Pod(surge),确认就绪后再删除旧 Pod
- 节点维护时最多只允许驱逐 1 个 Pod