3.3. 调度策略:亲和、污点与优先级

调度器先按硬约束过滤节点,再按软偏好打分,最后绑定。亲和性、污点、拓扑分布和优先级分别作用在这条链路的不同阶段,本章把默认调度器拆开,用可复现的实验观察每个开关的效果。

层级:

L3 · 平台

难度:

进阶

预计:

90 分钟

前置:

Pod 与容器、工作负载、节点与标签

3.3.1. 你将学到

  • kube-scheduler 的调度周期与绑定周期,Filter、Score、Bind 各自解决什么问题。

  • nodeSelector、节点亲和、Pod 亲和/反亲和、污点与容忍的适用边界。

  • 用 topologySpreadConstraints 控制副本在节点与可用区之间的分布。

  • PriorityClass 与抢占的行为,以及如何配置第二个调度器。

3.3.2. 调度器在做什么

kube-scheduler 只处理 spec.nodeName 为空的 Pod。它从 watch 流里拿到新 Pod 后进入调度周期:排序、预过滤、过滤、打分、预留、发放许可,最后在 Bind 阶段把节点写回 Pod。调度周期串行执行,绑定周期可以异步,前一个 Pod 还在写结果时,后一个已经开始筛选节点。

调度框架把每个阶段做成可插拔扩展点。Filter 插件只回答“这个节点行不行”,例如 NodeAffinity、TaintToleration、NodeResourcesFit、PodTopologySpread;Score 插件给可行节点打 0–100 分并按权重汇总,例如 NodeResourcesBalancedAllocation、ImageLocality、InterPodAffinity。所有节点都被过滤掉时,PostFilter 才运行,默认实现就是抢占。

架构图

图:Filter 与 Score 是默认路径,抢占只在没有可行节点时触发。

3.3.3. 把 Pod 引导到节点

3.3.3.1. nodeSelector:最简单的标签匹配

nodeSelector 是键值对集合,节点必须同时拥有这些标签且值完全相等,没有 In、Exists 操作符,也不能表达“尽量”。仓库里 pod-with-nodeselector.yaml 写的是 gpu: '',要求标签值恰好为空字符串;用 kubectl label node k8s-node01 gpu= 才能匹配,写成 gpu: 'true' 则对应非空值。这个细节很容易让 Pod 无事件地 Pending。

3.3.3.2. 节点亲和:硬约束与软偏好

节点亲和用 matchExpressions 表达 In、NotIn、Exists、DoesNotExist、Gt、Lt。requiredDuringSchedulingIgnoredDuringExecution 是硬约束,preferredDuringSchedulingIgnoredDuringExecution 是软偏好,每项权重 1–100。一个 nodeSelectorTerms 列表内的多个 term 是 OR,一个 term 内的多条表达式是 AND,这决定了“GPU 或 SSD”与“GPU 且非控制平面”的写法完全不同。IgnoredDuringExecution 表示调度完成后节点标签变化不会驱逐 Pod,要在运行时强制只能靠 NoExecute 污点。preferred 的分数叠加到节点总分上;原书的 node-affinity-preferred-demo.yaml 给 gpu 权重 60、region in (foo,bar) 权重 30,同时 requests 1.5 CPU / 1Gi,用来观察“偏好被资源不足压过”。

3.3.3.3. Pod 亲和与反亲和

Pod 亲和回答“我能不能和某类 Pod 待在同一拓扑域”,反亲和回答“我能不能和某类 Pod 待在一起”。topologyKey 指定拓扑域的节点标签,常用 kubernetes.io/hostname(一台节点一个域)和 topology.kubernetes.io/zone(一个可用区一个域)。

  • required 亲和:目标 Pod 不存在时新 Pod 一直 Pending,适合强耦合,但会引入启动顺序死锁。

  • required 反亲和:常用于每节点只放一个副本;副本数大于可用域数量时,多出的副本必然 Pending。

  • preferred:只加分,不阻塞调度。原书反亲和示例用 required,4 个副本在有 4 个以上节点的集群上刚好每节点一个。

labelSelector 为空或匹配范围过宽时,反亲和会误伤同命名空间的全部 Pod;跨命名空间匹配要显式写 namespaces 或 namespaceSelector。

