AI时代的Kubernetes教程36招-第19招-Priority、Preemption、Eviction和PDB怎么处理资源竞争与节点故障?

PriorityClass、Eviction 与 PDB:维护窗口里谁先被牺牲

中断分自愿与被动,混着谈必翻车

[读者] 在同一个 K8s 集群里混跑网关、GPU 推理与批任务的平台与运维同学
[痛点] drain 卡住、Pod 被 kubelet 悄悄杀掉、被抢占后再也排不回来
[现在读] 内核与 GPU 驱动升级窗口已经排进本周变更计划
[读完] 能给三类负载分别划出可配置、可预检、可验证的中断边界
[旧方案] 三类负载同优先级,靠 kubectl delete pod 直接删做维护
    |
    v
[新需求] 维护窗口压缩,且在线推理不允许掉可用容量
    |
    v
[冲突] 删除、抢占、压力驱逐各走各的路径,没有统一的预算闸门
    |
    v
[后果] 一次驱动升级把推理副本打到 Pending,网关被顺手带走

我是老李,在深圳带一支做 AI 推理平台的工程团队,集群里混了三类负载:入口网关(Deployment,长连接,掉了就是用户可感知的 5xx)、GPU 推理服务(Deployment,绑卡,冷启动以十秒计)、离线批任务(Job,白天积压夜里开跑,随时可以重来)。

过去的维护流程很朴素:发个变更通知,kubectl delete pod 或者 kubectl drain 轮着走一遍。真出事的那次是 GPU 驱动升级,前几个节点还算顺,轮到某个节点时命令直接卡在那里不动,等超时;同一时间网关的可用副本从 3 掉到 1,而两个 GPU 推理 Pod 停在 Pending 十几分钟起不来。

那之后我才认真把「Pod 为什么会消失」这件事拆开看。它根本不是一套机制,而是三条互不打招呼的路径:调度期的抢占、kubelet 的节点压力驱逐、以及通过 Eviction API 触发的自愿中断(drain 走的是这一条)。三条路径的约束条件、能拿到的证据、可调参数完全不同,混在一起调参只会越调越乱。

为什么我给 GPU 推理配了高优先级,它在节点内存压力下还是第一批被赶走的?

为什么 PDB 明明拦住了 drain,却拦不住调度器的抢占,这个预算到底保护了什么?

为什么同一条 drain 命令,网关、GPU 推理和批任务的表现能差得这么远?

01、故事

这套集群的节点池是混合的:网关和推理共用一批带卡的机器,批任务被允许在夜间塞进同一批节点的剩余资源里。这个安排本身没问题,问题是三类负载对「被打断」的耐受度完全不是一个量级。网关被重启一次,代价是几百条长连接重建;推理 Pod 被重启一次,代价是几十秒的模型加载和一段时间的容量缺口;批任务被干掉一次,代价是那一批分片重跑。

任务是:让例行维护(内核补丁、驱动升级、节点下水)在不牺牲在线容量的前提下做得完、做得可验证。

最初我给的方案是「优先级分层 + PDB 兜底」:给推理和网关配一个高优先级 PriorityClass,给批任务配低优先级,再给在线负载各挂一个 PodDisruptionBudget。这套配置上手很快,也很快给了我一种虚假的安全感——因为 drain 第一次跑就是失败的。

失败的样子很具体:kubectl drain 报出 cannot evict pod as it would violate the pod's disruption budget,然后一直重试到超时。我当时的第一反应是「副本不够,扩一个」,扩完副本发现还是不行。第二反应是「PDB 配错了」,把 minAvailable 从 2 改成 1,drain 通了——但那次维护结束后,网关在流量高峰少了一个副本,服务等级直接掉档。

真正的冲突在这里露出来了:我把 PDB 当成了「保证最少几个副本活着」的护盾,可它实际是一个「允许同时停几个」的闸门。护盾是静态的,闸门是动态的;护盾失效时你会掉容量,闸门卡死时你连维护都做不了。而真正决定「谁先被牺牲」的,根本不是 PDB,是优先级和 resources 画像一起算出来的排序。

维护窗口到
节点要腾空
谁都说别动
谁都没做错

02、问题

旧方案失效的地方不在「配置写错了」,而在「把三种中断当成了同一种中断」。

