说实话,我入行这十年,光“Pod 就是调度不到想让它去的地方”这种问题就排查了不下几十次。有时候是节点标签没打对,有时候是用了 nodeSelector 导致集群明明有空闲节点却没法用,还有时候——更坑爹——滚动更新时新老 Pod 混在一起,亲和性直接把新版本给“憋死”了。

读完这篇文章,你会搞懂:

  • nodeSelector——最简单的定向调度,但也是个“陷阱”,节点没标签就直接 Pending。
  • nodeAffinity——硬亲和 vs 软亲和,怎么组合使用。
  • podAffinity / podAntiAffinity——两个 Pod 的“捆绑”与“隔离”,以及 K8s 1.31 新增的 matchLabelKeys。
  • Taints & Tolerations——节点方主动“拒绝”,精确控制谁可以来。
  • 以及 调度规则冲突时该怎么排查,包含我遇到的真实案例。

先给个整体对比,心里有个数(重点信息加粗了):

调度策略

控制方

强制程度

典型场景

兜底机制

nodeSelector

Pod → Node

硬约束

精准部署到带 SSD 的节点

无——匹配失败 Pod 直接 Pending

nodeAffinity(硬)

Pod → Node

硬约束

GPU 任务强制部署到 GPU 节点

无——匹配失败 Pod 直接 Pending

nodeAffinity(软)

Pod → Node

软偏好

尽量靠近某类节点,但没符合条件的也没事

有——调度器会选其他节点

podAffinity

Pod → Pod

硬/软

延时敏感型服务“抱团”减少网络开销

硬约束下匹配失败则 Pending

podAntiAffinity

Pod → Pod

硬/软

高可用部署,避免所有副本挤在同一节点/机架

硬约束下若只有 1 个节点,replicas=2 直接 Pending

Taints(污点)

Node → Pod

节点主动排斥

隔离 GPU 节点只给 ML 任务用

节点自动污点(如 NotReady)驱逐 Pod

Tolerations(容忍)

Pod → Node

被动许可

特定 Pod 可以“无视”污点调度过来

有——调度器还会评估其他参数

以上是总览,下面分块实操。

一、nodeSelector:最“朴素”的定向调度

1.1 怎么用——先打标签,再选节点

Kubernetes 默认会用调度算法自动分配 Pod 到合适的节点,但如果你就是想让它去某个节点或某一类节点上(比如 SSD 的节点、高性能 CPU 的节点),nodeSelector 就能解决。

第一步:给目标节点打标签

kubectl label nodes k8s-node-1 disktype=ssd

打完后可以用 kubectl get nodes --show-labels 查看:

$ kubectl get nodes --show-labels
NAME         STATUS   ROLES    AGE   VERSION   LABELS
k8s-node-1   Ready    <none>   32d   v1.31.2   disktype=ssd,kubernetes.io/arch=amd64,...

第二步:在 Pod YAML 里加上 nodeSelector

apiVersion: v1
kind: Pod
metadata:
  name: nginx-ssd
spec:
  containers:
  - name: nginx
    image: nginx
  nodeSelector:
    disktype: ssd

执行 kubectl apply -f pod.yaml 后,调度器会强制把 Pod 调度到带有 disktype=ssd 标签的节点上。

1.2 缺点——不够灵活,且“要么调度成功,要么死”

nodeSelector 是“硬匹配”——如果集群里没有任何节点带有你指定的标签,Pod 就会一直 Pending。

$ kubectl describe pod nginx-ssd
...
Events:
  Type     Reason            Age   Message
  ----     ------            ----  -------
  Warning  FailedScheduling  12s   0/3 nodes are available: 3 node(s) didn't match node selector.

我个人的建议: nodeSelector 适合一些需求特别简单的场景,比如“我要把这个 Pod 运行在 SSD 节点上”。但如果你的需求稍微复杂一点(比如“尽量部署到某类节点,但如果没合适的节点也不要紧”),就必须上 nodeAffinity。

二、nodeAffinity:硬亲和(必须满足)和软亲和(尽量满足)

nodeAffinity 比 nodeSelector 强大很多,它把“必须”和“最好”区分开了。这里我用一个 GPU 的例子来说明。

2.1 硬亲和(Required):GPU 任务就得上 GPU 节点

apiVersion: v1
kind: Pod
metadata:
  name: gpu-task
spec:
  containers:
  - name: tensorflow
    image: tensorflow/tensorflow:latest-gpu
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: gpu
            operator: In
            values:
            - "nvidia"
            - "amd"

重点关注 requiredDuringSchedulingIgnoredDuringExecution,这里面的 matchExpressions 就是“硬条件”——Pod 必须部署到标签 gpu 值属于 nvidiaamd 的节点上。如果没有这种节点,Pod 就 Pending。

IgnoredDuringExecution 这部分的意思是:节点标签在 Pod 运行时可能发生变更,如果某个节点正在运行你这个 Pod,它的 gpu 标签突然被改了,K8s 调度器不会主动驱逐你的 Pod。这一点要记住,因为很多新手会误以为节点标签变化会让正在运行的 Pod 被重新调度。

