Kubernetes 基础体系 · 第 23/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。

Kubernetes 调度约束:NodeSelector、Affinity、Taint、Topology 和 Spread

Kubernetes 调度约束解决的是一个核心问题:当 Pod 进入调度队列后,调度器如何判断某个节点是否“允许”承载它,以及在多个可用节点之间如何选择更合适的节点。

这些机制作用层次不同:

  • nodeSelector:按节点标签做简单的硬匹配。
  • nodeAffinity:按节点标签做更丰富的硬约束或软偏好。
  • podAffinity / podAntiAffinity:根据其他 Pod 的位置决定当前 Pod 应该靠近或远离哪些 Pod。
  • taint / toleration:节点声明“通常不接纳哪些 Pod”,Pod 通过容忍来获得进入资格。
  • topologyKey:定义“节点属于哪个拓扑域”,例如可用区、机架或自定义故障域。
  • topologySpreadConstraints:限制一组 Pod 在多个拓扑域之间的不均衡程度。

理解这些字段时,必须区分两类行为:

  1. 硬约束:不满足就不能调度。
  2. 软偏好:满足条件的节点会被优先选择,但没有合适节点时仍可能调度。

调度器通常先执行可行性筛选,再进行打分,最后绑定:

Pod 创建
  │
  ▼
调度队列 Queue
  │
  ▼
Filter:排除违反硬约束、资源不足或不满足其他条件的节点
  │
  ▼
Score:根据软偏好和资源策略给可行节点打分
  │
  ▼
选择最高分节点
  │
  ▼
Bind:写入 Pod.spec.nodeName

这不是“所有字段简单相加”。不同约束由不同调度插件参与,最终分数还会经过插件权重和归一化处理。因此,不能仅凭 YAML 推导出调度器一定选择某个节点。


一、调度约束的共同基础:节点、标签与拓扑域

1. 节点标签是匹配的输入

节点标签是键值对:

metadata:
  labels:
    kubernetes.io/os: linux
    kubernetes.io/arch: amd64
    node-pool: compute
    topology.kubernetes.io/zone: zone-a

Pod 通过这些标签表达要求。例如:

spec:
  nodeSelector:
    node-pool: compute

表示 Pod 只能调度到包含:

node-pool=compute

的节点。

查看节点标签:

kubectl get nodes --show-labels
kubectl get nodes -L node-pool,topology.kubernetes.io/zone

为测试节点添加标签:

kubectl label node worker-1 node-pool=compute

删除标签:

kubectl label node worker-1 node-pool-

节点标签是调度输入,不是天然可信的安全边界。若普通用户能够修改节点标签,就可能把 Pod 引导到不应运行的位置。生产环境应限制节点标签修改权限,并关注 Node authorizer、NodeRestriction admission plugin 等安全机制。云厂商提供的节点池和区域标签也可能因发行版、云平台或自建集群而不同,不能只根据名称假定一定存在。

2. Topology 不是 Kubernetes 对象

topology 在这里表示节点之间的层次或故障域关系。常见拓扑标签包括:

topology.kubernetes.io/zone
topology.kubernetes.io/region
kubernetes.io/hostname

如果节点标签如下:

节点 topology.kubernetes.io/zone kubernetes.io/hostname
node-a1 zone-a node-a1
node-a2 zone-a node-a2
node-b1 zone-b node-b1

那么:

  • topology.kubernetes.io/zonetopologyKey 时,有两个域:zone-azone-b
  • kubernetes.io/hostnametopologyKey 时,每个节点通常都是一个独立域。

拓扑域是否真实代表独立故障域,取决于底层基础设施。一个自建集群中的 zone-a 标签,如果实际上只是人为命名,并不自动意味着它们位于不同机房或电源域。


二、NodeSelector:最简单的节点硬约束

nodeSelector 是 PodSpec 中的一个键值映射:

apiVersion: v1
kind: Pod
metadata:
  name: selector-demo
spec:
  nodeSelector:
    node-pool: compute
  containers:
    - name: app
      image: registry.k8s.io/pause:3.10

它的逻辑是:

节点可行 ⇔ 对每个 key,都存在完全相等的 node.label[key] == value

例如:

