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 的影响 |
|---|---|---|
|
不容忍则不调度 |
不驱逐 |
|
尽量不调度,资源紧张时仍可能落上去 |
不驱逐 |
|
不容忍则不调度 |
不容忍则驱逐,可用 |
添加用 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 被删除,继续照抄会直接启动失败。
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
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
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,事件 |
标签键值不匹配,或 |
|
删除容忍后 Pod 仍能运行 |
污点只影响新调度,旧 Pod 不会迁移 |
删掉旧 Pod 重建;确认 Effect 是 |
带 required 反亲和的 Deployment 扩容后 Pending |
副本数超过可用拓扑域数量 |
增加节点/可用区,或改成 |
拓扑分布一直 Pending |
节点缺少 |
给节点补标签;先用 |
高优先级 Pod 没有抢占 |
所有节点都有同等或更高优先级 Pod,或 PDB 阻止驱逐 |
看 |
3.3.7. 进阶挑战
用
PriorityClass加两个 Deployment 做抢占实验:先让低优先级 Pod 占满单节点,再创建高优先级 Pod,观察受害者、nominatedNodeName和 PDB 的作用。给 demoapp 同时加节点亲和、Pod 反亲和和 zone 级拓扑分布,构造“三个约束互相冲突”的场景,写出每种 Pending 事件对应的约束。
3.3.8. 小结
调度分成过滤、打分、绑定三个阶段;硬约束决定“能不能放”,软偏好和权重决定“放哪更好”。
nodeSelector 只做等值匹配;节点亲和补上操作符与软硬之分;Pod 亲和/反亲和以
topologyKey为域。污点是节点主动排斥,容忍是 Pod 的豁免;
NoExecute还会驱逐已运行的 Pod。拓扑分布控制域间差值,优先级与抢占处理资源竞争;自定义调度器的稳定配置版本是
v1。
3.3.9. 延伸阅读
资源请求与 HPA 的基础见调度、资源与弹性。