一个集群里混着CPU通用节点、带GPU的训练节点、大内存的缓存节点,这在实际生产环境里非常常见。如果不对调度做任何约束,Kubernetes默认调度器只看剩余资源量,很可能把一个普通Web服务调度到昂贵的GPU机器上,把一个吃显存的训练任务挤到CPU节点上反复失败。要解决这类问题,核心思路是给节点“打标”,让调度器在选点时有据可依。本文将从标签体系、污点与容忍度、节点池管理以及调度实践几个层面,详细讲解异构集群下的分组与调度方案。

一、标签与nodeSelector:最基础的分组手段
Kubernetes中一切分组的起点都是Label(标签)。标签是简单的键值对,可以随意挂在节点上,比如kubernetes.io/arch=amd64、hardware-type=gpu、memory-class=high。节点出厂时通常自带一些内置标签,例如kubernetes.io/hostname、beta.kubernetes.io/instance-type(云厂商节点),这些天然就是可用的分组维度。
给节点打标签的命令很简单:
kubectl label nodes gpu-node-01 hardware-type=gpu kubectl label nodes mem-node-01 memory-class=high
有了标签之后,Pod侧最直接的使用方式是nodeSelector,在PodSpec里指定nodeSelector: {hardware-type: gpu},调度器就只会把该Pod放到带有这个标签的节点上。它的优点是配置极简、语义清晰,适合小规模集群或者分组需求不复杂的场景。
但nodeSelector的能力是“硬性的”,只要不满足就完全不调度,Pod会一直Pending,而且无法表达“优先选A组、A满了再选B组”这种软性偏好。它的匹配能力也只支持等值判断,不支持集合运算。所以在异构集群规模变大之后,一般会升级到后面要讲的节点亲和性。
二、污点与容忍度:把不该来的Pod挡在外面
标签解决的是“我想去哪”,而污点(Taint)解决的是“谁可以来”。两者方向相反,配合起来才完整。给GPU节点打上污点,等于在节点上立了一块牌子:没有对应容忍度(Toleration)的Pod一律不许调度进来。这样即使某个工作负载忘了写nodeSelector,也不会意外占用GPU资源。
典型用法如下:
# 给GPU节点打污点:不调度不容忍的Pod,已有Pod不受影响 kubectl taint nodes gpu-node-01 hardware=gpu:NoSchedule # 想驱逐已有的Pod,使用NoExecute kubectl taint nodes gpu-node-01 hardware=gpu:NoExecute
Pod侧需要声明容忍度才能落在该节点上:
apiVersion: v1
kind: Pod
metadata:
name: train-job
spec:
tolerations:
- key: "hardware"
operator: "Equal"
value: "gpu"
effect: "NoSchedule"
containers:
- name: trainer
image: my-training-image污点的effect有三种:NoSchedule表示新Pod不能调度、已有Pod保留;PreferNoSchedule是软性版本,尽量不调度但资源紧张时可以妥协;NoExecute最严格,连已有不容忍的Pod也会被驱逐。异构集群中最常见的坑是:Pod写了nodeSelector却仍然Pending,排查时第一时间看kubectl describe pod输出中的Events,如果出现“had taint that the pod didn't tolerate”,就说明节点侧的污点没有匹配的容忍度,把标签和污点这两套机制对齐即可解决。
一个推荐的实践是:每种异构节点都同时打上标签和污点,Pod同时写nodeSelector(或亲和性)与tolerations,形成双向约束。这样无论哪一侧配置遗漏,都不会造成资源误占。
三、节点亲和性与节点池:进阶分组方案
当分组逻辑变复杂,nodeSelector就不够用了,此时应使用节点亲和性(nodeAffinity)。它支持集合匹配、软性偏好和多条件组合。requiredDuringSchedulingIgnoredDuringExecution是硬性规则,不满足就不调度;preferredDuringSchedulingIgnoredDuringExecution是软性权重,调度器会按打分高低择优。
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: hardware-type
operator: In
values: ["gpu", "npu"]
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 80
preference:
matchExpressions:
- key: gpu-model
operator: In
values: ["a100"]上面这段配置的含义是:Pod必须调度到hardware-type为gpu或npu的节点,在满足条件的前提下优先选择gpu-model为a100的机器。这种“硬约束圈定范围、软约束排优先级”的组合,是异构集群里非常实用的模式,比如同时存在多种GPU型号时,可以让对算力要求高的任务优先占用高端卡。
在云环境或者用Cluster API自建的场景下,节点池(NodePool)是更高一层的抽象。同一个节点池内的机器规格、标签、污点完全一致,扩缩容时新节点自动继承这些属性。以常见云厂商的节点池为例,可以创建一个“gpu-pool”统一设置hardware=gpu:NoSchedule污点和相关标签,业务只需要面向节点池做亲和性声明,不必关心具体某台机器。这种做法把分组从“节点级”提升到“池级”,大幅降低运维成本。
四、调度失败的排查与体系化实践
异构集群调度问题的排查有固定套路。第一步用kubectl get pods -o wide确认Pod是否Pending;第二步kubectl describe pod pod-name看Events,常见的失败原因包括:节点资源不足(Insufficient nvidia.com/gpu)、污点不匹配、亲和性无法满足、以及DevicePlugin未就绪导致GPU资源上报为零。第三步用kubectl describe node检查节点的Allocatable与已分配情况,确认资源口径。对于GPU场景,特别要注意驱动、容器运行时和DevicePlugin插件(如NVIDIA device plugin for Kubernetes)这套链路,任何一环出问题,节点即使插了卡也不会向调度器上报GPU资源。
体系化实践方面有几条建议。第一,建立统一的标签规范文档,硬件类型、机型代号、业务归属分别用独立的标签键管理,避免出现一个标签塞多个含义的情况。第二,所有异构节点一律打污点,从机制上杜绝误调度。第三,关键业务配合使用Pod反亲和性(podAntiAffinity)做拓扑打散,避免训练任务全堆在一个机架或者一台机器上。第四,对资源利用率做持续观测,结合Descheduler定期驱逐放置不合理的Pod,让软性亲和性策略能够持续生效。
总结一下,异构集群的调度本质上是“约束的组合艺术”:标签和nodeSelector负责圈定范围,污点与容忍度负责准入控制,亲和性负责精细化打分,节点池负责规模化运维。把这四层机制理解透并配合使用,就能在保障GPU等昂贵资源专用的同时,让通用资源得到充分利用,构建出既高效又稳定的调度体系。
Kubernetes调度nodeSelector污点与容忍度修改时间:2026-09-13 08:52:32