常用的 operator 有:

  • In:标签值在指定的列表里
  • NotIn:标签值不在指定的列表里
  • Exists:只要节点有这个标签键即可,不关心值
  • DoesNotExist:节点没有这个标签键

小心operator: Exists 时不要在 YAML 里写 values,否则校验不过——我遇到过有人卡在这儿半小时。

2.2 软亲和(Preferred):尽量,但不是必须

软亲和用 preferredDuringSchedulingIgnoredDuringExecution,这里可以配置权重:

affinity:
  nodeAffinity:
    preferredDuringSchedulingIgnoredDuringExecution:
    - weight: 80
      preference:
        matchExpressions:
        - key: node-type
          operator: In
          values:
          - high-memory
    - weight: 20
      preference:
        matchExpressions:
        - key: node-type
          operator: In
          values:
          - standard

权重大小表示偏好程度,值范围 1~100。调度器会给节点打分,最后把 Pod 调度到总分最高的节点上。就算找不到任何有 high-memorystandard 标签的节点,Pod 也仍然可以调度到其他节点。这个兜底机制很重要,生产环境里我推荐软亲和比硬亲和用得更多。

生产环境经验: 我曾经把一个要求 GPU 的任务写成 preferredDuringScheduling,结果它被调度到了没有 GPU 的节点上——启动自然失败。所以,需要 GPU 必须required;普通业务只是希望靠近高配节点,才用 preferred。不要搞反。

三、podAffinity/podAntiAffinity:Pod 之间的“捆绑”和“隔离”

节点亲和性决定 Pod 要落在哪些节点上;Pod 亲和性则决定 Pod 要和哪些已存在的 Pod挤在同一个节点或同一个拓扑域(比如一个可用区)里。

3.1 podAffinity(抱团)

通信频繁的前端与后端,如果能调度到同一节点或同一可用区,网络延迟会低很多。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend
spec:
  replicas: 3
  selector:
    matchLabels:
      app: frontend
  template:
    metadata:
      labels:
        app: frontend
    spec:
      affinity:
        podAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - backend
            topologyKey: kubernetes.io/hostname
      containers:
      - name: frontend
        image: myapp/frontend:latest

topologyKey: kubernetes.io/hostname 表示“同一个节点”,如果要“同一个可用区”,就用 topologyKey: topology.kubernetes.io/zone

3.2 podAntiAffinity(隔离)——高可用的核心

坦白讲,podAntiAffinity 可能是生产环境中你用得最多的调度策略。为什么?因为如果你部署了 3 个副本,它们全挤在一个节点上,那个节点挂掉,服务就全挂了。

affinity:
  podAntiAffinity:
    preferredDuringSchedulingIgnoredDuringExecution:
    - weight: 100
      podAffinityTerm:
        labelSelector:
          matchExpressions:
          - key: app
            operator: In
            values:
            - nginx
        topologyKey: kubernetes.io/hostname

上面的例子是软反亲和:调度器尽量(权重大,所以尽量)让新 Pod 不和现有 Pod 落在同一个节点上,但如果实在只剩这个节点可用,那就只能落下来了。

高可用的标准做法,尤其在公有云跨可用区场景下,用硬反亲和 + topologyKey: topology.kubernetes.io/zone

affinity:
  podAntiAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
    - labelSelector:
        matchExpressions:
        - key: app
          operator: In
          values:
          - nginx
      topologyKey: topology.kubernetes.io/zone

这样每个可用区最多只能有一个 nginx Pod。当 replicas=3 且集群有三个可用区时,完美打散。

警告:硬反亲和 + replicas > 节点数时,Pod 直接 Pending,调度器不会做任何妥协。这一点务必想清楚,不要把 replicas 设得太大。

3.3 Kubernetes 1.31 新特性:matchLabelKeys

这是 K8s 1.31 引入的一个非常实用的改进,默认已开启(Beta)。它的作用一句话概括:滚动更新时,调度器能区分新老 Pod 版本了

在旧版本中,如果你配置了 podAntiAffinity,比如要求某个 Deployment 的 Pod 不能和同类 Pod 落在同一节点。滚动更新时新 Pod 启动、老 Pod 还在运行,调度器会把包括老 Pod 在内的所有 Pod都视为“需要避开”的对象——结果新 Pod 找不到可用的节点,只能卡住。

K8s 1.31 中,用 matchLabelKeys 可以解决:

affinity:
  podAntiAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
    - labelSelector:
        matchExpressions:
        - key: app
          operator: In
          values:
          - nginx
      topologyKey: kubernetes.io/hostname
      matchLabelKeys:
      - pod-template-hash

pod-template-hash 是每个 Deployment 版本都会自动生成的标签。加上这个配置后,调度器只跟同一版本的 Pod 做反亲和比较,滚动更新瞬间顺畅了。这个特性是我 2025 年在生产环境验证过的,强烈建议开启。

四、Taints(污点)与 Tolerations(容忍):节点方的“黑名单”

前面说的亲和性,都是 Pod 主动表达“我要去那里”。但有时候你需要的是节点主动拒绝——“除了特定 Pod,谁都不许过来”