Pod 要求:node-pool=compute,kubernetes.io/arch=amd64

节点 node-1:
node-pool=compute
kubernetes.io/arch=arm64
结果:不可行

节点 node-2:
node-pool=compute
kubernetes.io/arch=amd64
结果:可行

多个键之间是逻辑 AND,没有 OR、存在性匹配、数值比较或否定表达式。

典型失败表现

如果集群中没有匹配节点,Pod 会保持 Pending。事件通常可以看到类似信息:

kubectl describe pod selector-demo

可能包含:

0/3 nodes are available: 3 node(s) didn't match Pod's node affinity/selector.

事件文本由 Kubernetes 版本和调度插件实现决定,不能把具体英文文本当成稳定 API;稳定的事实是调度器会记录未调度原因。

NodeSelector 与资源条件是 AND 关系

匹配标签并不代表一定能运行。调度器仍需检查:

  • CPU、内存和扩展资源请求;
  • 节点是否不可调度;
  • 污点和容忍;
  • Pod 的硬亲和性和反亲和性;
  • 拓扑分布约束;
  • 卷的拓扑限制;
  • 其他调度插件条件。

因此:

可调度节点
= 满足 nodeSelector
  AND 资源足够
  AND 满足 taint/toleration
  AND 满足其他硬约束

三、Node Affinity:节点硬约束与软偏好

nodeAffinity 是比 nodeSelector 更丰富的节点标签约束。它位于:

spec:
  affinity:
    nodeAffinity:

支持两种主要模式:

  • requiredDuringSchedulingIgnoredDuringExecution:调度时必须满足;
  • preferredDuringSchedulingIgnoredDuringExecution:调度时尽量满足。

这里的 IgnoredDuringExecution 很重要:Pod 调度后,如果节点标签后来发生变化,Kubernetes 通常不会因为该变化自动驱逐已经运行的 Pod。它只影响后续调度。当前稳定 API 中并没有一个通用的 requiredDuringSchedulingRequiredDuringExecution 字段可直接使用。

1. 硬节点亲和性

apiVersion: v1
kind: Pod
metadata:
  name: required-affinity-demo
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
          - matchExpressions:
              - key: node-pool
                operator: In
                values:
                  - compute
                  - memory
              - key: kubernetes.io/arch
                operator: In
                values:
                  - amd64
  containers:
    - name: app
      image: registry.k8s.io/pause:3.10

其逻辑需要特别准确地理解:

  • nodeSelectorTerms 之间是 OR;
  • 同一个 nodeSelectorTerm 内的 matchExpressions 之间是 AND;
  • 同一个表达式中的 values,对于 In 操作符表示 OR。

形式化表示为:

term1 OR term2 OR ...

其中:

termi = expressioni,1 AND expressioni,2 AND ...

上例表示:

(node-pool=compute OR node-pool=memory)
AND
(kubernetes.io/arch=amd64)

不是:

(node-pool=compute AND arch=amd64)
OR
(node-pool=memory)

常见操作符包括:

  • In:标签值属于给定集合;
  • NotIn:标签值不属于给定集合,缺少该标签也会影响匹配语义,使用时应验证目标版本行为;
  • Exists:存在该标签;
  • DoesNotExist:不存在该标签;
  • GtLt:对标签值进行整数比较,适用范围和解析规则比普通字符串匹配更严格。

ExistsDoesNotExist 不应填写 values。例如:

- key: accelerator
  operator: Exists

表示节点只要存在 accelerator 标签即可,不关心值。

2. 软节点亲和性

spec:
  affinity:
    nodeAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
        - weight: 80
          preference:
            matchExpressions:
              - key: node-pool
                operator: In
                values:
                  - compute
        - weight: 20
          preference:
            matchExpressions:
              - key: topology.kubernetes.io/zone
                operator: In
                values:
                  - zone-a

每个偏好项的 weight 必须在 1 到 100 之间。一个节点满足某个偏好项,就获得该项对应的偏好分值;调度器会结合多个插件的评分选择节点。

但以下推导是不成立的:

weight=80 的节点一定比 weight=20 的节点优先

