原文发布于 quant67.com,转载请保留出处。
本系列前面的篇章跟踪的是已经落在某台节点上的包 :veth、CNI、Service、NetworkPolicy。但有一类事故根本走不到抓包:Pod 长期 Pending,事件里写着 0/N nodes are available。包路径不存在,是因为调度器从未把 Pod 绑到任何节点。
本文回答一个更靠前的问题:nodeSelector、nodeAffinity、topologySpreadConstraints 和污点容忍各自解决什么,kube-scheduler 在 Filter / Score 里怎么用节点标签,以及为什么「标签写对了仍然 Pending」。
它挂在网络系列末尾,不是因为调度属于数据面,而是因为:没有合法落点,就没有节点上的网络栈可讲。 排查「Service 不通」之前,应先确认 Pod 是否已经 Running 且 nodeName 非空。
本文依据 :Kubernetes 官方文档 Assigning Pods to Nodes 、Taints and Tolerations 、Pod Topology Spread Constraints ;调度器插件源码路径以 kubernetes/kubernetes 主线
pkg/scheduler/framework/plugins/nodeaffinity为准。行为描述对齐官方语义;未做本机 kind 实测数字,不写吞吐/延迟 benchmark。
一、先给结论:四套旋钮各管什么
| 机制 | 作用对象 | 硬/软 | 调度器阶段(默认画像) | 典型用途 |
|---|---|---|---|---|
spec.nodeSelector |
节点标签 | 硬:全部 key=value 必须匹配 | Filter(NodeAffinity 插件一并处理节点选择) |
最简单的「只能去带某标签的节点」 |
affinity.nodeAffinity |
节点标签 | 硬(required...)或软(preferred...) |
Filter + Score | 多值、操作符、In/NotIn、偏好权重 |
tolerations + 节点 taints |
节点污点 | NoSchedule 硬拒;PreferNoSchedule 软;NoExecute 还可驱逐 |
Filter(TaintToleration) |
专用节点、GPU 节点、不可调度/故障驱逐 |
topologySpreadConstraints |
已有 Pod 在拓扑域的分布 | 可硬可软(whenUnsatisfiable) |
Filter / Score(PodTopologySpread) |
跨 zone / hostname 打散,降相关故障 |
官方文档把「把 Pod 放到特定节点」的推荐入口写成:用标签选择器 ,而不是手写 spec.nodeName(后者绕过调度器,也绕过大部分安全与容量检查)。见 Assigning Pods to Nodes。
二、nodeSelector:最简单的硬约束
2.1 语义
nodeSelector 是 PodSpec 上的一张 map[string]string。调度器只把 Pod 放到同时拥有每一个所列标签键值 的节点上。文档原话:Kubernetes only schedules the Pod onto nodes that have each of the labels you specify(Assigning Pods to Nodes · nodeSelector)。
yaml
apiVersion: v1
kind: Pod
metadata:
name: gpu-job
spec:
nodeSelector:
accelerator: nvidia
topology.kubernetes.io/zone: zone-a
containers:
- name: train
image: example/train:1.0
含义:节点必须同时有 accelerator=nvidia 且 topology.kubernetes.io/zone=zone-a。任意一个缺失 → 该节点在 Filter 阶段被否决。
2.2 它解决什么、不解决什么
- 适合:标签字典稳定、约束是「必须等于」的合取(AND)、不想写 affinity 语法。
- 不适合 :
In多个 zone、NotIn、软偏好、按其他 Pod 共置------这些需要nodeAffinity/ 拓扑打散 / inter-pod affinity。
官方明确:nodeSelector 是最简单的推荐形式;亲和性语言更表达力强,且可以声明 soft / preferred 规则(同上文档 Affinity and anti-affinity 一节)。
2.3 与 nodeAffinity 同时出现时
若同时指定 nodeSelector 与 nodeAffinity,两者都必须满足才能调度到该节点(官方 Note)。不要假设 affinity 会「覆盖」selector。
三、nodeAffinity:硬约束 + 软偏好 + 操作符
3.1 两种时机语义
节点亲和性概念上类似 nodeSelector,但多了表达力与软规则。官方定义两类(名称里的 IgnoredDuringExecution 表示:调度完成后节点标签变化,默认不因此驱逐已运行 Pod):
requiredDuringSchedulingIgnoredDuringExecution:不满足则不能调度(硬,Filter)。preferredDuringSchedulingIgnoredDuringExecution:尽量满足;找不到匹配节点仍可调度(软,Score)。
3.2 合取与析取
官方规则(必须按此理解 Pending 原因):
- 同一
nodeSelectorTerms里多个 term:OR(满足任一 term 即可)。 - 同一 term 的
matchExpressions里多个表达式:AND。 operator支持In、NotIn、Exists、DoesNotExist、Gt、Lt。NotIn/DoesNotExist可表达节点反亲和;也可用污点排斥(见第四节)。
yaml
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values: ["antarctica-east1", "antarctica-west1"]
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
preference:
matchExpressions:
- key: another-node-label-key
operator: In
values: ["another-node-label-value"]
3.3 调度器插件落点(A 级:源码路径)
默认调度画像里,节点选择与亲和由 NodeAffinity 插件实现:
- 源码目录:
pkg/scheduler/framework/plugins/nodeaffinity/(例如node_affinity.go)。 - 插件注释写明:检查 Pod 的 node selector 是否与节点标签匹配,并处理
.spec.affinity.nodeAffinity。 - Filter :强制
required...与(通过该插件路径处理的)节点选择约束。 - PreScore / Score :累加
preferred...各项weight;再经 NormalizeScore 缩放到框架约定区间。
因此搜索词里的「nodeSelector」在实现上并不是一个孤立的小函数,而是调度框架里 Filter 否决 + Score 加权 的同一插件族职责。软偏好只影响排名,不单独保证落点;集群自动扩缩容(Cluster Autoscaler)对 soft 约束的模拟也不完整------这是运维上反复踩坑的一点(见第八节开放问题)。
四、污点与容忍:排斥,不是「另一种 nodeSelector」
4.1 方向相反
官方一句话对照(Taints and Tolerations):
- Node affinity :Pod 被吸引到一组节点(硬或软)。
- Taints :节点排斥 一组 Pod;Pod 用 tolerations 声明「我能忍受这种排斥」。
常见混淆:给 GPU 节点打了 accelerator=nvidia 标签并写了 nodeSelector,但节点还有 NoSchedule 污点,而 Pod 没有对应 toleration → Filter 仍失败。标签匹配与污点过滤是两条独立否决链。
4.2 三种 effect
| effect | 调度含义 | 对已运行 Pod |
|---|---|---|
NoSchedule |
无匹配 toleration 则不调度上去 | 不驱逐已在节点上的 Pod |
PreferNoSchedule |
尽量不调度;非保证 | 不驱逐 |
NoExecute |
不调度;且可驱逐已运行且不匹配的 Pod | 可立即驱逐或按 tolerationSeconds |
多污点 / 多容忍的处理方式像过滤器:先去掉 Pod 能容忍的污点,看剩余污点的 effect(官方文档同页)。
4.3 专用节点与特殊硬件
官方用例组合往往是:污点把无关工作挡在外面 + 标签 / affinity 把目标工作吸进去 。例如专用节点:taint dedicated=groupName:NoSchedule,再给对应工作负载加 toleration,并通常再加 dedicated=groupName 标签与 affinity,避免「有 toleration 的 Pod 仍铺满整集群」。GPU 场景同理:污点保护稀缺硬件,Extended Resource + 准入控制器可自动注入 toleration(官方 Nodes with Special Hardware)。
五、topologySpreadConstraints:打散,不是选标签字典
拓扑打散约束回答的问题不同:在某个拓扑键(如 topology.kubernetes.io/zone、kubernetes.io/hostname)上,带某 labelSelector 的 Pod 分布是否够均匀。 官方文档:Pod Topology Spread Constraints。
与 nodeSelector 的对比:
| nodeSelector / required affinity | topologySpread | |
|---|---|---|
| 输入 | 「节点必须长什么样」 | 「同类 Pod 在拓扑域里差多少」 |
| 失败形态 | 没有带齐标签的节点 | DoNotSchedule 时域间 skew 超标 |
| 常见目标 | 绑 GPU / 绑机房 | 降单 AZ 故障面、防堆在同一主机 |
whenUnsatisfiable: DoNotSchedule 是硬约束;ScheduleAnyway 是软约束(仍参与打分)。maxSkew、minDomains、nodeAffinityPolicy / nodeTaintsPolicy 等字段会改变「哪些节点算进拓扑计数」------写打散规则时若同时叠了很严的 nodeSelector,可行域可能被裁成空集。
六、Filter 之后还有容量:为什么「标签对了」仍 Pending
即使 nodeSelector 与亲和、污点全部通过,NodeResourcesFit 仍会否决 CPU / 内存 / 扩展资源(如 GPU)不够的节点;默认打分常偏向剩余资源更多的节点(LeastAllocated 等策略,属 NodeResourcesFit Score 配置)。官方调度框架文档与默认插件列表见 Scheduler Configuration。
排查顺序建议:
kubectl describe pod:看FailedScheduling事件里是 node(s) didn't match Pod's node affinity/selector 、had untolerated taint 、Insufficient cpu/memory ,还是 didn't match pod topology spread constraints。kubectl get nodes --show-labels:核对选择器键值是否真的存在(拼写、zone 名、自定义标签是否被 NodeRestriction 管住)。kubectl describe node:污点、Allocatable、已分配 requests。- 确认没有把「软偏好」误当成硬保证;也没有同时 AND 过多硬约束把可行集打空。
七、常见踩坑清单
- 把污点当成标签。
kubectl taint不会自动产生可被nodeSelector匹配的标签;反之亦然。 nodeSelector与requiredaffinity 叠得过死。 两者都要满足;再叠加DoNotSchedule打散,Pending 概率陡增。- 信任可被 kubelet 改写的标签做隔离。 官方建议隔离场景使用 kubelet 不能 修改的键;
node-restriction.kubernetes.io/前缀需配合 Node 鉴权与NodeRestriction准入(Assigning Pods to Nodes · Node isolation)。 - 手写
spec.nodeName。 绕过调度器;即使节点有NoSchedule污点也可能被绑上;若还有NoExecute,kubelet 仍可能驱逐(Taints 文档说明)。 - 用 preferred 表达「必须」。 Score 阶段失败不会让 Pod Pending;只会落到次优节点------直到你误以为策略生效。
- 忽略
IgnoredDuringExecution。 节点标签事后被改掉,已运行 Pod 默认不搬迁;需要驱逐要用污点NoExecute、驱逐 API 或工作负载控制器重建。
八、谱系、争论与开放问题
谱系 :早期 Kubernetes 用简单的节点选择与优先级函数;调度框架(Scheduling Framework)把 Filter / Score / Bind 等扩展点插件化后,NodeAffinity、TaintToleration、PodTopologySpread、NodeResourcesFit 成为默认可组合策略。标签选择来自 Borg / Omega 一脉的「约束声明 + 中心调度」传统,但具体插件集合与拓扑打散语义是 Kubernetes 社区独立演进的结果。
工程争论:
- 硬约束过多 vs 软偏好过多:硬约束保证正确落点,却提高 Pending 与扩容失败率;软偏好提高调度成功率,却让「策略是否生效」难以从单次落点证明。
- Cluster Autoscaler 与 soft 约束 :CA 模拟调度时通常只完整考虑 Filter 类硬约束;
preferredDuringScheduling...落在 Score,扩容决策可能看不到你的软亲和。运维上若依赖 CA,关键放置应用硬约束或专用节点池,而不是只写 weight。
开放问题:
- 多租户下「标签可信根」:在 NodeRestriction 与云厂商自动标签之外,如何低成本证明节点隔离标签未被控制面配置错误扩大攻击面,仍缺统一标准答案。
- 拓扑打散与多调度器 / 队列:在 Gang scheduling、批量作业与默认调度器并存时,skew 的全局语义如何定义,仍是插件与 out-of-tree 调度器各自为政。
- 动态标签(例如随硬件热插拔变化)与
IgnoredDuringExecution的间隙:何时应自动重调度,社区倾向显式驱逐而非静默搬迁,产品化策略未收敛。
九、和本系列其余篇的关系
- 落点成功之后,节点上的包路径从 K8s 网络模型、CNI、Service / kube-proxy 继续。
Pending被误诊为「网络不通」时,回到 疑难杂症排查手册 之前,先用本文第六节排除调度否决。- 跨集群、跨云的「落在哪」会再叠一层联邦与全局负载均衡,见 多集群网络 与 跨云网络与联邦;那些层解决的是集群间可达,不替代单集群内的
nodeSelector。
参考资料
规范 / 官方文档
- Assigning Pods to Nodes ---
nodeSelector、nodeAffinity、nodeName、拓扑打散入口。 - Taints and Tolerations
- Pod Topology Spread Constraints
- Scheduler Configuration / Scheduling plugins --- 插件与扩展点对照。
源码
k8s.io/kubernetes/pkg/scheduler/framework/plugins/nodeaffinity--- Filter / PreScore / Score 实现节点选择与亲和。- 同树
plugins/tainttoleration、plugins/podtopologyspread、plugins/noderesources--- 污点、打散、资源 Fit。
工程判断(非 A 级单独结论)
- Cluster Autoscaler 对 Score 类 soft 约束覆盖不完整:排障时以硬约束与节点池设计为准,不以 preferred weight 作为容量规划唯一依据。