污点就是这样机制:在节点上“涂”一个排斥标记,Pod 必须带着“容忍度”才能被调度过来。

4.1 给节点打污点

kubectl taint nodes node-gpu gpu=ml:NoSchedule

effect 有三种取值:

  • NoSchedule:硬阻止新 Pod 调度过来(除非 Pod 有对应容忍度)
  • PreferNoSchedule:软阻止,尽量不让新 Pod 过来,但实在没节点也会过来
  • NoExecute:最“狠”——不仅新 Pod 进不来,已经在运行但没有容忍度的 Pod 会被驱逐

4.2 Pod 添加容忍度

假设某个机器学习 Pod 想运行在被打了污点的 GPU 节点上:

apiVersion: v1
kind: Pod
metadata:
  name: ml-pod
spec:
  containers:
  - name: ml
    image: tensorflow/tensorflow
  tolerations:
  - key: "gpu"
    operator: "Equal"
    value: "ml"
    effect: "NoSchedule"

operator: Equal 必须 key、value、effect 三个都匹配。operator: Exists 只需要 key 和 effect 匹配(无视 value)。两种都可以,根据场景选择。

4.3 系统自动添加的污点

Kubelet 在某些条件下会自动给节点打上污点,比如:

  • node.kubernetes.io/not-ready
  • node.kubernetes.io/unreachable
  • node.kubernetes.io/memory-pressure
  • node.kubernetes.io/disk-pressure

这些污点一般都会带 NoExecute effect,导致不满足容忍度的 Pod 被驱逐。DaemonSet 通常自带对这些污点的容忍,普通应用则需要注意。

一个真实案例:我之前遇到一个节点因为磁盘压力自动打上了污点,上面跑的一个有状态数据库 Pod 因为没有配置容忍度,直接被驱逐了。数据库还没来得及做 checkpoint,重启后数据出问题。老实说那次事故提醒了我:对有状态服务,最好加上对常见系统污点的容忍,至少给个缓冲时间。

五、调度规则冲突时的排查思路(含案例)

当调度策略变得复杂,各种亲和性与污点规则一起上时,调度失败几乎是必然会发生的事情。以下是我自己摸爬滚打总结的几步排查法:

5.1 看 Pod 状态和 Events

kubectl describe pod <pod-name>

重点关注 Events 部分的 FailedScheduling 信息,它会告诉你具体是哪个节点被拒绝了,因为什么原因。常见的三种报错类型:

报错关键字

问题定位

didn't match node selector

nodeSelector 或 nodeAffinity 硬条件没匹配

had untolerated taint

节点有污点,Pod 没容忍

Insufficient cpu / memory

节点资源不足

pod affinity/anti-affinity

podAffinity 或 podAntiAffinity 不满足

5.2 检查节点标签、污点、资源

kubectl get nodes --show-labels
kubectl describe node <node-name>

kubectl describe node 会显示节点的 Taints、Allocatable 资源量、已有 Pod 列表等信息,非常全面。

5.3 我的翻车案例:节点标签更新导致 Pod 被驱逐

Kubelet 重启时会重新校验 Pod 亲和性规则与节点标签是否匹配。如果你在 Pod 运行中修改了节点标签,且新标签不再满足 Pod 的 nodeAffinity,那么kubelet 重启后会把 Pod 标记为 Completed 并重新调度。如果集群里已经没有满足条件的节点,新 Pod 就会 Pending,可能触发服务中断。

解决方案很简单:修改节点标签前,先确认节点上没有受影响的 Pod(尤其是 Deployment 下挂的 Pod,因为它会被重建),或者确保新标签仍然覆盖原 affinity 条件。标签变化本身不会立即驱逐 Pod,但 Kubelet 一旦重启就会触发校验。

六、总结一下

把这几个调度策略串起来,我的经验是:

  1. nodeSelector 最简单但有风险——适合单条件强制绑定场景。
  2. nodeAffinity 更灵活——硬亲和用 required(GPU 场景),软亲和用 preferred(普通业务倾向)。
  3. podAntiAffinity 是高可用的基础——副本必须打散到不同节点 / 可用区,硬反亲和多副本 + 节点数不足时直接 Pending,记得检查节点数。
  4. Taints + Tolerations 适合节点资源隔离,比如 GPU 节点、高 IO 节点、预发布节点。
  5. K8s 1.31+ 别忘了 matchLabelKeys,它解决滚动更新时旧版 Pod“堵路”的老大难问题。
  6. 规则冲突时——先看 kubectl describe pod 的 Events,再查节点标签、污点、资源,定位问题。

写在最后:你还有什么更好的办法?

以上是我在生产环境里反复验证过的套路。不过 K8s 调度生态这两年变化很快,比如 Kueue、YuniKorn 这些更专业的调度器也开始被大规模使用。如果你有遇到过更奇葩的调度场景(比如上百个节点 + 上千个 Pod 的动态调度优化),欢迎评论区聊聊。

觉得有用的话,也欢迎点个赞或转给团队里还在踩调度坑的同学——我一个人踩过的坑,希望其他人少踩点。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