业务影响分两层:一是维护做不完,变更窗口被挤压到第二天,风险叠加;二是做完了也不安全——被 kubelet 压力驱逐带走的那次,推理容量出现了肉眼可见的缺口,而维护工单上写着「已成功下水」。

技术表现可以逐条对照:

  • kubectl drain 在某个节点上反复被拒,报错指向 disruption budget,kubectl get pdb 的 ALLOWED DISRUPTIONS 为 0。
  • 被维护节点上的 Pod 状态是 NotReady,Readiness 探针一直失败,但它依然占着 PDB 的健康预算。
  • 另一批节点出现 MemoryPressure 状态和对应的 taint,GPU 推理 Pod 被标记为 Evicted,reason 是节点资源不足,而不是被谁抢占。
  • 批任务的 Pod 反复进入 Pending,Event 里能看到 Preempted,但被抢占的推理 Pod 优雅终止期很长,调度器并没有马上成功。

完成标准我定成四条可验证的:一是任意时刻 kubectl get pdb -A 的 ALLOWED DISRUPTIONS 语义可解释;二是 draining 一个节点时,网关和推理的实际可用副本数不低于配置下限;三是压力驱逐时被选中的第一批 Pod 是我们事先预期的那些;四是整轮维护可以用一条命令推进,不需要人工干预中间状态。

旧法只删 Pod
中断无人管
副本一多走
服务就断层

03、原理

先分清三条路径。

抢占(Preemption)发生在调度期:新 Pod 调度不上去时,kube-scheduler 尝试在某个节点上选一批低优先级的受害者,把它们标记为待终止,并把 nominatedNodeName 写到抢占者身上。抢占只约束调度器自己的行为,它不走 Eviction API,所以 PDB 在这一步完全无效。被抢占的 Pod 仍然走自己的优雅终止流程,抢占也不是一次「资源兑换」,只是一次带启发式的下注。

节点压力驱逐(Node-pressure eviction)发生在节点本地:kubelet 持续监控 memory.available、nodefs/imagefs、pid 等信号,软阈值配 evictionSoftGracePeriod、硬阈值配 evictionHard,触发后由 kubelet 自己挑 Pod 杀掉。这一步同样不走 API 准入,PDB 无效,并且它会顺手给节点打上 node.kubernetes.io/memory-pressure 这类 taint,阻止新 Pod 调度进来。

主动驱逐(API-initiated eviction)走 Eviction API 子资源,kubectl drain 用的就是它。这是唯一一条会去校验 PodDisruptionBudget 的路径:预算不够,请求直接被拒。PDB 的定义就是「自愿中断」的预算,自愿中断包括 drain、节点池缩容、以及各种控制器主动删 Pod;节点崩溃、抢占、压力驱逐属于非自愿中断,PDB 管不着。

反直觉判断:kubelet 在压力驱逐时,第一排序键不是优先级,而是「用量是否超出 requests」。官方文档描述的排序规则是先看 Pod 的实际用量有没有超过它的 requests,再看 Pod 优先级,最后看超出 requests 的幅度。所以一个 requests 写得很小的「高优先级」Pod,会排在一个 requests 写得实在的「低优先级」Pod 前面被处理掉。优先级是第二顺位,不是免死金牌。

同样反直觉的还有一条:PDB 拦得住调度器之外的人,拦不住调度器自己。而调度器在挑选受害者节点时,会尽量优先选择那些「杀掉受害者不会破坏 PDB」的节点——这是启发式偏好,不是保证。所以 PDB 在抢占里既不是无效,也不是有效,它是一个软影响项。

中断分两种
自愿与被动
优先级排序
只在第二位

04、架构

[输入] 维护指令 / 节点资源水位信号 / 新 Pod 的调度请求
    |
    v
[模块] kube-scheduler 抢占逻辑 / kubelet 驱逐器 / Eviction API 与 PDB 控制器
    |
    v
[数据/状态] pod.spec.priority、PDB 的 disruptionsAllowed、节点 Pressure 状态与 taint
    |
    v
[处理] 挑选受害者 → 优雅终止 → 压力排序淘汰 → Eviction 准入校验
    |
    v
[输出] Pod 被终止或被拒绝、nominatedNodeName、Evicted 状态、drain 成功或超时