3.3.3.4. 污点与容忍:节点主动拒绝

污点写在节点上,容忍写在 Pod 上,方向与亲和相反。格式是 key=value:Effect:

Effect

对新 Pod 的影响

对已运行 Pod 的影响

NoSchedule

不容忍则不调度

不驱逐

PreferNoSchedule

尽量不调度,资源紧张时仍可能落上去

不驱逐

NoExecute

不容忍则不调度

不容忍则驱逐,可用 tolerationSeconds 延迟

添加用 kubectl taint node k8s-node01 dedicated=gpu:NoSchedule,删除时在末尾加 -。容忍的 operator 默认是 Equal,要求 key 和 value 都匹配;Exists 只要求 key 存在。Kubernetes 自带 not-ready、unreachable、memory-pressure、disk-pressure 等污点,控制平面节点在 v1.31 的污点是 node-role.kubernetes.io/control-plane:NoSchedule。

3.3.3.5. 拓扑分布约束

topologySpreadConstraints 按 topologyKey 把节点划分成域,计算每个域中匹配 labelSelector 的 Pod 数量,要求任意两个域的差值不超过 maxSkew。whenUnsatisfiable: DoNotSchedule 是硬约束,ScheduleAnyway 只影响打分。默认调度器还会给 Service 和 ReplicaSet 注入按主机名分布的隐式约束,显式写出来是为了控制分布维度和是否强制。

3.3.3.6. 优先级与抢占

PriorityClass 是集群级对象,value 越大优先级越高,上限 10 亿。系统保留 system-cluster-critical(2000000000)和 system-node-critical(2000001000);globalDefault: true 的类会成为默认值,但只能有一个。高优先级 Pod 找不到可行节点时,PostFilter 会尝试驱逐某个节点上的低优先级 Pod。抢占会尽量尊重 PDB,但只是“尽力”,没有别的选择时仍可能打破预算。被驱逐的 Pod 进入终止流程,高优先级 Pod 的状态里出现 nominatedNodeName;把 preemptionPolicy 设为 Never 可以得到“高优先级但不抢占”的效果。

3.3.4. 自定义调度器配置

从 v1.25 起稳定版本是 kubescheduler.config.k8s.io/v1,配置里可以定义多个 profile,schedulerName 相同的 Pod 由对应 profile 处理。原书使用的 v1alpha1/v1alpha2 以及 policy.cfg 早已移除,policy 文件在 v1.23 被删除,继续照抄会直接启动失败。

kube-scheduler-config.yaml
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
clientConnection:
  kubeconfig: /etc/kubernetes/scheduler.conf
profiles:
- schedulerName: default-scheduler
- schedulerName: demo-scheduler
  plugins:
    score:
      disabled:
      - name: NodeResourcesLeastAllocated
      enabled:
      - name: NodeResourcesMostAllocated
        weight: 5

第二个调度器以独立 Deployment 运行,挂载自己的 kubeconfig 与 ConfigMap,启动参数加 --config=/etc/kubernetes/scheduler/config.yaml,并设置独立的 leaderElection.resourceName。Pod 写 schedulerName: demo-scheduler 才会交给它;该调度器没运行时,Pod 会一直 Pending,且事件里看不到正常的调度失败原因。

3.3.5. 动手实验

实验一:节点亲和与污点容忍

目标:只让带 gpu=true 的节点接收 demoapp,同时容忍该节点的专用污点。

步骤 1 · 给节点打标签并加污点

kubectl label node k8s-node01 gpu=true
kubectl taint node k8s-node01 dedicated=gpu:NoSchedule

步骤 2 · 创建带亲和与容忍的 Deployment

scheduling-node-affinity.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demoapp-gpu
spec:
  replicas: 2
  selector:
    matchLabels: {app: demoapp-gpu}
  template:
    metadata:
      labels: {app: demoapp-gpu}
    spec:
      tolerations:
      - {key: dedicated, operator: Equal, value: gpu, effect: NoSchedule}
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - {key: gpu, operator: In, values: ["true"]}
              - {key: node-role.kubernetes.io/control-plane, operator: DoesNotExist}
      containers:
      - name: demoapp
        image: ikubernetes/demoapp:v1.0