原因是:

  1. 不同节点可能分别满足多个偏好项;
  2. 调度器会对插件分数进行归一化;
  3. 插件有调度权重;
  4. 资源均衡、Pod 拓扑、镜像本地性等其他插件也会参与评分;
  5. 并列时还可能使用实现相关的决策逻辑。

软亲和性适合表达“优先使用某类节点”,不适合表达“绝不能使用某类节点”。

3. nodeSelectornodeAffinity 同时存在

如果两者同时设置,必须同时满足:

spec:
  nodeSelector:
    node-pool: compute
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
          - matchExpressions:
              - key: kubernetes.io/arch
                operator: In
                values: [amd64]

等价于:

node-pool=compute
AND
kubernetes.io/arch=amd64

它们不是“二选一”,也不是后者覆盖前者。


四、Pod Affinity 与 Pod Anti-Affinity:根据其他 Pod 的位置调度

节点亲和性看的是:

目标节点自己的标签

Pod 亲和性和反亲和性看的是:

其他 Pod 的标签 + 其他 Pod 所在节点的拓扑标签

例如,一个 Web Pod 可以要求自己与带有 app=cache 的 Pod 位于同一可用区。

apiVersion: v1
kind: Pod
metadata:
  name: web
  labels:
    app: web
spec:
  affinity:
    podAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        - labelSelector:
            matchLabels:
              app: cache
          topologyKey: topology.kubernetes.io/zone
  containers:
    - name: app
      image: registry.k8s.io/pause:3.10

它的实际含义不是“节点必须有 app=cache 标签”,而是:

存在某个带 app=cache 的 Pod,
并且该 Pod 所在节点的 zone
等于候选节点的 zone

1. 亲和性匹配的基本条件

对每一个候选节点,调度器大致需要判断:

  1. 找出符合 labelSelector 的已有 Pod;
  2. 限制搜索的命名空间;
  3. 读取这些 Pod 所在节点的 topologyKey 标签值;
  4. 判断候选节点是否位于相同拓扑域;
  5. 对硬亲和性,不满足则过滤;对软亲和性,则参与打分。

默认情况下,Pod 亲和性规则匹配同一命名空间中的 Pod。可以用 namespacesnamespaceSelector 改变范围:

- labelSelector:
    matchLabels:
      app: cache
  namespaceSelector:
    matchLabels:
      team: platform
  topologyKey: topology.kubernetes.io/zone

namespaceSelector 本身选择命名空间,而不是 Pod;随后还要在这些命名空间中按 labelSelector 选择 Pod。

2. Pod Anti-Affinity

反亲和性表示尽量或必须远离匹配的 Pod:

apiVersion: v1
kind: Pod
metadata:
  name: replica
  labels:
    app: api
spec:
  affinity:
    podAntiAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        - labelSelector:
            matchLabels:
              app: api
          topologyKey: kubernetes.io/hostname
  containers:
    - name: app
      image: registry.k8s.io/pause:3.10

topologyKey: kubernetes.io/hostname 通常表示:

同一节点内不能出现匹配的 api Pod

如果改成:

topologyKey: topology.kubernetes.io/zone

则表示:

同一可用区内不能出现匹配的 api Pod

后者通常非常严格。假设只有三个可用区,却部署四个副本,第四个副本无法满足“每个 zone 至多一个”的硬反亲和性,因此会保持 Pending

Pod 反亲和性还具有明显的计算成本:调度器需要检查现有 Pod 与候选节点的拓扑关系。高规模集群中,大量复杂的硬 Pod 亲和性规则可能增加调度延迟。它也不像拓扑分布约束那样直接表达“尽量均匀分布”,而更适合表达“不要共置”这一类关系。


五、Taint 与 Toleration:节点的拒绝条件

亲和性是 Pod 主动选择节点;污点是节点主动拒绝 Pod。

给节点添加污点:

kubectl taint nodes worker-gpu dedicated=gpu:NoSchedule

它表示:

节点存在 dedicated=gpu:NoSchedule 污点;
没有匹配容忍的 Pod 不应被调度到该节点。

对应的 Pod 容忍如下:

spec:
  tolerations:
    - key: dedicated
      operator: Equal
      value: gpu
      effect: NoSchedule

operator: Exists 可以忽略具体值:

spec:
  tolerations:
    - key: dedicated
      operator: Exists
      effect: NoSchedule

key 为空时,operator 必须是 Exists,表示匹配指定 effect 的所有污点。若 effect 为空,则可匹配该 key 的所有 effect。生产配置中应尽量写出明确的 keyvalueeffect,避免容忍范围过大。

1. 三种主要污点效果

NoSchedule

没有匹配容忍的 Pod 不能新调度到节点。

它主要影响调度阶段,不会因为添加污点就自动删除已经运行的普通 Pod。

PreferNoSchedule

调度器尽量避开该节点,但这不是绝对禁止。若其他节点不合适,Pod 仍可能被调度到这里。

NoExecute

除了影响新调度,还会影响已经运行的 Pod:

  • 没有匹配容忍的 Pod 会被驱逐;
  • 有匹配容忍但设置了 tolerationSeconds 的 Pod,在容忍时间到期后可能被驱逐;
  • 永久容忍的 Pod 可以继续运行。

示例:

spec:
  tolerations:
    - key: node.kubernetes.io/not-ready
      operator: Exists
      effect: NoExecute
      tolerationSeconds: 300

这表示 Pod 可以在节点 NotReady 污点下继续运行 300 秒。真正执行已运行 Pod 驱逐的主要是节点侧组件和控制器路径,而不是简单理解为“调度器删除 Pod”。

2. 容忍不等于吸引

这是最常见的误解:

Pod tolerates dedicated=gpu

只表示:

它不会因为该污点被拒绝

并不表示:

它会优先去 GPU 节点

如果希望 Pod 只使用 GPU 节点,通常需要同时配置:

spec:
  nodeSelector:
    accelerator: nvidia
  tolerations:
    - key: dedicated
      operator: Equal
      value: gpu
      effect: NoSchedule

二者分别表达:

  • 节点选择:我想去哪里;
  • 污点容忍:我能否进入那里。

3. 多个污点的逻辑

假设节点有:

dedicated=gpu:NoSchedule
maintenance=true:NoExecute

Pod 必须分别有匹配的容忍。只容忍第一个污点,仍会受到第二个污点的影响。

形式化地说:

Pod 可通过污点检查
⇔ 对每一个影响它的污点,都存在匹配 toleration

匹配通常要求:

  • key 相同,除非使用合法的空 key + Exists
  • operator: Equal 时 value 相同;
  • effect 相同,除非容忍 effect 为空;
  • operator: Exists 时不比较 value。

4. 节点状态产生的污点

节点控制器可能添加诸如:

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

集群中的 DaemonSet、系统 Pod 或云厂商组件也可能配置自动容忍。这些自动行为依赖控制器和发行版配置,排查 Pod 为什么能留在异常节点上时,应直接查看 Pod 的完整 tolerations

kubectl get pod <pod> -o yaml
kubectl describe node <node>

六、Topology Spread Constraints:控制 Pod 的不均衡程度

拓扑分布约束用于表达:

一组匹配 Pod 在多个拓扑域之间最多允许有多大数量差异

示例:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 6
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
    spec:
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: api
      containers:
        - name: app
          image: registry.k8s.io/pause:3.10

字段含义:

  • maxSkew:允许的最大不均衡程度;
  • topologyKey:用于划分拓扑域的节点标签;
  • whenUnsatisfiable
    • DoNotSchedule:无法满足时过滤节点;
    • ScheduleAnyway:仍然调度,但尽量改善分布;
  • labelSelector:统计哪些已有 Pod;
  • minDomains:在支持该字段的集群版本中,可声明期望的最少拓扑域数量;版本和特性状态应以目标集群文档为准。

1. maxSkew 的计算

设:

  • count(d):拓扑域 d 中匹配 Pod 的数量;
  • globalMinimum:参与计算的拓扑域中最小的 Pod 数量;
  • 将候选 Pod 放入域 d 后,该域数量变成 count(d)+1

则候选放置的偏斜度可理解为:

skew(d) = count(d) + 1 - globalMinimum

当:

skew(d) > maxSkew

whenUnsatisfiable=DoNotSchedule 时,该节点不能作为候选。

完整算例:

