Kubernetes 基础体系 · 第 24/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。
Kubernetes Priority 与 Preemption:优先级、抢占、公平和关键服务保护
Kubernetes 中的 Priority(优先级)回答的是“当多个 Pod 竞争调度机会时,谁更重要”;Preemption(抢占)回答的是“高优先级 Pod 无法调度时,是否可以驱逐低优先级 Pod 来腾出资源”。
二者经常被合并理解,但它们不是同一个机制:
- 优先级首先影响调度队列中的顺序;
- 抢占只在高优先级 Pod 找不到可行节点时作为补救动作;
- 优先级本身不保证资源一定充足;
- 抢占不等于删除高优先级 Pod,也不等于绕过节点约束;
- 优先级也不提供租户公平性,配置不当时可能导致低优先级工作负载长期饥饿。
本文基于 Kubernetes 稳定 API,重点解释 PriorityClass、调度器的队列与抢占流程、PDB 的交互、关键服务保护、资源公平以及生产环境中的验证和风险。
一、调度器为什么需要优先级
假设集群中只有一个节点:
节点容量:CPU 4,内存 8 GiB
等待调度的 Pod:
A:CPU 2,优先级 100
B:CPU 2,优先级 10
C:CPU 1,优先级 1000
如果所有 Pod 同时进入调度队列,调度器需要决定先尝试哪个 Pod。优先级通常使队列顺序接近:
C → A → B
这并不意味着 C 一定能够运行。调度器仍然必须检查:
- 节点是否有足够的
resources.requests; nodeSelector和 Node Affinity 是否匹配;- Pod Affinity、Anti-Affinity 是否满足;
- 节点是否存在未容忍的 Taint;
- 拓扑分布约束是否允许放置;
- 其他调度过滤器是否通过。
因此,优先级影响的是“先尝试谁”,而不是“无条件放行谁”。
1. 优先级使用整数表示
Kubernetes 使用整数表示 Pod 优先级。数值越大,优先级越高。
Pod 通常不是直接填写整数,而是通过 PriorityClass 引用:
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: platform-critical
value: 900000000
globalDefault: false
description: "平台关键控制面工作负载"
随后在 Pod 或工作负载模板中使用:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
priorityClassName: platform-critical
containers:
- name: api
image: example/api:1.0
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "1"
memory: "1Gi"
这里的 priorityClassName 是 Pod 模板的一部分。Deployment 创建 Pod 时,API Server 的优先级相关准入逻辑会根据这个名称解析优先级,并把结果写入 Pod 的优先级字段。
2. PriorityClass 是集群级资源
PriorityClass 的 API 组是:
scheduling.k8s.io/v1
它不是 Namespace 级对象。因此,名称在整个集群中唯一,任何 Namespace 中的 Pod 都可以引用同一个 PriorityClass。
这会带来两个实际含义:
- Namespace 管理员通常不能仅靠 Namespace 隔离优先级定义;
- 如果允许用户创建或引用任意高优先级,租户之间可能出现资源竞争和抢占。
生产环境通常应通过 RBAC、准入策略或平台管理流程限制高优先级 PriorityClass 的使用,而不是把所有优先级定义权交给业务团队。
二、PriorityClass 的关键字段
一个 PriorityClass 的核心结构如下:
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: batch-low
value: -100
globalDefault: false
preemptionPolicy: Never
description: "低优先级、不可抢占其他 Pod 的批处理任务"
value:数值优先级
value 越大,优先级越高。Kubernetes 允许使用负值,这对于表达“尽量在后台运行”的工作负载很有用。
需要注意两点:
- Kubernetes 内置关键系统
PriorityClass使用很高的优先级; - 用户定义的优先级不应随意接近或超过系统保留范围。
当前 Kubernetes 文档对用户优先级值有保留范围约定:用户定义值通常不应超过 1,000,000,000,更高数值留给系统使用。具体内置对象和发行版可能随版本变化,部署前应检查集群实际存在的 PriorityClass:
kubectl get priorityclass
典型输出可能包含:
NAME VALUE GLOBAL-DEFAULT AGE
system-cluster-critical 2000000000 false 30d
system-node-critical 2000001000 false 30d
platform-critical 900000000 false 10d
batch-low -100 false 2d
system-node-critical 和 system-cluster-critical 是 Kubernetes 常见的内置关键系统优先级。它们用于保护系统组件,不应被普通业务工作负载复用。
globalDefault:未指定优先级时的默认值
最多只能有一个 PriorityClass 设置:
globalDefault: true
例如:
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: ordinary
value: 0
globalDefault: true
description: "未显式指定优先级的普通 Pod"
它只影响之后创建的、没有显式指定 priorityClassName 的 Pod。已经存在的 Pod 不会因为后来创建了默认 PriorityClass 而自动改变优先级。
因此,下面两种 Pod 不应混为一谈:
# 显式指定
spec:
priorityClassName: batch-low
以及:
# 未指定,使用集群默认值(如果存在)
spec:
containers:
- name: worker
image: example/worker:1.0
如果集群没有全局默认 PriorityClass,未指定优先级的 Pod 通常使用优先级 0。
同一个集群中不应存在多个 globalDefault: true 的定义。即使某些操作流程暂时允许对象同时存在,默认行为也会变得不明确,不应依赖这种状态。
preemptionPolicy:能否抢占其他 Pod
preemptionPolicy 当前支持:
preemptionPolicy: PreemptLowerPriority
和:
preemptionPolicy: Never
默认值是 PreemptLowerPriority。
两者区别如下:
| 配置 | 能否排队优先于低优先级 Pod | 能否驱逐低优先级 Pod |
|---|---|---|
PreemptLowerPriority |
可以 | 可以 |
Never |
可以 | 不可以 |
Never 不是“低优先级 Pod”,而是“具有一定调度优先级、但不主动抢占其他 Pod”。它适合某些希望优先获得新资源、但不希望破坏已有工作负载的批处理任务。
例如,批处理 Pod 的优先级可以高于普通后台任务,但设置为 Never:
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: batch-preferred
value: 100
preemptionPolicy: Never
globalDefault: false
description: "优先调度,但不抢占正在运行的低优先级 Pod"
此时,batch-preferred Pod 会排在优先级为 0 的 Pod 前面,但如果节点资源已经被后者占用,它不能通过抢占来腾出空间。
三、一个 Pod 的优先级是如何确定的
Pod 的优先级确定过程可以概括为:
flowchart TD
A[创建 Pod 或工作负载模板] --> B{是否指定 priorityClassName}
B -- 是 --> C[查找 PriorityClass]
C --> D{对象存在且可使用}
D -- 否 --> E[API 请求被拒绝]
D -- 是 --> F[写入 Pod priority 和相关策略]
B -- 否 --> G{是否存在 globalDefault PriorityClass}
G -- 是 --> H[使用全局默认 PriorityClass]
G -- 否 --> I[使用默认优先级 0]
F --> J[进入调度流程]
H --> J
I --> J
几个容易忽略的边界:
PriorityClass不存在时,引用它的 Pod 创建会失败,而不是创建一个“优先级未知”的 Pod。- 修改或删除
PriorityClass不会像修改 Deployment 模板那样自动重建已有 Pod。 - Deployment、StatefulSet、Job 等控制器会把模板中的优先级配置复制到新 Pod;已有 Pod 的优先级不会因为模板变化而原地修改。
- Pod 的优先级字段属于系统确定的结果,业务通常应设置
priorityClassName,而不是尝试直接伪造最终优先级。
可以使用以下命令检查某个 Pod:
kubectl get pod api-7d8f6c9d7b-x2abc -n production \
-o jsonpath='{.spec.priorityClassName}{"\n"}{.spec.priority}{"\n"}'
预期可能为:
platform-critical
900000000
如果 Pod 来自 Deployment,还应检查模板,否则下一次滚动更新可能重新创建成另一种优先级:
kubectl get deployment api -n production \
-o jsonpath='{.spec.template.spec.priorityClassName}{"\n"}'
四、调度队列中的优先级:优先并不等于立即运行
Kubernetes 调度器处理的是尚未绑定节点的 Pod。简化后的生命周期如下:
sequenceDiagram
participant API as API Server
participant Q as Scheduler Queue
participant S as Scheduler
participant K as Kubelet
API->>Q: 创建 Pending Pod
Q->>S: Pop 下一个待调度 Pod
S->>S: Filter 节点
S->>S: Score 可行节点
alt 存在可行节点
S->>API: 创建绑定关系
API->>K: 更新 Pod
K->>K: 创建容器
else 没有可行节点且允许抢占
S->>S: 计算候选节点和受害 Pod
S->>API: 删除受害 Pod
S->>Q: 记录 nominatedNodeName 并等待重试
else 没有可行节点且不能抢占
S->>Q: 进入 unschedulable/backoff
end
真实调度器内部还存在 active queue、backoff queue 和 unschedulable queue 等状态。调度失败的 Pod 不一定会持续高速重试,而是可能进入退避状态,等待集群变化或重新入队。
因此,“优先级高”通常表示:
- 在同一调度器队列中更早被尝试;
- 在调度器需要从等待队列选择下一个 Pod 时优先;
- 可能触发对低优先级 Pod 的抢占。
它不表示:
- 绕过所有过滤条件;
- 绕过资源请求;
- 绕过 Taint、Affinity 或拓扑约束;
- 立即获得一个节点;
- 能够抢占同优先级或更高优先级 Pod。
一个重要的反例:高优先级 Pod 也可能 Pending
假设:
节点 node-a:
CPU 总量 4
已分配 requests.cpu = 4
高优先级 Pod critical:
requests.cpu = 1
nodeSelector: topology.kubernetes.io/zone=zone-c
而集群中没有任何属于 zone-c 的节点。即使 critical 的优先级极高,也没有任何节点满足其硬约束。抢占低优先级 Pod 不会创造 zone-c 节点,因此抢占不能解决这个问题。
另一个反例是:
Pod requests.cpu = 1
容器实际使用 CPU = 100m
调度器依据的是 requests.cpu,而不是当前实际使用量。即使节点监控显示还有空闲 CPU,只要调度视图中的可分配请求空间不足,Pod 仍可能被判定为不可调度。
五、抢占的形式化条件
设:
- 是当前等待调度的 Pod;
- 是其优先级;
- 是一个候选节点;
- 是节点 上可以被考虑驱逐的低优先级 Pod 集合;
- 是 Pod 的资源请求;
- 表示在节点 上保留 Pod 集合 时,所有调度过滤条件都满足;
- 是节点 上原有的 Pod 集合。
如果存在某个节点 ,使得:
并且对每个被驱逐的 Pod :
那么节点 可能成为抢占候选节点。
这里的“过滤条件”不仅包括 CPU 和内存,还包括:
- Node Selector;
- Node Affinity;
- Pod Affinity 和 Anti-Affinity;
- Taint 与 Toleration;
- 端口冲突;
- Volume 拓扑;
- Topology Spread Constraints;
- 其他调度插件约束。
抢占的核心不是“找一个低优先级 Pod 删除”,而是:
- 找到一个原本不适合 的节点;
- 选择一组低优先级 Pod 作为潜在受害者;
- 移除这些 Pod 后重新运行调度过滤逻辑;
- 只有过滤条件变为可行,节点才有抢占价值。
完整算例:资源抢占
集群有两个节点:
node-a:CPU 8,已分配 8
node-b:CPU 8,已分配 6
node-a 上:
pod-a1:CPU 3,优先级 10
pod-a2:CPU 3,优先级 20
pod-a3:CPU 2,优先级 30
node-b 上:
pod-b1:CPU 2,优先级 10
pod-b2:CPU 2,优先级 20
pod-b3:CPU 2,优先级 30
现在创建:
critical:CPU 4,优先级 100
调度器先尝试普通调度:
- node-a 剩余请求容量为 0,不可行;
- node-b 剩余请求容量为 2,不可行。
然后考虑抢占。
对 node-a:
- 驱逐
pod-a1后只释放 3 CPU,仍不足; - 驱逐
pod-a1和pod-a2后释放 6 CPU,剩余 6 CPU,可容纳critical; - 被驱逐 Pod 的优先级都低于 100。
对 node-b:
- 驱逐
pod-b1和pod-b2后释放 4 CPU,也可容纳critical。
调度器会综合候选节点和受害者,倾向于选择破坏性更小的方案,而不是简单地选择“优先级最低的节点”。实际选择还会受节点打分、Pod 优先级、Pod 数量、PDB 影响程度等因素影响;不能把所有版本和调度器配置下的精确选择顺序当作 API 保证。
同优先级 Pod 不会互相抢占
如果:
critical:优先级 100
peer:优先级 100
即使 peer 占用了 critical 所需的资源,critical 也不能因为同优先级而抢占 peer。
这意味着优先级层级应当设计成有意义的分层,而不是把所有关键服务都设置成同一个高值后,期待它们之间仍然存在细粒度保护关系。
六、抢占的实际状态变化
抢占不是一个原子操作。典型过程如下:
- 高优先级 Pod 调度失败;
- 调度器分析节点并选择潜在受害者;
- 调度器向 API Server 发出删除受害 Pod 的请求;
- 受害 Pod 进入删除流程,可能处于
Terminating; - 高优先级 Pod 通常会记录
status.nominatedNodeName,表示调度器倾向于使用该节点; - 等待资源真正释放、Pod 删除完成或集群状态变化;
- 高优先级 Pod 再次进入调度流程;
- 如果节点仍然满足条件,完成绑定;否则继续 Pending。
nominatedNodeName 是调度器的意图提示,不是最终绑定保证。它可能因为以下原因失效:
- 受害 Pod 尚未完全终止;
- 另一个高优先级 Pod 抢先占用了资源;
- 节点状态变化;
- 新的 Pod 被调度到该节点;
- 原来的过滤条件不再满足。
检查 Pending Pod:
kubectl get pod critical -n production \
-o jsonpath='{.status.phase}{"\n"}{.status.nominatedNodeName}{"\n"}'
检查事件:
kubectl describe pod critical -n production
常见事件包括:
Warning FailedScheduling ... 0/3 nodes are available: ...
Normal Scheduled ... Successfully assigned ...
被抢占的 Pod 通常可以在事件中看到类似 Preempted 的原因,但事件属于诊断信息,保留时间和具体文本不应作为长期审计依据。生产环境需要结合 API 审计日志、控制器事件、Pod 状态和调度器日志建立更可靠的追踪。
删除宽限期会影响恢复速度
抢占通常通过删除 Pod 触发终止流程。受害 Pod 的 terminationGracePeriodSeconds 较长时,资源释放和后续调度可能延迟。
这不表示可以把所有服务的终止宽限期改成极小值。宽限期承载连接排空、状态写回和进程清理等语义。真正需要评估的是:
- 服务能否安全处理中断;
- EndpointSlice 和负载均衡摘除是否及时;
- 应用是否支持优雅关闭;
- 抢占后的恢复时间是否满足业务目标。
抢占带来的可用性损失不能用“Pod 最终会重建”来抵消。
七、哪些 Pod 可以被抢占
默认情况下,候选受害者需要满足:
受害者优先级 < 抢占者优先级
但还有实际边界。
1. 不会抢占更高优先级 Pod
如果节点上只有:
system-node-critical:优先级 2000001000
critical:优先级 100000000
那么 critical 不能通过抢占系统 Pod 来获得资源。
这正是系统关键服务保护的基础:系统优先级需要高于普通业务优先级。
2. 低优先级 Pod 也可能不适合作为受害者
即使某个 Pod 优先级较低,调度器仍然要考虑抢占后的节点状态。例如驱逐它后,Pod Affinity、Anti-Affinity 或拓扑约束仍可能不满足,节点就不是有效候选。
3. 受害者可能不止一个
如果高优先级 Pod 请求 8 CPU,而节点上有多个低优先级 Pod,每个只占 1 CPU,调度器可能需要选择多个受害者。
因此,抢占造成的影响面可能大于“杀掉一个 Pod”:
- 多个服务同时重启;
- 多个控制器同时创建替代 Pod;
- 连接重试和日志量突然增加;
- 节点上的拓扑分布暂时改变。
4. DaemonSet Pod 不是天然不可抢占
DaemonSet 创建的 Pod 仍然是普通 Pod,是否会成为受害者取决于其优先级和其他调度条件。不能仅因为工作负载类型是 DaemonSet,就假设它不受抢占影响。
系统级 DaemonSet 通常应使用合适的系统优先级;业务 DaemonSet 则应根据其实际重要性配置。
八、PodDisruptionBudget 与抢占的关系
PodDisruptionBudget(PDB)用于约束一组 Pod 同时发生的自愿中断,例如:
- 节点排空;
- 集群维护;
- Cluster Autoscaler 缩容;
- 通过 Eviction API 发起的驱逐;
- 某些滚动更新场景中的可用性限制。
PDB 的典型定义如下:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: api-pdb
namespace: production
spec:
minAvailable: 2
selector:
matchLabels:
app: api
如果 api 当前有 3 个健康副本,minAvailable: 2 通常表示一次自愿驱逐最多让一个副本离开可用状态。
但抢占属于调度器为高优先级 Pod 释放资源而发起的破坏性动作,不应被理解为普通的自愿驱逐。Kubernetes 调度器会在选择抢占方案时尽量考虑 PDB,通常优先选择不违反 PDB 的受害者;如果没有其他可行方案,PDB 仍不能保证绝对阻止抢占。
因此下面这个结论是错误的:
配置了
minAvailable: 2,就保证永远不会少于两个副本。
更准确的说法是:
PDB 为特定的自愿中断建立可用性约束;它不是对节点故障、OOM、强制删除和所有抢占事件的绝对保护。
PDB 保护与 Priority 的组合
假设有三个 API Pod:
api-1:运行中
api-2:运行中
api-3:运行中
并配置:
minAvailable: 2
如果一个高优先级 Pod 需要抢占资源:
- 调度器会优先寻找不会突破 PDB 的受害者;
- 如果某个其他低优先级 Pod 可被驱逐,通常优先选择它;
- 如果只有 API Pod 能释放足够资源,抢占仍可能影响 API;
- 如果没有任何低优先级 Pod 可以移除,抢占不会凭空创造资源。
PDB 的 status 可以辅助判断当前允许的自愿中断数量:
kubectl get pdb api-pdb -n production -o yaml
关注:
status:
currentHealthy: 3
desiredHealthy: 2
disruptionsAllowed: 1
expectedPods: 3
这些字段反映的是 PDB 控制器观察到的状态,可能存在短暂延迟;不能把它当成抢占完成后的实时事务锁。
九、Priority 与资源请求必须一起设计
优先级只比较“重要程度”,调度能否成功还取决于资源请求。
考虑以下两个 Pod:
Pod A:
priority = 1000
requests.cpu = 8
Pod B:
priority = 100
requests.cpu = 1
在一个剩余 CPU 只有 2 的节点上:
- A 不能放入;
- B 可以放入;
- 如果没有可驱逐的低优先级 Pod,A 仍然 Pending。
这说明高优先级不是容量替代品。关键服务保护至少需要同时考虑:
- 合理的
requests; - 足够的节点容量;
- 正确的节点标签和拓扑;
- 节点池或专用节点;
- 必要时使用 Taint 和 Toleration;
- 副本数与 PDB;
- 优先级和抢占策略。
requests 过小也会制造错误保护
如果一个服务实际稳定使用 2 CPU,却只声明:
resources:
requests:
cpu: "100m"
调度器会认为它只占用 100m。大量类似 Pod 可能被安排到同一节点,最终在运行时发生 CPU 争抢或内存压力。
反过来,如果 requests 过大,服务可能因为难以找到完整资源块而频繁 Pending。Priority 不能修正错误的容量模型。
十、优先级不是公平性
“公平”至少有三种不同含义:
- 高优先级工作负载先获得调度机会;
- 不同 Namespace 或团队得到相对公平的资源份额;
- 同一服务的多个副本不会长期被某类任务挤压。
Priority 主要解决第一种,不自动解决后两种。
1. 低优先级工作负载可能饥饿
假设:
高优先级 Deployment:持续产生新 Pod
低优先级 Job:等待调度
如果集群长期接近满载,高优先级 Pod 不断进入队列,低优先级 Pod 可能长时间得不到调度机会。尤其是高优先级 Pod 还可以抢占低优先级 Pod 时,低优先级工作负载可能反复经历:
Pending → Scheduled → 被抢占 → 控制器重建 → Pending
这就是优先级导致的饥饿或抖动。
preemptionPolicy: Never 可以让某些高优先级任务优先排队,但不能让它们在没有资源时凭空运行;它通常比允许无限抢占更适合可延迟的批处理优先级。
2. Priority 不提供 Namespace 配额公平
如果两个团队都能使用同一个高优先级 PriorityClass,调度器看到的是 Pod 优先级,而不是团队身份。后创建的 Pod 也可能因为优先级更高而抢占另一团队的 Pod。
应使用 ResourceQuota 限制 Namespace 的资源总量,并在需要时按优先级设置配额范围。例如,集群可以针对某个 PriorityClass 配置资源配额范围:
apiVersion: v1
kind: ResourceQuota
metadata:
name: critical-quota
namespace: production
spec:
hard:
pods: "20"
requests.cpu: "20"
requests.memory: 40Gi
scopeSelector:
matchExpressions:
- operator: In
scopeName: PriorityClass
values:
- platform-critical
该配置的意图是限制 production Namespace 中使用 platform-critical 的 Pod 总量。实际使用前应通过集群版本文档和测试集群确认配额范围、准入策略及与现有 Quota 的组合方式。
3. 调度顺序不是完整的资源分配算法
调度器通常按优先级从队列取 Pod,但节点选择还要经过 Filter 和 Score。一个较低优先级 Pod 只要先被调度成功,并不代表它以后一定会被高优先级 Pod 抢占;只有高优先级 Pod 无法正常调度、且存在满足所有约束的抢占方案时,抢占才会发生。
因此,不能用“Pod 创建时间”或“节点当前 CPU 使用率”简单推断谁会被抢占。
十一、系统关键服务保护的分层方式
一个常见的优先级层级可以是:
系统节点级组件 system-node-critical
集群关键组件 system-cluster-critical
平台基础设施 platform-critical
在线核心服务 online-critical
普通在线服务 ordinary
后台批处理 batch-low
示例定义:
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: online-critical
value: 100000
globalDefault: false
preemptionPolicy: PreemptLowerPriority
description: "核心在线服务,可抢占普通后台工作负载"
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: batch-low
value: -100
globalDefault: false
preemptionPolicy: Never
description: "后台批处理,不抢占其他工作负载"
这套层级表达了三个不同政策:
- 系统组件不应被业务 Pod 抢占;
- 核心在线服务可以在资源紧张时优先恢复;
- 后台任务可以使用闲置资源,但不应破坏已有在线服务。
但“高优先级”不应成为所有问题的默认答案。对于真正关键的服务,还需要使用专用节点或节点池:
spec:
nodeSelector:
workload: online
tolerations:
- key: workload
operator: Equal
value: online
effect: NoSchedule
节点侧:
kubectl taint nodes node-online-1 \
workload=online:NoSchedule
这里的作用不同:
PriorityClass处理 Pod 之间的调度竞争;- Taint/Toleration 控制哪些 Pod可以进入节点;
- Node Affinity 或
nodeSelector控制 Pod必须进入哪些节点; - 专用节点提供容量和故障域隔离。
如果只设置高优先级,却没有为关键服务保留容量,抢占可能只是把普通服务换成关键服务,同时让系统整体进入频繁重启状态。
十二、抢占与拓扑约束的边界
抢占不能消除硬拓扑约束。
例如 Pod 要求:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- zone-a
如果 zone-a 中没有节点,抢占其他 Zone 的 Pod 没有意义。
对于 topologySpreadConstraints,调度器还必须考虑抢占后的副本分布是否满足拓扑要求。一个候选节点即使有足够 CPU,也可能因为会造成区域或节点分布不满足约束而不是有效候选节点。
这也是常见失败表现:
0/10 nodes are available:
3 node(s) didn't match Pod's node affinity/selector,
4 node(s) had untolerated taint,
3 node(s) insufficient cpu
其中只有 insufficient cpu 这类原因可能通过抢占解决;Affinity、Taint 或拓扑原因通常需要改变 Pod 约束、节点配置或容量,而不是继续提高优先级。
十三、如何配置并验证一个最小实验
下面的实验用于观察优先级和不可抢占策略。它要求:
- 集群中至少有一个可调度节点;
- 节点上有足够空间运行基础实验 Pod;
- 当前用户有创建 Namespace、PriorityClass、Deployment 和 Pod 的权限。
第一步:创建优先级类别
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: lab-high
value: 1000
globalDefault: false
preemptionPolicy: PreemptLowerPriority
description: "实验用高优先级"
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: lab-preferred
value: 500
globalDefault: false
preemptionPolicy: Never
description: "实验用高队列优先级,但不抢占"
应用:
kubectl apply -f priorityclass.yaml
kubectl get priorityclass lab-high lab-preferred
第二步:创建低优先级工作负载
apiVersion: apps/v1
kind: Deployment
metadata:
name: low
namespace: default
spec:
replicas: 3
selector:
matchLabels:
app: low
template:
metadata:
labels:
app: low
spec:
priorityClassName: lab-high
containers:
- name: pause
image: registry.k8s.io/pause:3.9
resources:
requests:
cpu: "500m"
memory: "256Mi"
为了测试抢占,实际实验中应让这些 Pod 的优先级低于测试 Pod。例如再创建一个更低的 PriorityClass:
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: lab-low
value: 10
globalDefault: false
preemptionPolicy: Never
description: "实验用低优先级"
然后把 Deployment 改成:
spec:
template:
spec:
priorityClassName: lab-low
应用修改:
kubectl apply -f low.yaml
kubectl rollout status deployment/low
第三步:创建高优先级 Pod
apiVersion: v1
kind: Pod
metadata:
name: high
namespace: default
spec:
priorityClassName: lab-high
restartPolicy: Never
containers:
- name: pause
image: registry.k8s.io/pause:3.9
resources:
requests:
cpu: "2"
memory: "512Mi"
如果节点资源不足但低优先级 Pod 释放后可以容纳它,可能观察到:
kubectl get pod -o wide
kubectl describe pod high
kubectl get events --sort-by=.lastTimestamp
被驱逐的低优先级 Pod 会由 Deployment 控制器重新创建。随后它们可能再次 Pending,直到集群有空闲容量。
第四步:测试 preemptionPolicy: Never
把高优先级 Pod 改为:
spec:
priorityClassName: lab-preferred
此时它的优先级仍高于 lab-low,但不会驱逐 lab-low Pod。如果资源不足,预期状态是:
high Pending
事件通常会说明资源不足,但不会出现成功抢占低优先级 Pod 的路径。
实验结束后清理:
kubectl delete pod high
kubectl delete deployment low
kubectl delete priorityclass lab-high lab-preferred lab-low
这个实验不应直接在生产集群进行。抢占会真实终止工作负载,且调度器、节点容量、镜像拉取和控制器恢复速度都会影响结果。
十四、生产诊断方法
1. 先确认 Pod 是否真的处于调度问题
kubectl get pod <pod-name> -n <namespace> -o wide
重点看:
STATUS是否为Pending;- 是否已经有
NODE; READY与RESTARTS;- 是否长期没有变化。
如果 Pod 已经有节点但容器没有运行,问题可能属于镜像拉取、挂载、探针、运行时或节点故障,不是调度优先级问题。
2. 查看调度事件
kubectl describe pod <pod-name> -n <namespace>
优先查看 Events。常见原因包括:
Insufficient cpu
Insufficient memory
node(s) didn't match Pod's node affinity/selector
node(s) had untolerated taint
didn't have free ports
volume node affinity conflict
只有资源不足且存在低优先级可驱逐 Pod 时,抢占才可能有效。
3. 检查优先级和抢占策略
kubectl get pod <pod-name> -n <namespace> \
-o jsonpath='priorityClassName={.spec.priorityClassName}{"\n"}priority={.spec.priority}{"\n"}'
kubectl get priorityclass <class-name> -o yaml
要确认:
- Pod 是否引用了预期的
PriorityClass; - 解析后的数值是否符合设计;
preemptionPolicy是否为Never;- 工作负载模板是否仍保留该配置。
4. 查看是否有 nominated node
kubectl get pod <pod-name> -n <namespace> \
-o jsonpath='{.status.nominatedNodeName}{"\n"}'
有值说明调度器曾经考虑过某个节点,但不代表抢占一定成功,也不代表 Pod 已绑定该节点。
5. 检查实际资源请求,而不是只看使用率
kubectl describe node <node-name>
观察:
Allocatable
Allocated resources
Allocated resources 主要反映调度器依据的请求分配情况。kubectl top node 显示的是运行时使用率,二者用途不同:
- 调度主要基于 requests 和节点可分配资源;
- 运行时压力还受 limits、QoS、内核回收和设备资源影响。
十五、常见误解与失败模式
误解一:提高优先级就能让 Pod 运行
错误。优先级不能解决:
- 没有符合 Affinity 的节点;
- 没有对应 GPU 或本地盘;
- Taint 没有容忍;
- Volume 拓扑不匹配;
- 请求的资源在所有节点都不存在;
- 节点数量不足。
正确诊断方式是先读调度事件,再判断失败原因是否属于可通过抢占缓解的资源冲突。
误解二:抢占只删除一个最低优先级 Pod
错误。调度器可能需要删除多个 Pod,且会综合考虑候选节点、受害者集合和约束。被选中的 Pod 不一定是全局优先级最低的那个,因为它还必须位于能够释放有效资源的节点上。
误解三:PDB 能阻止所有抢占
错误。PDB 主要约束自愿中断,调度器会尽量尊重它,但不能把 PDB 当成高优先级抢占的绝对防火墙。
误解四:Pod 已经被抢占就意味着高优先级 Pod 已经启动
错误。受害 Pod 删除和高优先级 Pod 绑定之间存在时间窗口。镜像拉取、终止宽限期、节点压力和再次过滤都可能让高优先级 Pod 继续 Pending。
误解五:设置同一个高优先级就能保护所有关键服务
错误。相同优先级之间不能互相抢占。若需要区分控制面、平台基础设施和核心业务,应使用分层优先级,或者结合专用节点和容量预留。
误解六:优先级可以替代容量规划
错误。优先级是一种竞争策略,不是容量。关键服务只有在节点、区域、存储和网络等资源真实存在时,才可能从抢占中获益。
误解七:滚动更新一定会遵守 PDB
PDB 和 Deployment 滚动更新关注的对象不同:
- PDB 主要控制自愿驱逐;
- Deployment 的
maxUnavailable和maxSurge控制滚动更新过程中的副本变化; - 抢占、节点故障和 OOM 可能造成 PDB 无法完全保护的中断。
三者需要联合设计,不能只设置其中一个就推断出整体可用性。
十六、优先级设计中的生产取舍
1. 为优先级定义有限且稳定的层级
优先级值不应按团队、应用实例或发布时间无限细分。过多层级会导致:
- 调度行为难以解释;
- 不同团队争夺更高数值;
- 审计和配额难以管理;
- 低优先级任务长期饥饿。
通常应先定义少量语义稳定的类别,例如系统、平台、核心在线、普通在线、后台批处理。
2. 限制谁可以使用高优先级
PriorityClass 本身是集群级对象,但 Pod 是否能引用某个名称还涉及准入和权限控制。生产环境可以使用:
- RBAC 限制对象创建和修改;
- ResourceQuota 限制高优先级 Pod 数量和资源;
- ValidatingAdmissionPolicy 或外部准入 Webhook 校验 Namespace 与 PriorityClass 的对应关系;
- 审计日志追踪谁创建了高优先级工作负载。
具体准入能力和表达式语法具有版本要求,部署时应按目标 Kubernetes 版本验证。
3. 关键服务优先考虑隔离,再考虑抢占
如果某服务必须稳定运行,优先顺序通常应是:
容量和故障域设计
→ 节点隔离与拓扑约束
→ 副本和 PDB
→ 合理 requests
→ PriorityClass
→ 必要时才使用抢占
抢占适合处理“资源暂时被低优先级工作负载占用”的情况,不适合代替多可用区部署、容量预留或故障恢复设计。
4. 评估被抢占者的恢复成本
一个无状态 Pod 被删除,可能只需要重新拉起容器;一个批处理任务被删除,可能导致整个作业重复;一个有本地缓存、长连接或昂贵初始化过程的服务,则可能产生更大代价。
配置抢占前应验证:
- 控制器是否会自动重建;
- 应用是否幂等;
- 是否会丢失本地状态;
- 终止信号是否能被正确处理;
- 重建后是否会造成连接风暴;
- 抢占频率是否可观测和可告警。
十七、关于调度器定制的版本边界
Kubernetes 支持通过调度器配置和调度插件调整调度行为。默认调度器通常使用优先级相关的队列排序和抢占插件,但自定义调度器、调度器 Profile 或插件配置可能改变实际行为。
因此,以下内容属于常见默认行为,而不是所有自定义调度器的绝对保证:
- 等待队列严格按优先级处理;
- 使用默认抢占插件;
- 使用默认的受害者选择和节点打分;
- 事件文本与默认调度器完全一致。
如果集群使用了:
- 自定义 scheduler;
- 多个 scheduler profile;
- 自定义
QueueSort; - 禁用或替换默认插件;
- 云厂商封装的调度策略;
应以实际调度器配置、版本文档和实验结果为准。尤其需要确认所有使用同一调度队列的 Pod 是否采用兼容的队列排序插件;调度器自定义不是 PriorityClass API 本身的语义延伸。
十八、一个可操作的设计检查
为一个高优先级在线服务设计调度策略时,可以按以下因果链验证:
-
它的优先级是否确实高于要保护的后台工作负载?
检查priorityClassName和解析后的.spec.priority。 -
它的 requests 是否真实反映启动和稳定运行所需资源?
如果 requests 错误,优先级比较没有可靠基础。 -
集群是否存在满足所有硬约束的节点?
检查节点标签、Taint、存储拓扑、区域和资源类型。 -
资源被低优先级 Pod 占用时,驱逐它们是否能使高优先级 Pod 可行?
如果约束本身不满足,抢占不会解决问题。 -
是否允许该高优先级 Pod 抢占?
查看preemptionPolicy。 -
被抢占的工作负载是否能安全恢复?
检查控制器、幂等性、PDB、终止流程和重建成本。 -
高优先级 Pod 是否可能持续产生并造成低优先级饥饿?
使用 ResourceQuota、限流、批处理并发控制和容量规划降低风险。 -
是否有可观测性验证策略生效?
记录 Pending 时长、调度失败原因、抢占事件、重建次数和nominatedNodeName变化。
最终,Priority 与 Preemption 应被看作一套“受约束的资源竞争政策”:优先级决定谁更早获得机会,抢占决定在特定条件下是否牺牲低优先级工作负载,PDB 和拓扑约束限制破坏范围,而容量、隔离和配额决定这套机制是否能够在生产环境中稳定运行。
系列导航与关联阅读
- 系列入口:Kubernetes 完整学习路线:从 Pod 与控制面到安全、运维和 Operator
- 上一篇:Kubernetes 调度约束:NodeSelector、Affinity、Taint、Topology 和 Spread
- 下一篇:Pod 拓扑设计:Zone、Node、故障域、反亲和和数据局部性
- 延伸:PodDisruptionBudget 与可用性:自愿中断、Eviction、滚动和误区
官方资料
本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。

评论
0 条讨论