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

这会带来两个实际含义:

  1. Namespace 管理员通常不能仅靠 Namespace 隔离优先级定义;
  2. 如果允许用户创建或引用任意高优先级,租户之间可能出现资源竞争和抢占。

生产环境通常应通过 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-criticalsystem-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

几个容易忽略的边界:

  1. PriorityClass 不存在时,引用它的 Pod 创建会失败,而不是创建一个“优先级未知”的 Pod。
  2. 修改或删除 PriorityClass 不会像修改 Deployment 模板那样自动重建已有 Pod。
  3. Deployment、StatefulSet、Job 等控制器会把模板中的优先级配置复制到新 Pod;已有 Pod 的优先级不会因为模板变化而原地修改。
  4. 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 仍可能被判定为不可调度。


五、抢占的形式化条件

设:

  • PP 是当前等待调度的 Pod;
  • priority(P)priority(P) 是其优先级;
  • nn 是一个候选节点;
  • VnV_n 是节点 nn 上可以被考虑驱逐的低优先级 Pod 集合;
  • R(P)R(P) 是 Pod 的资源请求;
  • F(n,X)F(n, X) 表示在节点 nn 上保留 Pod 集合 XX 时,所有调度过滤条件都满足;
  • XnX_n 是节点 nn 上原有的 Pod 集合。

如果存在某个节点 nn,使得:

F(n,XnVn{P})=trueF(n, X_n \setminus V_n \cup \{P\}) = true

并且对每个被驱逐的 Pod vv

priority(v)<priority(P)priority(v) < priority(P)

那么节点 nn 可能成为抢占候选节点。

这里的“过滤条件”不仅包括 CPU 和内存,还包括:

  • Node Selector;
  • Node Affinity;
  • Pod Affinity 和 Anti-Affinity;
  • Taint 与 Toleration;
  • 端口冲突;
  • Volume 拓扑;
  • Topology Spread Constraints;
  • 其他调度插件约束。

抢占的核心不是“找一个低优先级 Pod 删除”,而是:

  1. 找到一个原本不适合 PP 的节点;
  2. 选择一组低优先级 Pod 作为潜在受害者;
  3. 移除这些 Pod 后重新运行调度过滤逻辑;
  4. 只有过滤条件变为可行,节点才有抢占价值。

完整算例:资源抢占

集群有两个节点:

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-a1pod-a2 后释放 6 CPU,剩余 6 CPU,可容纳 critical
  • 被驱逐 Pod 的优先级都低于 100。

对 node-b:

  • 驱逐 pod-b1pod-b2 后释放 4 CPU,也可容纳 critical

调度器会综合候选节点和受害者,倾向于选择破坏性更小的方案,而不是简单地选择“优先级最低的节点”。实际选择还会受节点打分、Pod 优先级、Pod 数量、PDB 影响程度等因素影响;不能把所有版本和调度器配置下的精确选择顺序当作 API 保证。

同优先级 Pod 不会互相抢占

如果:

critical:优先级 100
peer:优先级 100

即使 peer 占用了 critical 所需的资源,critical 也不能因为同优先级而抢占 peer

这意味着优先级层级应当设计成有意义的分层,而不是把所有关键服务都设置成同一个高值后,期待它们之间仍然存在细粒度保护关系。


六、抢占的实际状态变化

抢占不是一个原子操作。典型过程如下:

  1. 高优先级 Pod 调度失败;
  2. 调度器分析节点并选择潜在受害者;
  3. 调度器向 API Server 发出删除受害 Pod 的请求;
  4. 受害 Pod 进入删除流程,可能处于 Terminating
  5. 高优先级 Pod 通常会记录 status.nominatedNodeName,表示调度器倾向于使用该节点;
  6. 等待资源真正释放、Pod 删除完成或集群状态变化;
  7. 高优先级 Pod 再次进入调度流程;
  8. 如果节点仍然满足条件,完成绑定;否则继续 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。

这说明高优先级不是容量替代品。关键服务保护至少需要同时考虑:

  1. 合理的 requests
  2. 足够的节点容量;
  3. 正确的节点标签和拓扑;
  4. 节点池或专用节点;
  5. 必要时使用 Taint 和 Toleration;
  6. 副本数与 PDB;
  7. 优先级和抢占策略。

requests 过小也会制造错误保护

如果一个服务实际稳定使用 2 CPU,却只声明:

resources:
  requests:
    cpu: "100m"

调度器会认为它只占用 100m。大量类似 Pod 可能被安排到同一节点,最终在运行时发生 CPU 争抢或内存压力。

反过来,如果 requests 过大,服务可能因为难以找到完整资源块而频繁 Pending。Priority 不能修正错误的容量模型。


十、优先级不是公平性

“公平”至少有三种不同含义:

  1. 高优先级工作负载先获得调度机会;
  2. 不同 Namespace 或团队得到相对公平的资源份额;
  3. 同一服务的多个副本不会长期被某类任务挤压。

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
  • READYRESTARTS
  • 是否长期没有变化。

如果 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 的 maxUnavailablemaxSurge 控制滚动更新过程中的副本变化;
  • 抢占、节点故障和 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 本身的语义延伸。


十八、一个可操作的设计检查

为一个高优先级在线服务设计调度策略时,可以按以下因果链验证:

  1. 它的优先级是否确实高于要保护的后台工作负载?
    检查 priorityClassName 和解析后的 .spec.priority

  2. 它的 requests 是否真实反映启动和稳定运行所需资源?
    如果 requests 错误,优先级比较没有可靠基础。

  3. 集群是否存在满足所有硬约束的节点?
    检查节点标签、Taint、存储拓扑、区域和资源类型。

  4. 资源被低优先级 Pod 占用时,驱逐它们是否能使高优先级 Pod 可行?
    如果约束本身不满足,抢占不会解决问题。

  5. 是否允许该高优先级 Pod 抢占?
    查看 preemptionPolicy

  6. 被抢占的工作负载是否能安全恢复?
    检查控制器、幂等性、PDB、终止流程和重建成本。

  7. 高优先级 Pod 是否可能持续产生并造成低优先级饥饿?
    使用 ResourceQuota、限流、批处理并发控制和容量规划降低风险。

  8. 是否有可观测性验证策略生效?
    记录 Pending 时长、调度失败原因、抢占事件、重建次数和 nominatedNodeName 变化。

最终,Priority 与 Preemption 应被看作一套“受约束的资源竞争政策”:优先级决定谁更早获得机会,抢占决定在特定条件下是否牺牲低优先级工作负载,PDB 和拓扑约束限制破坏范围,而容量、隔离和配额决定这套机制是否能够在生产环境中稳定运行。


系列导航与关联阅读

官方资料

本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。