Kubernetes Pod 调度深度实战——nodeSelector、affinity、taint 完整使用指南
Kubernetes Pod 调度深度实战——nodeSelector、affinity、taint 完整使用指南
适读人群:在生产环境使用 K8s 的工程师,需要精确控制 Pod 落在哪台节点的人 | 阅读时长:约 16 分钟 | 核心价值:彻底搞懂三种调度控制手段的适用场景和正确写法
某个周五晚上 8 点,运维突然 call 我,说线上的推理服务全挂了。我打开 Dashboard 一看,十几个 Pod 全是 Pending 状态,Events 里写着 0/8 nodes are available: 8 node(s) didn't match node selector。
排查了二十分钟才搞清楚:前一天有人给集群扩容加了几台节点,用了不同的 label 规范,而推理服务的 YAML 里写了硬性的 nodeSelector,新节点 label 不匹配,服务根本没地方跑。
这次事故让我重新梳理了一遍 K8s 调度机制。
为什么需要调度控制?
K8s 默认的调度器(kube-scheduler)会根据资源可用性自动决定 Pod 落在哪台节点,大多数情况下这已经够用了。
但有些场景需要你干预:
- GPU 推理服务只能跑在有 GPU 的节点
- 数据库 Pod 需要固定在有 SSD 的节点
- 同一服务的多个副本要分散在不同 AZ(可用区),避免单点故障
- 某些节点是专用节点,普通业务不能调度上去
- 某些 Pod 对延迟极敏感,需要和依赖的 Pod 在同一个节点
对应这些需求,K8s 提供了三类工具:nodeSelector(最简单)、affinity/anti-affinity(最灵活)、taint/toleration(排斥机制)。
nodeSelector:最简单的节点选择
nodeSelector 是最基础的调度约束,指定 Pod 必须调度到带有特定 label 的节点。
apiVersion: v1
kind: Pod
metadata:
name: gpu-inference
spec:
nodeSelector:
accelerator: nvidia-a100 # 只调度到有这个 label 的节点
disktype: ssd
containers:
- name: inference
image: mymodel:latest
resources:
limits:
nvidia.com/gpu: 1先给节点打 label:
kubectl label node gpu-node-01 accelerator=nvidia-a100
kubectl label node gpu-node-01 disktype=ssdnodeSelector 的局限性
nodeSelector 只支持等值匹配,没有"优先调度到 A,没有再去 B"这种软性要求。
我上面说的那次事故,根本原因就是用了 nodeSelector 做硬性约束,扩容时新节点 label 不一致,所有 Pod 都 Pending 了。
对于这种情况,应该用 affinity。
Node Affinity:更灵活的节点亲和性
Node Affinity 是 nodeSelector 的升级版,支持更丰富的表达式,以及硬性(required)和软性(preferred)两种约束。
apiVersion: apps/v1
kind: Deployment
metadata:
name: inference-service
spec:
replicas: 3
selector:
matchLabels:
app: inference
template:
metadata:
labels:
app: inference
spec:
affinity:
nodeAffinity:
# 硬性要求:必须满足,否则 Pending
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: accelerator
operator: In # 支持 In / NotIn / Exists / DoesNotExist / Gt / Lt
values:
- nvidia-a100
- nvidia-v100 # 满足任一即可
# 软性优先:尽量满足,不影响调度
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 80 # 权重 1-100,越高越优先
preference:
matchExpressions:
- key: disktype
operator: In
values:
- nvme
- weight: 20
preference:
matchExpressions:
- key: zone
operator: In
values:
- cn-beijing-a
containers:
- name: inference
image: mymodel:latest
resources:
limits:
nvidia.com/gpu: 1
memory: "8Gi"
requests:
nvidia.com/gpu: 1
memory: "6Gi"IgnoredDuringExecution 是什么意思?Pod 已经在运行的情况下,即使节点 label 变了,也不驱逐这个 Pod。这是当前唯一支持的模式。
Pod Affinity / Anti-Affinity:Pod 之间的调度关系
Node Affinity 控制的是"Pod 和节点的关系",Pod Affinity 控制的是"Pod 和 Pod 的关系"。
场景一:让 App Pod 和 Redis Pod 在同一台节点(降低延迟)
spec:
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- redis
topologyKey: kubernetes.io/hostname # 同一台机器场景二:让同一服务的多个副本分散在不同节点(高可用)
spec:
affinity:
podAntiAffinity:
# 软性反亲和:尽量分散,不强制
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: myservice
topologyKey: kubernetes.io/hostname # 不同主机
# 如果要强制跨 AZ 分散,改成 required:
# requiredDuringSchedulingIgnoredDuringExecution:
# - labelSelector:
# matchLabels:
# app: myservice
# topologyKey: topology.kubernetes.io/zone踩坑实录一:podAntiAffinity required 导致副本数受限
现象:设置了 3 个副本,但只调度成功了 2 个,第三个一直 Pending,Events 显示 0/3 nodes are available: 3 node(s) didn't match pod affinity/anti-affinity。
原因:集群只有 3 台节点,用了 requiredDuringScheduling 要求每台节点最多一个副本,但一台节点已经因为其他原因不可用,所以只能调度到 2 台节点,3 个副本无法全部调度。
解法:改成 preferred(软性约束),允许在极端情况下多个副本落在同一节点,保证服务可用性优先。
Taint 和 Toleration:节点排斥机制
Taint 和 Toleration 是另一套机制,逻辑上与 affinity 相反:
- Taint(污点):打在节点上,意思是"普通 Pod 不要来这里"
- Toleration(容忍):打在 Pod 上,意思是"我可以容忍某种污点,愿意去那里"
这个机制常用于:
- 专用节点(GPU 节点只跑 AI 服务)
- 主节点保护(master 节点默认有 taint,防止普通 Pod 调度上去)
- 节点维护(打上 taint,驱逐 Pod)
# 给节点打污点
kubectl taint node gpu-node-01 dedicated=ml-training:NoSchedule
# NoSchedule:不容忍的 Pod 不会调度到此节点
# PreferNoSchedule:尽量不调度,不强制
# NoExecute:不容忍的 Pod 会被驱逐(包括已运行的)
# 查看节点的污点
kubectl describe node gpu-node-01 | grep Taint
# 移除污点
kubectl taint node gpu-node-01 dedicated=ml-training:NoSchedule-对应的 Pod 需要声明容忍:
spec:
tolerations:
- key: "dedicated"
operator: "Equal"
value: "ml-training"
effect: "NoSchedule"
# 如果只声明 key,不声明 value,使用 Exists operator
- key: "dedicated"
operator: "Exists"
effect: "NoSchedule"踩坑实录二:master 节点被调度上了 Pod
现象:新搭的集群,业务 Pod 调度到了 master 节点,资源竞争导致 API Server 变慢。
原因:某些工具(如 kubeadm 1.21 之前的版本)搭建的集群,master 节点没有自动添加 taint。
解法:手动给 master 节点添加 taint,阻止普通 Pod 调度:
kubectl taint node master-node \
node-role.kubernetes.io/master:NoSchedule
# 新版 K8s (1.24+) control-plane 节点的 taint key 变了
kubectl taint node master-node \
node-role.kubernetes.io/control-plane:NoSchedule如果确实需要某些系统 Pod(如 DaemonSet)跑在 master 节点,在 Pod spec 里添加对应 toleration。
踩坑实录三:Topology Spread 配置错误导致调度不均
现象:集群有 3 个 AZ,每个 AZ 有 3 台节点,共 9 台。部署了 9 个副本,希望每个 AZ 均匀分布,结果大部分副本堆在了同一个 AZ。
原因:当时用的是 podAntiAffinity,但 topologyKey 设置的是 hostname(主机级别),而不是 AZ 级别。主机间确实分散了,但 AZ 间没做约束。
解法:用 K8s 1.19+ 的 topologySpreadConstraints,它专门用来控制 Pod 在拓扑域(AZ、Region、节点)上的分布均匀度:
spec:
topologySpreadConstraints:
# 约束 1:跨 AZ 尽量均匀
- maxSkew: 1 # 各 AZ 之间 Pod 数量差不超过 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule # 不满足时拒绝调度
labelSelector:
matchLabels:
app: myservice
# 约束 2:同时在主机级别也分散
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway # 不满足时仍可调度(软性)
labelSelector:
matchLabels:
app: myservicetopologySpreadConstraints 是比 podAntiAffinity 更强大的分布控制,推荐用这个来替代纯粹的反亲和性配置。
三种机制的选型总结
| 需求 | 推荐方案 |
|---|---|
| Pod 必须在特定节点(如 GPU 节点) | nodeAffinity required |
| Pod 尽量在特定节点,不强制 | nodeAffinity preferred |
| 副本跨节点/跨 AZ 分散 | topologySpreadConstraints |
| 专用节点隔离(只跑特定 Pod) | taint + toleration |
| Pod 和某类 Pod 靠近(降延迟) | podAffinity preferred |
| 灾备隔离(不能在同一节点) | podAntiAffinity required |
K8s 调度是个很深的话题,调度器本身还支持调度框架插件、调度 profile 等进阶功能。但日常工作中,把这三类工具用好,能解决 90% 以上的调度需求。
有一个我一直强调的原则:调度约束越严格,你的集群扩缩容就越麻烦。不要什么 Pod 都加硬性约束,先用软性约束,真正有硬性需求(比如 GPU)才用 required。