这个结构里,三块模块的边界要画清楚。调度器的边界是「节点选择」——它只关心新 Pod 能不能放下,牺牲谁只是手段。kubelet 的边界是「本节点存活」——它不关心业务容量,只关心别把自己压死。Eviction API 的边界是「准入」——它不产生中断,只决定要不要放行一次中断。

收益是:所有中断都能被归到其中一条路径上,于是每类问题都有独立的可观测点和可调参数——抢占看 Event 和 nominatedNodeName,压力驱逐看节点 Condition 与 Pod 的 Evicted reason,主动驱逐看 PDB 的 ALLOWED DISRUPTIONS。

代价是:三套参数不共享语义。优先级配错了不会影响 PDB,PDB 配错了也不会影响 kubelet,于是「我明明保护了」和「它还是被杀了」可以同时成立。

适用条件是混合部署、且中断可被容忍但必须可预期。如果负载连一次非自愿中断都承受不了,那正确的做法是物理隔离节点池,而不是继续加参数。

分层定边界
优先定次序
预算定闸门
压力定水位

05、实战一次

环境与依赖:Kubernetes v1.29 及以上(policy/v1 的 PodDisruptionBudget 自 1.21 起为稳定版,policy/v1beta1 已在 1.25 移除);kubectl 与集群小版本差不超过一个;GPU 节点已安装设备插件,nvidia.com/gpu 资源可见。

未在当前环境实测,以下为预期结果,输出均标注为示例输出。

项目目录放在 deploy/base/resilience/:

deploy/base/resilience/
├── kustomization.yaml
├── priorityclass.yaml
├── gateway-pdb.yaml
├── gpu-pdb.yaml
└── batch-job.yaml

优先级分层,注意 preemptionPolicy 这个字段是两个方向的事:它描述的是「这个优先级的 Pod 会不会去抢占别人」,而不是「它会不会被别人抢占」。

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: inference-critical
value: 1000000
globalDefault: false
preemptionPolicy: PreemptLowerPriority
description: 在线网关与 GPU 推理,维护窗口内优先保住
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: batch-preemptible
value: 1000
globalDefault: true
preemptionPolicy: Never
description: 离线批任务,可被抢占,不主动抢占他人

在线负载各挂一个 PDB,第一版用 minAvailable:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: gateway-pdb
  namespace: ai
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: gateway
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: gpu-pdb
  namespace: ai
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: gpu-inference

批任务故意不配 PDB:它要的是「便宜地被打断」,不是「被保护」。

apiVersion: batch/v1
kind: Job
metadata:
  name: nightly-embedding
  namespace: ai
spec:
  backoffLimit: 6
  activeDeadlineSeconds: 7200
  template:
    spec:
      priorityClassName: batch-preemptible
      restartPolicy: OnFailure
      containers:
        - name: worker
          image: registry.local/ai/batch-worker:1.4.0
          resources:
            requests:
              cpu: 500m
              memory: 512Mi

应用并验证:

kubectl apply -k deploy/base/resilience/
kubectl -n ai get pdb

示例输出:

NAME          MIN AVAILABLE   MAX UNAVAILABLE   ALLOWED DISRUPTIONS   AGE
gateway-pdb   2               N/A               1                     40s
gpu-pdb       2               N/A               1                     40s

三个副本全健康时,预算刚好剩 1,看起来够用。V1 到这里就算跑通了,但它留了一个必须炸的坑:这个 1 是「当前健康副本数」算出来的,不是「配置副本数」算出来的。等某个副本的 Readiness 掉下去,这个 1 会变成 0,而那个不健康的副本大概率正好就在你要下水的节点上——这是 06 要排的第一个问题。

先跑通最小
再留一道口
无伤不成例
有证才敢改

06、排查

现象一:drain 被 PDB 拒绝,重试到超时

怀疑: 副本数不够,或者 minAvailable 写大了。

检查:

kubectl -n ai get pdb gateway-pdb -o custom-columns=NAME:.metadata.name,MIN:.spec.minAvailable,HEALTHY:.status.currentHealthy,DESIRED:.status.desiredHealthy,ALLOWED:.status.disruptionsAllowed
kubectl -n ai get pods -l app=gateway -o wide

证据: HEALTHY 是 2,DESIRED 是 3,ALLOWED 是 0。三个副本里有一个长期 NotReady,Readiness 探针一直在失败。