拓扑域 当前匹配 Pod 数
zone-a 3
zone-b 1
zone-c 0

因此:

globalMinimum = 0
maxSkew = 2

尝试把下一个 Pod 放入不同域:

目标域 放置后数量 偏斜度 结果
zone-a 4 4 - 0 = 4 拒绝
zone-b 2 2 - 0 = 2 允许
zone-c 1 1 - 0 = 1 允许

这里容易产生错误直觉:有人会比较 zone-a=3zone-b=1 的差值 2,认为再放一个到 zone-a 只是差 3。实际上 globalMinimum 仍包括空的 zone-c,所以候选放入 zone-a 后的 skew 是 4。

如果只有两个参与计算的域:

拓扑域 当前匹配 Pod 数
zone-a 3
zone-b 1

则:

globalMinimum = 1

把 Pod 放入 zone-a 后:

skew = 4 - 1 = 3

即使同样是 maxSkew=2,结果也不同。可见参与计算的拓扑域集合非常重要。

2. DoNotScheduleScheduleAnyway

whenUnsatisfiable: DoNotSchedule

是硬约束。违反时,调度器在 Filter 阶段排除节点。

whenUnsatisfiable: ScheduleAnyway

是软约束。调度器会通过评分优先选择能改善分布的节点,但如果资源、污点或其他约束使其不合适,仍可能选择导致暂时不均衡的节点。

对于跨可用区高可用服务,DoNotSchedule 不能脱离容量设计使用。如果某个可用区没有可用节点、节点资源不足或卷不能跨区挂载,严格分布会直接导致 Pod Pending,而不是自动牺牲约束来恢复服务。

3. minDomains 的边界

minDomains 用于表达“至少应有多少个拓扑域参与分布”。它适合防止只有一个可用域时,调度器把所有副本都集中到该域却仍被认为满足分布。

不过,minDomains 的 API 特性状态、默认行为和对 globalMinimum 的影响具有版本敏感性。使用前应确认目标集群支持的 Kubernetes 版本;不能把新版本文档中的字段直接复制到旧集群。

4. 统计对象不等于 Deployment 副本数

labelSelector 决定统计哪些已有 Pod。若标签写得过宽:

labelSelector:
  matchLabels:
    app: api

可能把多个工作负载都算入同一分布集合;若标签写得过窄,则可能完全没有统计到目标 Pod。

因此,Deployment 的 selector、Pod template labels 和 spread constraint 的 selector 应共同设计。尤其要避免修改标签后造成新旧 Pod 不再属于同一分布集合。


七、Topology Spread 与 Pod Anti-Affinity 的区别

两者都能影响 Pod 的位置,但表达能力不同。

1. Pod Anti-Affinity 更像“禁止共置”

podAntiAffinity:
  requiredDuringSchedulingIgnoredDuringExecution:
    - labelSelector:
        matchLabels:
          app: api
      topologyKey: kubernetes.io/hostname

含义是:同一节点不能出现匹配 Pod。

它适合:

一个节点最多一个副本

但它不直接表达:

zone-a 有 3 个,zone-b 有 2 个,zone-c 有 1 个时,下一步去哪

2. Topology Spread 更像“限制数量差”

topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: topology.kubernetes.io/zone
    whenUnsatisfiable: DoNotSchedule

它适合表达:

不同可用区之间的副本数尽量均匀

这允许一个域拥有多个副本,只要数量差不超过约束。

3. 两者同时使用时是 AND

如果同时配置:

  • zone 级别的 spread;
  • hostname 级别的硬反亲和性;

那么 Pod 必须同时满足:

zone 分布约束
AND
同节点不能共置

这可能是合理的高可用设计,也可能过于严格。部署副本数、节点数、可用区数量和故障恢复后的容量必须一起计算。


八、多个约束组合时如何推导结果

考虑下面的 Pod:

apiVersion: v1
kind: Pod
metadata:
  name: combined-demo
  labels:
    app: api
