PriorityClass、抢占、PDB 与容量分级
集群容量吃紧时,一批标记为“紧急”的报表任务先于在线 API 获得节点。平台把报表和 API 都绑定到同一个高值 PriorityClass,希望避免任务排队,却把业务重要性、排队顺序和可抢占资格混成了一个字段。节点上较低优先级的在线副本被逐出,报表开始运行,但 API 的可用副本跌破安全水位。监控看到的是驱逐和重建,真正的起点却是一个没有准入边界的集群级优先级对象。
事故修复时,团队又把 PodDisruptionBudget 设为 minAvailable: 100%,认为这样就能阻止抢占。下一次资源枯竭时,高优先级 Pod 仍然可能选择违反 PDB 的受害者,因为调度抢占对 PDB 只做 best effort:它会优先寻找不违反 PDB 的候选方案,却不承诺永远不违反。Priority、preemption、PDB、quota 和公平共享分别回答不同问题,只有把它们组合成容量合同,才能既保住关键服务又避免优先级通胀。
五个对象回答五个不同问题
PriorityClass 是集群级对象,把名称映射为整数值;Pod 通过 priorityClassName 引用,准入后其 priority 字段得到对应值。值越大,Pod 在调度队列中通常越优先,也更可能在无法调度时触发抢占。globalDefault 只允许一个类为 true,且只影响没有显式 priorityClassName 的新 Pod;修改默认不会追溯已有 Pod。
抢占是 kube-scheduler 的 PostFilter 行为:当高优先级 Pod 没有可行节点时,调度器模拟移除较低优先级 Pod,寻找能让待调度 Pod 通过 Filter 的节点,再提名候选节点并删除受害者。抢占不创建容量,也不保证立刻绑定;受害者有优雅终止时间,节点状态可能在间隙变化,更高优先级 Pod 也可能竞争同一节点。
PDB 约束自愿中断时可同时不可用的副本数。驱逐 API、节点排空和一些控制器会尊重它,但调度抢占只尽力减少违反 PDB 的受害者。ResourceQuota 可以按 PriorityClass 限定 namespace 的 Pod 数量或资源 requests,是“谁能消耗多少高等级容量”的准入边界。公平性则还需要租户份额、队列、借用、回收与饥饿防护;原生 PriorityClass 本身不是公平共享算法。
官方 Pod Priority and Preemption 说明了队列与抢占语义,PodDisruptionBudget 解释自愿中断保护,ResourceQuota 给出按 PriorityClass 限定资源的 scopeSelector 入口。三者必须联合阅读,不能用其中一个替代另外两个。
启用前先盘点默认类、权限和容量事实
Priority 与抢占是 kube-scheduler 内置能力,不需要另装控制器。实验需要有权限读取 Node、创建实验 namespace、PriorityClass、ResourceQuota、PDB 与工作负载,并能读取 Event。PriorityClass 是非命名空间对象,创建权限等价于改变全体租户可引用的等级词典,普通应用发布身份不应持有。
kubectl version
kubectl get priorityclass
kubectl get nodes -o custom-columns=NAME:.metadata.name,CPU:.status.allocatable.cpu,MEMORY:.status.allocatable.memory
kubectl auth can-i create priorityclasses.scheduling.k8s.io
kubectl auth can-i create resourcequotas -n priority-lab
kubectl auth can-i create poddisruptionbudgets.policy -n priority-lab
kubectl auth can-i list events -n priority-lab优先在一次性本地集群执行抢占实验,因为它会真实删除受害 Pod。共享集群只能使用平台批准的专用节点池、独立 namespace 和容量窗口,并先确认节点上的 DaemonSet、系统 Pod、本地盘和其他租户不会成为实验对象。抢占候选只包含比待调度 Pod 优先级低且移除后能让约束成立的 Pod;高优先级系统组件、静态 Pod、硬 affinity 不匹配或卷拓扑冲突都不会被“优先级更高”神奇修复。
创建两个显式类,避免依赖现有默认值:
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: lab-normal
value: 1000
globalDefault: false
description: "Normal priority for an isolated scheduling lab"
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: lab-critical
value: 100000
globalDefault: false
preemptionPolicy: PreemptLowerPriority
description: "Critical priority for an isolated scheduling lab"内置 system-cluster-critical 与 system-node-critical 保留给系统关键组件,业务不得仿造其高值或名称。PriorityClass 的 value 越高并不代表资源已预留;没有 headroom 时,高优先级工作负载只能等待、抢占或触发节点供给。
调度队列先比较优先级,再处理等待年龄
待调度 Pod 在 activeQ、backoffQ 和 unschedulablePods 等结构间移动。默认 QueueSort 插件 PrioritySort 先比较 Pod priority,再按时间等条件维持顺序。高优先级 Pod 因此能越过低优先级排队者,但它仍必须通过 NodeResourcesFit、NodeAffinity、TaintToleration、VolumeBinding、TopologySpread 等 Filter。优先级改变顺序,不删除硬约束。
当一个低优先级 Pod 已经绑定并运行,高优先级 Pod 不会因为“排名更高”直接覆盖它;只有高优先级 Pod当前不可调度、PostFilter 进入抢占评估、存在可行受害者集合时,删除才发生。调度器先过滤可能节点,再逐个模拟移除低优先级 Pod,检查待调度 Pod 是否可放入,并尝试重新放回不必牺牲的 Pod。最终候选还会比较 PDB 违反数、最高受害者优先级、受害者优先级总和和数量等因素。
status.nominatedNodeName 表示调度器认为某节点可能在受害者退出后容纳待调度 Pod,不是资源锁,也不是最终绑定承诺。受害 Pod 的 terminationGracePeriodSeconds 过长会扩大空窗;调度器可能清除或更换 nominated node;其他 Pod 也可能先占用释放容量。排障时要同时保存高优先级 Pod、受害者、节点 Event 和时间线。
正向实验验证低优先级受害者被替换
创建 priority-lab,选择一次性集群中的一个实验节点并加隔离标签。不要修改生产拓扑标签:
kubectl create namespace priority-lab
kubectl label node <lab-node> scheduling.example.com/priority-lab=true
kubectl apply -f priorityclasses.yaml
kubectl get node <lab-node> -o jsonpath='{.status.allocatable.cpu}{"\n"}'根据节点 allocatable.cpu 和 DaemonSet 等既有 Pod 的 requests 选择演示值。下面假设实验节点约有 2 核可用于这组工作负载:三个低优先级副本各申请 500m,高优先级 Pod 申请 1200m。先确认三个低优先级副本运行后,节点剩余 CPU 满足 free + 500m < 1200m <= free + 1000m;这样删除一个受害者仍不够,删除两个才足以容纳高优先级 Pod。若不满足这个不等式,就按实际余量调整请求值,不要继续套用示例。资源 requests 才参与调度,实时 CPU 使用率不会替代这个计算。
apiVersion: apps/v1
kind: Deployment
metadata:
name: low-workers
namespace: priority-lab
spec:
replicas: 3
selector:
matchLabels:
app: low-workers
template:
metadata:
labels:
app: low-workers
spec:
priorityClassName: lab-normal
terminationGracePeriodSeconds: 5
nodeSelector:
scheduling.example.com/priority-lab: "true"
containers:
- name: pause
image: registry.k8s.io/pause:3.10
resources:
requests:
cpu: 500m
memory: 32Mi
---
apiVersion: v1
kind: Pod
metadata:
name: high-request
namespace: priority-lab
spec:
priorityClassName: lab-critical
nodeSelector:
scheduling.example.com/priority-lab: "true"
containers:
- name: pause
image: registry.k8s.io/pause:3.10
resources:
requests:
cpu: 1200m
memory: 32Mi先只应用 Deployment,确认三个副本都在实验节点,再应用高优先级 Pod:
kubectl apply -f low-workers.yaml
kubectl -n priority-lab rollout status deploy/low-workers
kubectl describe node <lab-node> | sed -n '/Allocated resources:/,/Events:/p'
kubectl apply -f high-request.yaml
kubectl -n priority-lab get pods -w
kubectl -n priority-lab get pod high-request \
-o jsonpath='{.spec.priorityClassName}{"\t"}{.status.nominatedNodeName}{"\t"}{.spec.nodeName}{"\n"}'
kubectl -n priority-lab get events --sort-by=.lastTimestamp在上述不等式成立且没有其他工作负载同时释放容量时,预期事件链是高优先级 Pod 最初因 CPU requests 不足不可调度,随后两个 low-workers 被抢占并进入 Terminating,high-request 获得 nominated node,受害者释放 requests 后被绑定。Deployment 会继续补副本,但新低优先级 Pod 可能保持 Pending,因为高优先级 Pod 已占用容量。若受害者数量不是两个,应先重算实验期间的节点余量与全部 requests,不能把结果硬解释成调度器违反了预期。
证明闭环需要四类证据:PriorityClass 值正确;高优先级 Pod 的 PodScheduled 最终为 True;受害 Pod 出现与 preemption 相关的 Event 或删除时间;节点 allocated requests 在前后发生可解释变化。单看高优先级 Pod Running 无法证明是否发生了抢占,也可能是节点扩容或其他 Pod 自然退出。
反向实验证明 PDB 不是抢占的硬锁
先删除高优先级 Pod,等待 Deployment 恢复三个副本。给低优先级服务添加一个允许单副本中断的 PDB:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: low-workers
namespace: priority-lab
spec:
maxUnavailable: 1
selector:
matchLabels:
app: low-workerskubectl -n priority-lab delete pod high-request --ignore-not-found
kubectl -n priority-lab rollout status deploy/low-workers
kubectl apply -f low-workers-pdb.yaml
kubectl -n priority-lab get pdb low-workers
kubectl -n priority-lab get pdb low-workers \
-o custom-columns=ALLOWED:.status.disruptionsAllowed,HEALTHY:.status.currentHealthy,DESIRED:.status.desiredHealthy
kubectl apply -f high-request.yaml
kubectl -n priority-lab get pods -w调度器会优先选择 PDB 违反数更少的候选节点和受害者集合。在前面的容量不等式仍成立、disruptionsAllowed 为 1 且没有别的节点或容量变化时,调度器必须删除两个副本才能放下 high-request;第二个受害者超出预算,但抢占仍可能继续,这正是 best effort 的含义。预期证据是两个受害 Pod 被删除、PDB 健康副本一度低于期望值、高优先级 Pod 最终绑定,而不是“PDB 拒绝了 API 请求”。若只删除一个受害者,说明实验余量已经变化,本轮不能作为 PDB 可被违反的证据。事件文字随版本变化,应以受害者删除、PDB status 与绑定时间线为准。
若要验证没有可行受害者的另一种失败,把高优先级 Pod 增加一个实验节点不具备的 required node affinity。即使它优先级最高,事件仍会指出 node affinity/selector 不匹配,抢占对调度没有帮助。这个反例区分了“资源可通过移除 Pod 释放”和“节点事实根本不满足”两类 Pending。
实验结束先删除高优先级 Pod和 PDB,确保低优先级 Deployment 恢复,再进入清理。不要把 minAvailable: 100% 当成止损开关;真正阻止某类 Pod主动抢占,应使用 preemptionPolicy: Never,而保护被抢占方还需要容量冗余、优先级层级与准入约束共同作用。
非抢占优先级只改变发起方行为
创建非抢占类:
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: lab-critical-nonpreempting
value: 90000
globalDefault: false
preemptionPolicy: Never
description: "Queue priority without initiating preemption"引用它的 Pod 仍会在队列中排在低优先级 Pod 前面,但不会为了自己删除低优先级 Pod。它会等待自然容量释放或节点扩容,适合任务重要但不允许破坏在线服务的场景。preemptionPolicy: Never 只约束这个 Pod不发起抢占,并不使它免疫:更高优先级且允许抢占的 Pod 仍可把它选为受害者。
队列优先但不抢占也可能导致低优先级工作负载饥饿。高优先级 Pod 持续到达时,低等级 Pod即使等待很久也不一定反超。批处理场景需要配额、队列与公平共享能力提供份额和老化策略;在线场景则更常用有限的服务等级、专属 headroom 和发布准入,而不是创建数十个数值接近的 PriorityClass。
ResourceQuota 才能限制高等级容量入口
PriorityClass 是集群级共享词典,任何能在 Pod spec 引用名称的租户都可能“借用”高优先级。可以用 ResourceQuota 的 PriorityClass scope 把高等级 Pod 限制在明确预算内:
apiVersion: v1
kind: ResourceQuota
metadata:
name: critical-budget
namespace: priority-lab
spec:
hard:
pods: "2"
requests.cpu: "2"
requests.memory: 2Gi
scopeSelector:
matchExpressions:
- scopeName: PriorityClass
operator: In
values:
- lab-critical
- lab-critical-nonpreempting该配额限制带指定优先级的 Pod 数量和 requests,但不会自动为它们预留节点容量。还应通过 ValidatingAdmissionPolicy、策略引擎或发布平台建立“命名空间/工作负载身份 -> 允许 PriorityClass”映射,拒绝未批准的高等级引用、禁止业务使用系统关键类,并要求每个 Pod 声明 requests。ResourceQuota 的创建和变更也要纳入审计,否则 namespace owner 可以自行抬高预算。
公平治理至少包含四层:租户准入限制谁能使用等级;quota 限制最多消耗多少 requests;节点池与冗余保证关键等级有恢复空间;队列系统处理批任务份额、借用、回收和饥饿。只使用 PriorityClass 会形成“谁喊得更急谁赢”,最终所有工作负载都升级到同一高值,排序信号失效。
从 Event 和状态字段还原抢占时间线
排障时先确认高优先级 Pod 是否真的因为资源不足而不可调度,再检查 nominated node 与受害者。常用证据:
kubectl -n <namespace> get pod <pending-pod> -o yaml
kubectl -n <namespace> describe pod <pending-pod>
kubectl -n <namespace> get events --sort-by=.lastTimestamp
kubectl -n <namespace> get pdb
kubectl -n <namespace> get resourcequota
kubectl get priorityclass
kubectl describe node <candidate-node>Preemption is not helpful for scheduling 一类信息意味着移除低优先级 Pod 也无法满足硬约束或释放所需资源;它不是“抢占功能关闭”。有 nominated node 却长期 Pending,要检查受害者终止宽限期、finalizer、本地卷、PDB 状态、节点条件和新的竞争者。受害 Pod 被重建后反复遭抢占,则是容量不足或优先级层级错误,不应靠缩短终止时间掩盖。
监控应跟踪按 PriorityClass 分组的 pending 年龄、调度尝试结果、抢占次数、受害者等级、PDB 健康缺口、quota 拒绝、节点 headroom 和业务 SLO。低等级被抢占次数单调增长、高等级长期 Pending、同一 Deployment 反复重建、PDB disruptionsAllowed=0 持续过久,都是治理信号。Event 有保留期且可能聚合,事故证据要及时关联 Pod UID、owner reference 和审计记录。
抢占的容量代价是中断、冷启动与反复重建
抢占释放的是 requests 账面容量,真实业务恢复还要等待受害者终止、高优先级镜像拉取、卷挂载、启动探针和预热。若高优先级 Pod 的镜像很大或依赖稀缺卷,删除受害者后仍可能长时间不可用,形成“双输窗口”。容量模型必须把 termination grace、镜像冷启动、启动 p99、最小可用副本和节点供给延迟纳入恢复时间。
Deployment 会补回被抢占副本。如果节点仍无空间,新副本进入 Pending;若扩容器随后创建节点,低优先级服务又恢复。这段时间产生额外镜像、网络、日志和节点成本。抢占频繁通常说明持续容量缺口,不是可以免费反复使用的稳态调度手段。
关键在线服务宜保留可度量 headroom,并把 PriorityClass 层级控制在少量、语义稳定的等级。批任务适合配额与队列准入,允许在闲时借用、压力时回收;系统组件使用保留的系统等级;普通服务保持默认或业务常规等级。每个等级都要绑定最大 requests、允许使用者、被抢占后果、SLO 与扩容策略。
升级、回滚和责任治理防止优先级通胀
升级控制面或修改调度器 Profile 前,在隔离环境回放四组场景:资源不足且可抢占、硬约束不可修复、PDB 可满足、PDB 必须违反。保存升级前后的 Event 类型、受害者集合、nominated node、调度延迟和 PDB 状态。Priority 与非抢占 PriorityClass 已通过稳定 API 提供,但具体调度器默认插件与指标仍应按目标发行线验证。
PriorityClass 的 value 不能原地随意调整来完成迁移。更稳妥的做法是创建新名称,让 canary 工作负载引用新类,观察配额、抢占和 SLO,再分批迁移模板;最后确认没有 Pod 引用旧类后删除。回滚顺序相反:恢复旧类和 quota,切回工作负载模板,验证新建 Pod 的 priority,再退役新类。删除仍被引用的 PriorityClass 会使新 Pod 准入失败,已有 Pod 的 priority 也不会因此自动重新计算。
平台团队拥有 PriorityClass 词典、调度器与容量分级;应用团队说明业务等级、requests、PDB 和被中断后果;容量团队维护 headroom、节点供给与恢复时间;安全或治理团队审批系统类与高等级引用;批平台负责租户份额和公平队列。每个高等级类都应有 owner、允许命名空间、quota 模板、使用审计、演练记录和退出条件。凭证本身不应写入工作负载清单,自动化创建 PriorityClass 的身份使用短期凭证或工作负载身份,并限制为专用流水线。
清理顺序先停止触发抢占的 Pod,再恢复受害工作负载,最后移除集群级对象和节点标签:
kubectl -n priority-lab delete pod high-request --ignore-not-found
kubectl -n priority-lab delete pdb low-workers --ignore-not-found
kubectl -n priority-lab rollout status deploy/low-workers --timeout=2m
kubectl delete namespace priority-lab --wait=true
kubectl delete priorityclass lab-critical lab-critical-nonpreempting lab-normal \
--ignore-not-found
kubectl label node <lab-node> scheduling.example.com/priority-lab-
kubectl get priorityclass
kubectl get pods -A -o custom-columns=NS:.metadata.namespace,NAME:.metadata.name,PC:.spec.priorityClassName \
| grep -E 'lab-critical|lab-normal' || true完成证据包括实验 namespace 消失、节点标签恢复、没有 Pod 引用实验类、PriorityClass 和 quota 均已删除、低优先级受害者不再重建、集群调度延迟回到基线。若实验创建过临时集群,直接销毁它比逐项清理更可靠,同时确认云节点、磁盘、负载均衡和日志索引没有产生残留费用。