根因: minAvailable 保护的是健康副本数,不是副本总数。一个 NotReady 的副本会同时做两件坏事:它吃掉一份健康预算,又占着不干活。而当它恰好落在待维护节点上时,你连「把它驱逐掉重新调度」这一步都做不到,因为预算算出来是 0。这是一个死锁:节点有问题导致副本不健康,节点有问题所以你想 drain,而副本不健康导致你不能 drain。

修复: 见 07,用 unhealthyPodEvictionPolicy 把不健康 Pod 从预算计算里解放出来。

现象二:GPU 推理 Pod 被压力驱逐,优先级没救它

怀疑: 高优先级不是应该最后才被杀吗?

检查:

kubectl -n ai describe pod gpu-inference-xxxxx
kubectl get node gpu-node-07 -o jsonpath='{.status.conditions[?(@.type=="MemoryPressure")].status}'
kubectl get node gpu-node-07 -o jsonpath='{.spec.taints}'

证据: Pod 状态是 Evicted,事件里的信息是节点资源不足(内存),节点 Condition 显示 MemoryPressure 为 True,并且带上了 node.kubernetes.io/memory-pressure 的 taint。再看 resources 画像:该 Pod 是 Burstable,memory requests 明显低于它在推理期真实占用的水位。

根因: kubelet 的排序规则里,「用量超出 requests」排在优先级之前。优先级再高,只要它超出自己声明的 requests,就会排在那些没超出 requests 的低优先级 Pod 前面被淘汰。requests 不是申请书,是排序输入。

修复: 把 requests 调到贴近真实水位,并让 limits 与 requests 对齐,用 Guaranteed 画像退出第一梯队。

错误尝试: 我给 GPU 推理 Pod 加了一条对 node.kubernetes.io/memory-pressure 的 toleration,想让它「免疫」压力 taint。为什么错:压力 taint 的语义是「别往这个节点调度新 Pod」,toleration 只是让调度器无视这个信号,并没有改变 kubelet 的驱逐排序——既有 Pod 照样被淘汰,新 Pod 还会被继续塞进一个本该被腾空的节点,把维护窗口拖得更糟。这是在掩盖信号,不是在解决问题。

现象三:批任务反复 Pending,Event 里全是 Preempted

怀疑: 抢占失败,或者优先级配反了。

检查:

kubectl -n ai describe pod nightly-embedding-xxxxx
kubectl -n ai get events --field-selector reason=Preempted

证据: Event 里能看到 Preempted 记录,抢占者身上有 nominatedNodeName,但绑定并没有立刻完成。

根因: 抢占是「提名」,不是「兑换」。抢占者被写上一个候选节点,还要等受害者真正退出才可能真正调度上去;受害者的优雅终止期取决于它自己的 terminationGracePeriodSeconds,而低优先级批任务本身也没有任何「抢占一定成功」的保证——它只是被牺牲顺序里靠后的那一类。

修复: 不要对抗中断,改成适配中断:任务切小、支持重试、能续跑就续跑(backoffLimit 与 activeDeadlineSeconds 兜底)。

证据不撒谎
猜测会误导
先查预算数
再看排序键

07、优化

V2 只做三件事,每件都对应 06 里的一条证据。

第一件,修 PDB 的健康语义。根因是不健康副本会占掉全部预算,所以把 PDB 改成 maxUnavailable: 1,并显式声明不健康 Pod 的驱逐策略。maxUnavailable 的基准是期望副本数,扩缩容时预算会跟着自动伸缩,不需要每次调副本都回来改 PDB。

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: gateway-pdb
  namespace: ai
spec:
  maxUnavailable: 1
  unhealthyPodEvictionPolicy: AlwaysAllow
  selector:
    matchLabels:
      app: gateway

unhealthyPodEvictionPolicy 取 AlwaysAllow 时,即使预算已经算成 0,不健康的 Pod 依然允许被驱逐;取 IfHealthyBudget 则维持旧的严格行为。这个字段属于较新的字段,你的集群版本是否默认支持需要按当前官方文档核验;如果 API Server 不认这个字段,apply 会直接被拒,这是最快的版本探测方式。

第二件,修驱逐排序里的位置。把 GPU 推理的 requests 调到贴近真实水位,limits 与 requests 对齐:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: gpu-inference
  namespace: ai