kubectl apply -f scheduling-node-affinity.yaml
kubectl get pod -l app=demoapp-gpu -o wide

两个副本都应落在 k8s-node01。把 tolerations 整段删掉再 apply,新 Pod 会因为污点被过滤,事件为 untolerated taint {dedicated: gpu}。随后给节点换一个标签值,已运行的 Pod 不会迁移,这正好验证 IgnoredDuringExecution 的边界。

实验二:跨可用区拓扑分布

目标:6 个副本在三个可用区之间保持 maxSkew: 1。

步骤 1 · 给节点标注可用区

kubectl label node k8s-node01 topology.kubernetes.io/zone=zone-a
kubectl label node k8s-node02 topology.kubernetes.io/zone=zone-b
kubectl label node k8s-node03 topology.kubernetes.io/zone=zone-c

步骤 2 · 创建 Deployment

scheduling-spread.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: spread-demo
spec:
  replicas: 6
  selector:
    matchLabels: {app: spread-demo}
  template:
    metadata:
      labels: {app: spread-demo}
    spec:
      topologySpreadConstraints:
      - maxSkew: 1
        topologyKey: topology.kubernetes.io/zone
        whenUnsatisfiable: DoNotSchedule
        labelSelector:
          matchLabels: {app: spread-demo}
      containers:
      - name: demoapp
        image: ikubernetes/demoapp:v1.0
kubectl apply -f scheduling-spread.yaml
kubectl get pod -l app=spread-demo \
  -o custom-columns=NAME:.metadata.name,NODE:.spec.nodeName --no-headers | sort -k2

三个 zone 各两台节点时,副本应保持每区 2 个。若节点缺少 topology.kubernetes.io/zone 标签,DoNotSchedule 会让 Pod 直接 Pending;把 whenUnsatisfiable 改成 ScheduleAnyway 可以先用软约束观察分布,再收紧。

3.3.5.1. 验证清单

  • kubectl get pod -o wide 中,实验一的副本只在带 gpu=true 的节点上。

  • 删除容忍后重建 Pod,事件出现 untolerated taint,而不是笼统的 Pending。

  • 实验二的副本在三个 zone 间分布差值不超过 1。

3.3.6. 常见问题与排错

现象

可能原因

处理方式

Pending,事件 node(s) didn't match Pod's node affinity/selector

标签键值不匹配,或 gpu: '' 与 gpu=true 混用

kubectl get node --show-labels 核对;用 kubectl describe pod 看完整事件

删除容忍后 Pod 仍能运行

污点只影响新调度,旧 Pod 不会迁移

删掉旧 Pod 重建;确认 Effect 是 NoSchedule 而非 PreferNoSchedule

带 required 反亲和的 Deployment 扩容后 Pending

副本数超过可用拓扑域数量

增加节点/可用区,或改成 preferred、放宽 topologyKey

拓扑分布一直 Pending

节点缺少 topologyKey 标签,或 maxSkew 设得过小

给节点补标签;先用 ScheduleAnyway 观察分布再收紧

高优先级 Pod 没有抢占

所有节点都有同等或更高优先级 Pod,或 PDB 阻止驱逐

看 nominatedNodeName 与调度器日志;临时降低 PDB 约束验证

3.3.7. 进阶挑战

  1. 用 PriorityClass 加两个 Deployment 做抢占实验:先让低优先级 Pod 占满单节点,再创建高优先级 Pod,观察受害者、nominatedNodeName 和 PDB 的作用。

  2. 给 demoapp 同时加节点亲和、Pod 反亲和和 zone 级拓扑分布,构造“三个约束互相冲突”的场景,写出每种 Pending 事件对应的约束。

3.3.8. 小结

  • 调度分成过滤、打分、绑定三个阶段;硬约束决定“能不能放”,软偏好和权重决定“放哪更好”。

  • nodeSelector 只做等值匹配;节点亲和补上操作符与软硬之分;Pod 亲和/反亲和以 topologyKey 为域。

  • 污点是节点主动排斥,容忍是 Pod 的豁免;NoExecute 还会驱逐已运行的 Pod。

  • 拓扑分布控制域间差值,优先级与抢占处理资源竞争;自定义调度器的稳定配置版本是 v1。

3.3.9. 延伸阅读