本系列前面的篇章跟踪的是已经落在某台节点上的包: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。
flowchart TD
pod[Pod 进入调度队列]
pre[PreFilter 预处理约束]
filter[Filter 否决不可行节点]
score[Score 给可行节点打分]
bind[Bind 写入 nodeName]
pod --> pre --> filter --> score --> bind
filter --> ns[NodeAffinity: nodeSelector 与 required affinity]
filter --> tt[TaintToleration]
filter --> fit[NodeResourcesFit]
filter --> pts[PodTopologySpread]
score --> pref[preferred nodeAffinity 权重]
score --> other[资源打分 / 镜像本地性等]
二、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)。
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可表达节点反亲和;也可用污点排斥(见第四节)。
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 作为容量规划唯一依据。
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
Kubernetes 网络深度系列
从 Linux 网络栈的第一个字节到多集群联邦网络的最后一跳——用代码、抓包和内核源码把 Kubernetes 网络讲透
【Kubernetes 网络深度系列】Cilium 深度拆解:eBPF 原生的云原生网络
从 eBPF 数据面到 Identity 安全模型,全面拆解 Cilium 的架构与实现
【Kubernetes 网络深度系列】Kubernetes 网络模型:三条规则统治一切
K8s 网络三条铁律、pause 容器的真实作用、Pod 网络命名空间的创建全过程
【Kubernetes 网络深度系列】CNI 规范拆解:一个 JSON 配置怎么变成网络
从 CNI 规范的四个操作到手写一个最简 CNI 插件,理解 K8s 网络的插件化机制