spec:
  replicas: 3
  template:
    spec:
      priorityClassName: inference-critical
      terminationGracePeriodSeconds: 45
      containers:
        - name: server
          image: registry.local/ai/inference:2.7.1
          resources:
            limits:
              cpu: 4
              memory: 24Gi
              nvidia.com/gpu: 1
            requests:
              cpu: 4
              memory: 24Gi
              nvidia.com/gpu: 1

这里有个真实的取舍:把 requests 抬到真实水位,等于向调度器承诺「这块内存我一直在用」,集群可用率会下降,批任务能塞进来的空隙变小。我选择接受这个下降,因为推理 Pod 冷启动的代价远高于一批离线分片重跑。

第三件,把批任务的定位写明确。它继续用低优先级,但配上 preemptionPolicy: Never,表示它不去抢占别人,只接受被别人抢占;同时给显式 requests,避免它因为「超出 requests」成为压力驱逐的第一顺位。

验收口径也固化成命令,不再靠人眼看:

kubectl -n ai get pdb -o custom-columns=NAME:.metadata.name,MAXUNAVAIL:.spec.maxUnavailable,HEALTHY:.status.currentHealthy,DESIRED:.status.desiredHealthy,ALLOWED:.status.disruptionsAllowed
kubectl drain gpu-node-07 --ignore-daemonsets --delete-emptydir-data --timeout=600s
kubectl -n ai get pods -l app=gateway -o wide

示例输出(第一行是维护前,第二行是维护中):

NAME          MAXUNAVAIL   HEALTHY   DESIRED   ALLOWED
gateway-pdb   1            2         3         0

注意 ALLOWED 依然是 0——maxUnavailable: 1 配 3 个期望副本时,预算余量本来就是 1,一个副本不健康就归零。真正让 drain 能推进的不是这个数字,而是 AlwaysAllow 让那个不健康的 Pod 不再受预算约束。这正是最容易读错的一处:预算为 0,不等于一步都走不了。

改一处字段
少一场死锁
取舍写下来
才算真优化

08、演进

[同一输入]
    |
    +--[V1] 同优先级 + minAvailable 兜底 + kubectl delete:中断无预算、排序无依据
    |        代价:维护靠人盯,容量掉档和 Pending 都事后才发现
    |
    +--[V2] 优先级分层 + 健康语义修正的 PDB + requests 贴近真实水位
    |        代价:集群利用率下降,参数语义变多,需要版本支撑新字段
    |
[Trade-off]
得到什么:中断数可预算、牺牲顺序可推导、维护过程可预检
失去什么:一部分混部利用率,以及「一套配置打天下」的简单性
适用边界:单集群混合负载 + 例行维护窗口;承受不住任何非自愿中断的负载应独立节点池

正确性上,V2 把「Pod 为什么消失」变成了可回答的问题:drain 走的路径看 PDB 预算,压力驱逐看 requests 画像,抢占看优先级与 nominatedNodeName,三条路径各有各的证据。

稳定性上,V2 在节点异常时的表现明显更好——V1 会死锁在「不健康副本吃光预算」上,V2 允许把不健康的 Pod 驱逐出去重建。但 V2 也引入了一个新的不稳定性来源:如果 AlwaysAllow 被滥用在一个副本本来就很少的负载上,drain 可能把仅存的健康副本也一起带走。所以这个字段只应该给「有控制器兜底 + 有一定冗余」的负载用。

复杂度上,V2 多了两个概念(优先级分层、健康语义),配置行数翻倍,但换来的是变更窗口内不需要人做判断。成本主要是利用率:requests 到真实水位之后,混部的余量变小,批任务的可调度窗口变窄。

适用范围要诚实:这套东西解决的是「中断可预测」,不解决「中断不发生」。如果某个推理实例连一次重启都不能承受,那应该在节点池层面隔离,而不是继续加 PDB 字段。

遗留问题有两个:一是 kubelet 侧的 evictionHard / evictionSoft 阈值属于节点配置,不在这个 kustomize 目录里,需要走节点池的 KubeletConfiguration 通道统一管理;二是 unhealthyPodEvictionPolicy 的版本支持需要按当前官方文档核验后再进生产。

同一台节点
两条不同路
代价各不同
边界各不同

09、洞见

9.1 优先级描述的是顺序,不是保护