spec:
  nodeSelector:
    node-pool: compute

  tolerations:
    - key: dedicated
      operator: Equal
      value: api
      effect: NoSchedule

  affinity:
    nodeAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
        - weight: 80
          preference:
            matchExpressions:
              - key: topology.kubernetes.io/zone
                operator: In
                values: [zone-a]

    podAntiAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        - labelSelector:
            matchLabels:
              app: api
          topologyKey: kubernetes.io/hostname

  topologySpreadConstraints:
    - maxSkew: 1
      topologyKey: topology.kubernetes.io/zone
      whenUnsatisfiable: DoNotSchedule
      labelSelector:
        matchLabels:
          app: api

  containers:
    - name: app
      image: registry.k8s.io/pause:3.10

调度一个新副本时,可以按以下顺序理解:

第一步:NodeSelector 筛选

节点必须满足:

node-pool=compute

否则直接不可行。

第二步:污点筛选

节点若有:

dedicated=api:NoSchedule

则该 Pod 可以通过,因为容忍匹配。

节点若有:

dedicated=gpu:NoSchedule

则仍然不能通过,因为没有对应容忍。

第三步:节点硬约束和资源检查

本例没有硬 nodeAffinity,但调度器仍检查 CPU、内存、卷、节点状态等条件。

第四步:Pod 反亲和性检查

候选节点的 hostname 域中不能已经有带:

app=api

的 Pod。

第五步:拓扑分布检查

统计所有相关 app=api Pod 在各 zone 的数量,检查放入候选 zone 后是否满足:

skew <= 1

由于本例使用 DoNotSchedule,不满足就过滤。

第六步:软偏好评分

对剩余节点,优先选择位于 zone-a 的节点,但这只是偏好,不会推翻前面任何硬条件。

最终可以概括为:

候选节点
= NodeSelector
  AND Taint/Toleration
  AND 资源与系统条件
  AND PodAntiAffinity
  AND TopologySpread

然后在候选节点中依据软 nodeAffinity 和其他评分插件进行选择。


九、调度失败、抢占与约束的关系

当所有节点都不可行时,调度器可能考虑抢占低优先级 Pod。抢占的作用是释放资源,而不是绕过硬约束。

例如:

节点有足够 CPU,但违反 required nodeAffinity

驱逐其他 Pod 也不能让该节点变成可行节点。

同样:

节点带 NoSchedule 污点,目标 Pod 没有容忍

抢占普通 Pod 不会自动授予目标 Pod 容忍资格。

因此,抢占能够解决的主要是:

资源不足导致的不可行

而通常不能解决:

标签不匹配
硬亲和性不满足
硬反亲和性冲突
污点没有容忍
拓扑硬约束冲突
卷拓扑不兼容

一个优先级很高的 Pod,如果自身硬约束过严,仍然可能长期 Pending。这也是优先级、抢占和调度约束之间的边界:优先级改变竞争顺序,不能把错误的约束变成正确的约束。


十、完整验证示例

创建一个测试节点标签:

kubectl label node worker-1 node-pool=compute topology.kubernetes.io/zone=zone-a
kubectl label node worker-2 node-pool=general topology.kubernetes.io/zone=zone-b

创建一个只允许 compute 节点的 Pod:

apiVersion: v1
kind: Pod
metadata:
  name: scheduling-test
spec:
  nodeSelector:
    node-pool: compute
  containers:
    - name: pause
      image: registry.k8s.io/pause:3.10

应用并查看:

kubectl apply -f scheduling-test.yaml
kubectl get pod scheduling-test -o wide

预期结果是 Pod 被绑定到标签为 node-pool=compute 的节点。如果 Pod 为 Pending

kubectl describe pod scheduling-test
kubectl get events --sort-by=.lastTimestamp

重点查看:

  • 是否没有节点匹配标签;
  • 是否 CPU 或内存不足;
  • 是否有未容忍污点;
  • 是否存在亲和性或拓扑约束冲突;
  • 是否存在卷拓扑限制。

查看实际绑定结果:

kubectl get pod scheduling-test \
  -o jsonpath='{.spec.nodeName}{"\n"}'

查看调度器写入的事件:

kubectl get events \
  --field-selector involvedObject.name=scheduling-test \
  --sort-by=.lastTimestamp

删除测试对象:

kubectl delete pod scheduling-test

验证污点:

kubectl taint nodes worker-1 dedicated=api:NoSchedule
kubectl describe node worker-1 | grep -A5 Taints