优先级从来不是「免死」,它只是在同一把尺子上的相对位置。反直觉判断:把优先级调高带来的最大收益,往往不是「更不容易被杀」,而是「被杀之后更容易排回来」——因为抢占和调度排序里,高优先级能更快拿到资源。如果你的问题是被中断后的恢复速度,调优先级有用;如果你的问题是被中断本身,调优先级基本没用,得去调 requests 画像和 PDB 预算。

9.2 压力驱逐的第一排序键是 requests 画像

反直觉判断:一个优先级的数字,救不了一个 requests 写得太小的 Pod。kubelet 在节点压力下先看「用量有没有超出自己声明的 requests」,再看优先级。这意味着 resources 字段同时承担了两个角色——调度时的资源声明,和驱逐时的排序输入。很多人只把它当第一个角色用,于是「资源申请得越小越容易调度」这个直觉,在驱逐阶段变成了「越容易被牺牲」。

9.3 PDB 是闸门,只对自愿中断生效

PDB 只约束 Eviction API 这条路径。抢占不走它,kubelet 压力驱逐不走它。所以「我配了 PDB 它怎么还是被杀」和「我配了 PDB 维护怎么做不完」这两句话都是对的,它们描述的是两条不同的路径。把 PDB 当护盾,会在节点异常时死锁;把 PDB 当闸门,才会去检查它的分子分母(健康副本数与期望副本数)到底是谁算的。

9.4 抢占是一次带启发式的下注

抢占会写 nominatedNodeName,但那只是提名。它能否真正落地,取决于受害者愿不愿意退出、退出得多快,以及调度器后续的重试。同时调度器在挑选受害者节点时,会尽量优先选择不会破坏 PDB 的节点——这是一种偏好,不是约束。把抢占当资源保证来设计系统,通常会在维护窗口里第一次被现实教育。

优先级非王
Requests 先行
预算非护盾
边界即答案

10、系统落地

原来有什么:一套靠人工判断的维护流程,三类负载同优先级,PDB 只写在在线负载上,中断是否发生全凭运气。

本篇新增了什么:deploy/base/resilience/ 这个目录。它把优先级分层(inference-critical / batch-preemptible)、在线负载的中断预算(gateway-pdb / gpu-pdb)、以及批任务的可牺牲声明(低优先级 + 不抢占)收进同一处,让中断边界成为可评审的配置文件,而不是散落在各个 Deployment 里的字段。

现在能做什么:维护前可以先用 kubectl get pdb 读出每一步的可中断步数,预判这条 drain 命令会不会卡;维护中出现的拒绝有明确的归因路径,不用凭经验猜;压力驱逐的受害者名单可以被推导,而不是事后从事件里考古。

还缺什么:第一,kubelet 侧的驱逐阈值还没有纳入这套配置,节点压力信号目前只能观察不能调;第二,缺少对「预算长期为 0」的告警,这类负载实际上已经失去了可维护性,但表面上看不出问题;第三,跨节点池的优先级是否分层,取决于未来是否要做物理隔离。

下一步:把 PDB 的 ALLOWED DISRUPTIONS 和节点的 Pressure Condition 接进变更前置检查,让 drain 在有人执行之前就先被拦住一次;再把 KubeletConfiguration 里的驱逐阈值按节点池收敛,让压力信号从「观察项」变成「配置项」。

从无到有配
从有到能守
从守到能证
一步一留痕

11、小结

Q1 → 高优先级不等于免驱逐,kubelet 先看用量是否超出 requests,优先级只是第二排序键
Q2 → PDB 只约束 Eviction API 这类自愿中断,拦不住调度抢占与节点压力驱逐
Q3 → 差异来自 requests 画像、PDB 健康语义与抢占策略三者的组合,而不是同一条命令的随机性
状态 → deploy/base/resilience/ 下三类负载的中断边界可配置、可预检、可验证

回到最开始那个死锁:节点有问题导致副本 NotReady,副本不健康导致预算归零,预算归零导致你无法把这个节点排水。它的解法不在运维流程里,而在 PDB 的健康语义里——不健康 Pod 不应该占着健康预算。这类问题的共同点是:当你发现两个正确配置互相咬死时,八成是把某个字段的语义记成了它邻居的语义。

问题有答案
能力有边界
结论有证据
落地有路径

12、作业

12.1 理解题:一个 Pod 有高优先级、Guaranteed 画像,节点内存压力触发时它还会被驱逐吗?

参考答案:仍有可能。优先级和 QoS 画像只影响排序权重,不构成免疫。kubelet 在压力驱逐时的排序是启发式的:先看是否超出 requests,再看优先级,然后看超出幅度。Guaranteed 画像的作用是让它不落在「超出 requests」这一层,从而排到靠后;但如果整个节点上所有 Pod 都没有超出 requests,而压力仍在持续,kubelet 依然要按规则淘汰一些 Pod 才能回收资源。真正给它兜底的是「有控制器重建 + 调度优先级让它更容易回来」,不是「它永远不死」。

12.2 实战题:给你的网关 Deployment 设计一套 PDB 与探针组合,要求在「一个副本 NotReady」时依然可以执行 drain。

参考答案:第一,选择 maxUnavailable 而不是 minAvailable,让预算基准是期望副本数,扩缩容时自动伸缩。第二,显式声明 unhealthyPodEvictionPolicy: AlwaysAllow,使不健康的 Pod 不占用预算、可被驱逐(该字段的版本支持需按当前官方文档核验)。第三,Readiness 探针要能真实反映「能否接流量」,而不是「进程是否活着」,否则会出现 Pod 显示 Ready 但实际不可用、或者反过来长期 NotReady 吃预算。第四,terminationGracePeriodSeconds 要和 preStop 里的连接摘除逻辑、以及 drain 的 --timeout 对齐,否则会出现「预算允许,但退出太慢」。最后,把 ALLOWED DISRUPTIONS 加入变更前置检查。

12.3 排障题:kubectl drain 报 Cannot evict pod as it would violate the pod's disruption budget,但你确认副本数是够的,如何定位?

参考答案:先读数字再读配置。用 kubectl get pdb -o yaml 或 custom-columns 看 status.currentHealthy、status.desiredHealthy、status.disruptionsAllowed,再和 spec.minAvailable / spec.maxUnavailable 对照。常见根因是「健康副本数小于期望副本数」——也就是有 Pod 处于 NotReady,它吃掉了预算但看起来「副本数是够的」。接着查 kubectl get pods -l <selector> -o wide 找出不健康的 Pod,再 describe 看探针失败原因。还要注意先确认 drain 走的是 Eviction API(没有加 --disable-eviction),否则这条报错根本不会出现。

12.4 架构判断题:为了提升集群利用率,是否应该把在线推理的 requests 调到远低于真实用量的水平?

参考答案:不建议。requests 同时是调度输入和驱逐排序输入。调低 requests 确实能让更多 Pod 装进同一个节点,短期利用率上升,但代价是:这些 Pod 会落进「用量超出 requests」的第一梯队,在节点压力下被优先淘汰;同时实际的 overcommit 让节点更早进入 Pressure 状态,把整批负载一起拖进不稳定窗口。更稳的做法是把 requests 定在贴近真实水位的位置,用优先级分层来区分负载重要性,用 PDB 来控制主动维护时的中断上限,再把利用率问题交给批任务的低优先级去填谷——也就是让「便宜、可重跑」的那一类去承担混部风险,而不是让在线负载去承担。

题不在难易
在于能否验
答不在多寡
在于能否用

13、思考

这一篇真正想留下的是一个判断顺序:遇到「Pod 为什么会消失」,先问它是被谁弄消失的——调度器的抢占、kubelet 的压力驱逐、还是 Eviction API 放行的自愿中断。这三者对应三套完全不同的参数和证据,先归因再调参,比先调参再解释要省掉一整个维护窗口。

第二个判断是这个:把中断当成容量来管,而不是当成异常来防。防是防不住的——节点会坏、磁盘会满、维护必须做。能管的是「同时能停几个」和「停下来之后谁先回来」,前者靠 PDB 与优先级分层,后者靠 requests 画像和调度器。任何时候你发现自己在讨论「怎么让它绝对不被杀」,都应该停下来,转而讨论「被杀之后恢复的代价是多少,能不能接受」。

第三个判断跟字段有关。这套体系里最容易被记混的三个语义是:优先级是顺序不是保护,PDB 是闸门不是护盾,requests 既是声明也是排序输入。记住它们,比记住任何一份配置模板都值钱。

一念辨边界
一证定根因
一改知取舍
一役见真章

posted @ 2026-10-12 10:13  李福春  阅读(2)  评论(0)    收藏  举报