此时,原本只满足 nodeSelector 的 Pod 如果没有 dedicated=api 容忍,就可能无法调度到 worker-1。添加容忍后,只是恢复资格;若还希望它主动优先去该节点,仍需节点标签和亲和性偏好。


十一、常见误区与生产边界

1. 把 preferred 当成强制条件

preferredDuringSchedulingIgnoredDuringExecution 失败不会让 Pod Pending。如果业务绝不能运行在某类节点上,必须使用 requiredDuringSchedulingIgnoredDuringExecutionnodeSelector 或其他硬约束。

2. 认为 toleration 会把 Pod 引导到目标节点

容忍只解除污点造成的拒绝。需要“定向进入”时,应同时使用节点标签和节点亲和性。

3. 把 NoExecute 当成只影响新 Pod

NoExecute 还可能驱逐已经运行的 Pod。修改节点污点前,应确认工作负载的容忍时间、控制器重建行为和业务对短暂迁移的承受能力。

4. 使用不存在或不一致的拓扑标签

如果约束使用:

topologyKey: topology.kubernetes.io/zone

但节点没有这个标签,拓扑域计算就可能与预期完全不同。应先检查:

kubectl get nodes \
  -L topology.kubernetes.io/zone,topology.kubernetes.io/region,kubernetes.io/hostname

云平台的 zone 标签通常由云控制器或节点初始化组件提供;自建集群必须自行保证标签来源、命名和持续性。

5. 过度使用硬 Pod 反亲和性

“每个 zone 不能有两个副本”和“每个节点不能有两个副本”是不同约束。副本数、节点数不足或节点故障时,硬反亲和性会直接造成 Pending。若目标只是改善分布而不是绝对禁止,可以考虑 ScheduleAnyway 的拓扑分布约束或软反亲和性。

6. 忽略标签变更后的执行期语义

带有 IgnoredDuringExecution 的节点亲和性主要在调度时生效。节点标签被修改后,已经运行的 Pod 通常不会自动因为该规则被驱逐。若标签代表安全边界、硬件能力或合规属性,不能仅依赖调度时的一次判断,还要控制标签写权限并设计额外的运行期校验。

7. 只看 Pod YAML,不看节点和事件

调度问题至少需要同时检查:

kubectl get pod <pod> -o yaml
kubectl describe pod <pod>
kubectl describe node <node>
kubectl get nodes --show-labels
kubectl get events --sort-by=.lastTimestamp

Pod YAML 告诉你声明了什么,节点信息告诉你实际提供了什么,事件告诉你调度器在哪个阶段拒绝了候选节点。只有三者结合,才能区分“没有匹配标签”“资源不足”“污点未容忍”和“拓扑约束冲突”。


十二、如何选择这些机制

可以把常见需求映射为以下语义:

需求 更合适的机制
只运行在 node-pool=compute nodeSelector
节点标签需要 OR、Exists、NotIn 或软偏好 nodeAffinity
与某类 Pod 同 zone podAffinity
同一节点或同一 zone 不共置 podAntiAffinity
节点只接纳专用工作负载 taint + Pod toleration
容灾副本跨 zone 均匀分布 topologySpreadConstraints
节点异常后允许 Pod 暂留一段时间 NoExecute + tolerationSeconds
高优先级 Pod 资源不足时清理低优先级 Pod PriorityClass + 抢占,但不能绕过硬约束

最重要的设计原则不是“字段越多越可靠”,而是让每个字段只表达一种明确关系:

nodeSelector / required nodeAffinity:我必须去哪里
preferred nodeAffinity:我更想去哪里
toleration:我可以进入哪里
taint:这里通常不接纳谁
podAffinity:我要靠近谁
podAntiAffinity:我要远离谁
topologySpread:数量差最多允许多大

当多个硬约束交集为空时,调度器不会替应用猜测哪条规则更重要,而是让 Pod 保持 Pending。因此,部署前应按节点标签、污点、资源、拓扑域数量和副本数逐项计算可行集合;运行中则通过 Pod 事件、节点状态和实际标签验证约束是否仍然成立。


系列导航与关联阅读

官方资料

本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。