一文搞懂 nodeAffinity、podAffinity、podAntiAffinity、topologySpreadConstraints,附完整 YAML 案例 + 调度失败调试方法

前言

在 Kubernetes 集群中,调度策略是决定 Pod 落在哪台机器上的核心机制。你是否遇到过这些问题:

  • Pod 一直 Pending,但不知道是哪个规则卡住了?
  • nodeSelectorTermsmatchExpressions 的逻辑到底是 AND 还是 OR?
  • podAffinitypodAntiAffinity 到底有什么区别,什么时候用哪个?
  • 拓扑域(topologyKey)到底是个什么东西?

本文将带你彻底搞懂 Kubernetes 调度策略的全貌,从基础概念到生产级完整配置,再到系统化的调试方法,一篇足够。

一、调度策略全景图

Kubernetes 的调度策略可以归纳为以下四个维度:

调度策略
├── 选机器(Node)
│   ├── nodeSelector(简单版:只能"要")
│   └── nodeAffinity(增强版:能"要"也能"不要")
│
├── 选 Pod
│   ├── podAffinity(要跟谁在一起)
│   └── podAntiAffinity(不要跟谁在一起)
│
└── 均匀分布
    └── topologySpreadConstraints(只看数量是否均匀)

1.1 核心概念速览

策略作用类比
nodeSelector必须调度到有特定标签的节点“我只坐靠窗位”
nodeAffinity灵活选择节点,支持"要"和"不要"“最好靠窗,不要过道”
podAffinity和特定 Pod 待在同一个拓扑域“我要和同事坐一起”
podAntiAffinity不和特定 Pod 待在同一个拓扑域“我不要和领导坐一起”
topologySpreadConstraints在拓扑域内均匀分布“每个车厢人数差不多”

二、节点亲和与反亲和(nodeAffinity)

2.1 基本概念

nodeAffinitynodeSelector 的升级版,它提供了更灵活的表达式匹配,并且支持"硬性"和"软性"两种模式。

关键字段

字段含义类比
requiredDuringSchedulingIgnoredDuringExecution硬性规则,不满足就不调度“非靠窗不坐”
preferredDuringSchedulingIgnoredDuringExecution软性规则,尽量满足,不行就算了“最好靠窗,没有也行”

⚠️ 注意:Kubernetes 没有独立的 nodeAntiAffinity 字段。节点反亲和是通过 operator: NotInoperator: DoesNotExistnodeAffinity 中实现的。

2.2 节点亲和 vs 反亲和

策略写法含义
节点亲和operator: In“我要去有 GPU 的节点”
节点反亲和operator: NotIn“我不要去有 GPU 的节点”
节点反亲和operator: DoesNotExist“我不要去带某个标签的节点”

2.3 nodeSelectorTerms 和 matchExpressions 的逻辑关系(重点!)

这是最容易搞混的地方,我们详细拆解。

nodeAffinity:
  requiredDuringSchedulingIgnoredDuringExecution:
    nodeSelectorTerms:        # 多个 term 之间是 OR 关系
    - matchExpressions:       # 同一个 term 内是 AND 关系
      - key: disk-type
        operator: In
        values: ["ssd"]
      - key: cpu-class
        operator: In
        values: ["high"]      # 这里 AND:必须同时满足 ssd AND high
    - matchExpressions:       # 这是第二个 term(OR 关系)
      - key: gpu-type
        operator: Exists      # 只要有 gpu-type 标签即可

逻辑公式

满足条件 = (disk-type=ssd AND cpu-class=high) OR (存在 gpu-type 标签)

速记口诀

  • matchExpressions 内部与(AND):必须同时满足
  • nodeSelectorTerms 内部或(OR):满足任意一个即可

2.4 常用 operator 详解

operator含义是否需要 values示例
In标签值在列表中✅ 需要values: ["ssd", "nvme"]
NotIn标签值不在列表中✅ 需要values: ["master"]
Exists标签存在(不管值)❌ 不需要只要有 gpu-type 标签即可
DoesNotExist标签不存在❌ 不需要没有 maintenance 标签
Gt标签值大于某值(数值比较)✅ 需要values: ["10"]
Lt标签值小于某值(数值比较)✅ 需要values: ["5"]

三、Pod 亲和与反亲和(podAffinity / podAntiAffinity)

3.1 核心区别

策略字段含义典型场景
Pod 亲和podAffinity“我要和某某 Pod 待在同一个拓扑域里”缓存和业务放一起,减少网络延迟
Pod 反亲和podAntiAffinity“我不要和某某 Pod 待在同一个拓扑域里”高可用,把副本分散到不同节点

3.2 拓扑域(topologyKey)详解

拓扑域定义了"在什么范围内算在一起/避开"。它是 podAffinitypodAntiAffinitytopologySpreadConstraints核心坐标系统

topologyKey 取值含义说明
kubernetes.io/hostname同一个节点最常用,节点默认自带此标签
topology.kubernetes.io/zone同一个可用区云环境自动注入
topology.kubernetes.io/region同一个地域云环境自动注入
自定义标签(如 rack同一个机柜需要自己给节点打标签

⚠️ 注意nodeAffinity 不需要拓扑域,因为它直接选节点,不依赖其他 Pod 的位置。

3.3 Pod 亲和性示例

podAffinity:
  requiredDuringSchedulingIgnoredDuringExecution:
  - labelSelector:
      matchLabels:
        app: redis-cache      # 找带有 app=redis-cache 标签的 Pod
    topologyKey: "topology.kubernetes.io/zone"  # 和它们在同一个可用区

3.4 Pod 反亲和示例

podAntiAffinity:
  preferredDuringSchedulingIgnoredDuringExecution:
  - weight: 100
    podAffinityTerm:
      labelSelector:
        matchLabels:
          app: web-server     # 找带有 app=web-server 标签的 Pod
      topologyKey: "kubernetes.io/hostname"  # 尽量不要和它们在同一个节点

四、拓扑分布约束(topologySpreadConstraints)

这是 Kubernetes 1.19+ 引入的专门用于均匀分布 Pod 的调度插件,可以看作是反亲和性的"专用增强版"。

4.1 核心参数

topologySpreadConstraints:
- maxSkew: 1                        # 最大数量偏差
  topologyKey: "kubernetes.io/hostname"  # 按什么维度统计
  whenUnsatisfiable: DoNotSchedule  # 不满足时怎么办
  labelSelector:                    # 统计哪些 Pod
    matchLabels:
      app: my-app

4.2 参数详解

参数含义说明
maxSkew最大偏差值表示"Pod 最多的拓扑域"与"Pod 最少的拓扑域"的数量差不能超过此值。设为 1 表示尽量做到完全均匀。
topologyKey拓扑域维度和亲和性中的 topologyKey 含义相同
whenUnsatisfiable不满足时的行为见下表
labelSelector统计哪些 Pod只统计匹配此标签的 Pod,不统计其他 Pod

4.3 whenUnsatisfiable 的两种模式

取值含义对应亲和性类比
DoNotSchedule(默认)硬性,不满足就不调度对应"强制反亲和"“必须隔开坐,没位置我就站着等”
ScheduleAnyway软性,尽力满足,不行也调度对应"优先反亲和"“尽量隔开坐,如果没位置,挤一挤也行”

4.4 与反亲和性的关键区别

对比维度PodTopologySpreadPodAntiAffinity
核心目标均匀分布数量决定跟谁同节点/避开谁
是否关心 Pod 身份只关心数量,不关心对方是谁非常关心对方是谁(通过 labelSelector 精确匹配)
配置复杂度简单,只有 maxSkewtopologyKeywhenUnsatisfiable复杂,可设置硬/软规则、权重、多种操作符
典型场景保证每个节点/可用区承载的 Pod 数量差不多保证主从不跑一起,或缓存和业务跑一起

五、调度器执行顺序(理解决策流程)

当 Pod 被创建后,调度器按以下顺序决策:

第 1 步:nodeSelector / nodeAffinity(硬性)
    ↓ 筛选出"能去哪些节点"
第 2 步:podAffinity / podAntiAffinity(硬性)
    ↓ 在这些节点中,剔除不满足 Pod 关系的节点
第 3 步:topologySpreadConstraints(硬性 DoNotSchedule)
    ↓ 再筛掉"会导致分布不均"的节点
第 4 步:preferred 软性规则(打分)
    ↓ 在剩余节点中,按权重打分排序
最终:选择分数最高的节点调度

关键原则:硬性规则(required)是"一票否决",软性规则(preferred)只是"打分偏好"。

六、生产级完整案例(三者组合 + 拓扑分布)

6.1 场景需求

部署一个高可用的 Web 应用(3 副本),要求:

  1. 节点要求:只跑在 Linux 节点上,最好是高性能节点,但绝对不要跑在 master 节点
  2. Pod 反亲和:3 个副本必须分散到不同的节点(高可用)
  3. Pod 亲和:如果集群中有 Redis 缓存 Pod,尽量和它在同一个可用区(低延迟)
  4. 均匀分布:在可用区层面尽量均匀分布

6.2 完整 YAML(带详细注释)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-app
  namespace: production
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web-app
  template:
    metadata:
      labels:
        app: web-app
        version: v2
    spec:
      # ============================================
      # 第一部分:节点选择(nodeAffinity)
      # ============================================
      affinity:
        nodeAffinity:
          # 硬性规则:必须满足
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              # Term 1:标准 Linux 节点,且不是 master(节点反亲和)
              - matchExpressions:
                - key: kubernetes.io/os
                  operator: In
                  values: ["linux"]
                - key: node-role.kubernetes.io/master
                  operator: NotIn          # 节点反亲和:不要 master
                  values: ["true"]
              # Term 2:或者专用 worker 节点的 Linux 节点(备选)
              - matchExpressions:
                - key: node-role.kubernetes.io/worker
                  operator: Exists         # 只要有 worker 标签就行
                - key: kubernetes.io/os
                  operator: In
                  values: ["linux"]
          
          # 软性规则:尽量满足(打分项)
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 80                    # 权重 80
            preference:
              matchExpressions:
              - key: node-type
                operator: In
                values: ["high-performance"]  # 尽量调度到高性能节点
          - weight: 20                    # 权重 20
            preference:
              matchExpressions:
              - key: disk-type
                operator: In
                values: ["ssd"]              # 有 SSD 更好,但权重低一些

        # ============================================
        # 第二部分:Pod 亲和(podAffinity)
        # ============================================
        podAffinity:
          # 软性:尽量和 Redis 在同一个可用区
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 100
            podAffinityTerm:
              labelSelector:
                matchLabels:
                  app: redis-cache          # 找 Redis Pod
              topologyKey: "topology.kubernetes.io/zone"  # 在可用区维度
              namespaces:                   # 限定命名空间
              - cache-system

        # ============================================
        # 第三部分:Pod 反亲和(podAntiAffinity)
        # ============================================
        podAntiAffinity:
          # 硬性:绝对不能和同应用的 Pod 在同一个节点
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values: ["web-app"]
            topologyKey: "kubernetes.io/hostname"  # 节点维度

      # ============================================
      # 第四部分:拓扑分布约束(topologySpreadConstraints)
      # ============================================
      topologySpreadConstraints:
      - maxSkew: 1                         # 最大偏差不超过 1
        topologyKey: "topology.kubernetes.io/zone"  # 按可用区统计
        whenUnsatisfiable: DoNotSchedule   # 硬性:必须均匀
        labelSelector:
          matchLabels:
            app: web-app
      
      - maxSkew: 2                         # 宽松一些
        topologyKey: "kubernetes.io/hostname"  # 按节点统计
        whenUnsatisfiable: ScheduleAnyway  # 软性:尽力而为
        labelSelector:
          matchLabels:
            app: web-app

      containers:
      - name: nginx
        image: nginx:1.21
        ports:
        - containerPort: 80
        resources:
          requests:
            cpu: "500m"
            memory: "512Mi"

6.3 这个配置的调度执行流程

调度器收到这个 Pod 后,按以下顺序决策:

第 1 步:硬性节点筛选(required nodeAffinity

筛选出同时满足以下条件的节点:

  • 操作系统是 Linux AND
  • 不是 master 节点 AND
  • (有 worker 标签) OR (是高性能节点)

结果:假设集群有 10 个节点,筛出 6 个符合条件的

第 2 步:硬性 Pod 反亲和筛选(required podAntiAffinity

在 6 个节点中,剔除掉已经运行了 app=web-app Pod 的节点(因为一个节点最多只能有一个本应用 Pod)。

结果:如果已有 2 个副本正在运行,6 个节点中可能只剩 4 个可用

第 3 步:硬性拓扑分布约束(whenUnsatisfiable: DoNotSchedule

在剩余节点中,进一步剔除会导致可用区分布不均的节点(maxSkew: 1)。

结果:确保最终 3 个副本分布在 3 个不同可用区

第 4 步:软性规则打分(权重加权)

对剩余候选节点进行加权打分,分数越高越优先:

规则权重加分项
高性能节点(node-type=high-performance80+80 分
有 SSD 磁盘20+20 分
与 Redis 在同一可用区100+100 分
节点级均匀分布(ScheduleAnyway-再加一些偏向分散的分数

最终选择总分最高的节点调度

七、Pod Pending 调试完整指南

本章节目标:当你遇到 Pod 一直处于 Pending 状态时,能够系统化地定位问题,而不是盲目地改 YAML。

7.1 Pod 调度状态机

在开始调试之前,先理解 Pod 调度的生命周期:

Pod 创建
    ↓
调度器开始调度(Pending)
    ↓
┌─────────────────────────────────────────────────────┐
│  1. Filter(过滤阶段)                             │
│  - nodeSelector/nodeAffinity                       │
│  - podAffinity/podAntiAffinity                     │
│  - topologySpreadConstraints                       │
│  - 资源是否充足                                    │
│  - 端口是否冲突                                    │
│  - PV/PVC 是否满足                                │
│  - 污点容忍(Taints/Tolerations)                  │
└─────────────────────────────────────────────────────┘
    ↓ 通过
┌─────────────────────────────────────────────────────┐
│  2. Score(打分阶段)                              │
│  - preferred 软性规则                              │
│  - 各调度插件打分                                  │
│  - 加权求和                                        │
└─────────────────────────────────────────────────────┘
    ↓ 选出最高分节点
┌─────────────────────────────────────────────────────┐
│  3. Bind(绑定阶段)                               │
│  - 将 Pod 绑定到节点                               │
└─────────────────────────────────────────────────────┘
    ↓
Pod 状态变为 Running

任何阶段失败,Pod 都会停留在 Pending 状态。

7.2 调试第一斧:使用 kubectl describe pod(最常用)

这是最直接、最常用的方法,90% 的问题都能在这里找到答案

命令
kubectl describe pod <pod-name> -n <namespace>
关键信息解读

describe 的输出中,最关键的三个部分:

1. Conditions 部分(Pod 当前状态)

Conditions:
  Type           Status
  PodScheduled   False     # False 表示还没调度成功
  Initialized    True
  Ready          False
  ContainersReady False

2. Status 部分

Status: Pending

3. Events 部分(最重要!)

Events 会列出调度器尝试调度的记录,以及失败原因。

常见错误信息对照表
错误信息关键字根本原因排查方向
didn't match node selectornodeSelector/nodeAffinity 硬性规则不满足检查节点标签是否匹配
didn't match pod affinity/anti-affinityPod 亲和/反亲和硬性规则不满足检查参照 Pod 是否存在和运行状态
didn't satisfy topology spread constraint拓扑分布约束不满足(DoNotSchedule检查节点拓扑标签和 maxSkew 设置
Insufficient cpu所有候选节点 CPU 资源不足降低 Pod 资源请求或扩容节点
Insufficient memory所有候选节点内存资源不足降低 Pod 资源请求或扩容节点
Insufficient ephemeral-storage节点临时存储不足清理节点镜像或扩容
node(s) had volume node affinity conflictPV 的节点亲和性与 Pod 冲突检查 PV 配置的 nodeAffinity
failed to find available persistent volumes to bindPVC 找不到匹配的 PV检查 StorageClass 或 PV 状态
node(s) didn't tolerate taint节点有污点,Pod 没有对应的容忍检查节点的 Taints 和 Pod 的 Tolerations
port x is already in use on node节点上的端口已被占用检查 hostPort 冲突
node(s) had no available disk节点磁盘空间不足清理节点磁盘
完整示例
$ kubectl describe pod web-app-7d8f9c-abcde -n production

Events:
  Type     Reason            Age   From               Message
  ----     ------            ----  ----               -------
  Warning  FailedScheduling  12s   default-scheduler  0/6 nodes are available: 
                                                      3 node(s) didn't match node selector, 
                                                      2 node(s) didn't match pod affinity rules, 
                                                      1 node(s) Insufficient memory.

解读

  • 6 个节点全部不可用
  • 3 个节点:nodeSelector/nodeAffinity 不匹配 → 检查节点标签
  • 2 个节点:podAffinity/podAntiAffinity 不满足 → 检查参照 Pod
  • 1 个节点:内存不足 → 检查节点资源使用情况

7.3 调试第二斧:节点信息检查

当事件信息指向节点相关问题时,需要进一步检查节点。

7.3.1 查看节点标签
# 查看所有节点的全部标签
kubectl get nodes --show-labels

# 按标签筛选节点(例如:找有 GPU 的节点)
kubectl get nodes -l gpu-type=nvidia

# 查看特定节点的所有标签
kubectl describe node <node-name> | grep -A 10 "Labels"

常见问题

  • YAML 中写了 topology.kubernetes.io/zone,但节点上根本没有这个标签
  • nodeSelectorTerms 中使用了 operator: Exists,但节点的标签名拼写错误
7.3.2 查看节点资源使用情况
kubectl describe node <node-name>

关注以下字段:

Capacity:
  cpu:                16
  memory:             ########Ki
Allocatable:
  cpu:                15
  memory:             ########Ki
Allocated resources:
  (Total limits may be over 100 percent, i.e., overcommitted.)
  Resource           Requests    Limits
  cpu                10 (66%)    8 (53%)
  memory             20Gi (60%)  15Gi (45%)

如果 Requests 接近或超过 Allocatable,节点可能无法容纳新的 Pod。

7.3.3 查看节点污点(Taints)
kubectl describe node <node-name> | grep -A 5 "Taints"

如果有污点且 Pod 没有对应的容忍(Tolerations),Pod 无法调度到该节点。

7.4 调试第三斧:检查和 Pod 相关的其他资源

7.4.1 检查参照 Pod(针对 Pod 亲和性)

podAffinity 使用 requiredDuringScheduling 时,调度器会去找已经运行、带有匹配标签的 Pod

# 检查参照 Pod 是否存在
kubectl get pods -n <namespace> -l <label-key>=<label-value>

# 查看参照 Pod 的分布情况
kubectl get pods -n <namespace> -l app=redis-cache -o wide

可能的问题

  • 参照 Pod 不存在 → 需要先启动参照 Pod
  • 参照 Pod 分布在所有可用节点上 → 没有节点可以满足"和参照 Pod 一起"的要求
7.4.2 检查 PVC 和 PV(针对存储相关错误)
# 查看 PVC 状态
kubectl get pvc -n <namespace>

# 查看 PVC 详情
kubectl describe pvc <pvc-name> -n <namespace>

# 查看 PV 状态
kubectl get pv

# 查看 PV 的节点亲和性
kubectl describe pv <pv-name> | grep -A 10 "Node Affinity"

常见问题

  • PVC 处于 Pending 状态 → StorageClass 或 PV 配置有问题
  • PV 的 nodeAffinity 限制了只能调度到特定节点,但该节点资源不足

7.5 调试第四斧:调度器日志分析(深度调试)

kubectl describe 的信息不足以定位问题时,需要查看调度器日志。

7.5.1 找到调度器 Pod
# 查找 kube-scheduler Pod
kubectl get pods -n kube-system | grep scheduler

# 输出示例
kube-scheduler-control-plane   1/1     Running   0          12d
7.5.2 查看调度器日志
# 查看最近 200 行日志
kubectl logs <scheduler-pod-name> -n kube-system --tail=200

# 过滤特定 Pod 的日志
kubectl logs <scheduler-pod-name> -n kube-system --tail=500 | grep <pod-name>

# 实时跟踪日志
kubectl logs -f <scheduler-pod-name> -n kube-system
7.5.3 调整日志级别(获取更详细信息)

默认的调度器日志级别(--v=0)信息较少。如果需要更详细的调度决策信息,可以修改调度器启动参数。

方法:在 kube-scheduler 的 manifest 文件中添加 --v=4

# /etc/kubernetes/manifests/kube-scheduler.yaml
spec:
  containers:
  - command:
    - kube-scheduler
    - --v=4              # 增加日志级别
    - --leader-elect=true

⚠️ 生产环境注意--v=4 会产生大量日志,建议只在调试时开启,完成后恢复为 --v=0--v=1

不同日志级别对应的详细程度

级别输出内容
--v=0只输出重要事件(调度失败、Leader 选举等)
--v=1添加调度成功/失败的基本信息
--v=2添加调度器缓存的变更信息
--v=3添加 Filter 阶段的详细信息(哪些节点被过滤及原因)
--v=4添加 Score 阶段的详细信息(每个节点的各项打分)
--v=5添加调度队列和处理器的详细状态
7.5.4 日志解读示例

Filter 阶段日志(--v=4 级别)

I0825 10:00:01.123456   1 scheduler.go:497] "Attempting to schedule pod" pod="production/web-app-7d8f9c-abcde"
I0825 10:00:01.124567   1 filter.go:45] "Filtering pod" pod="production/web-app-7d8f9c-abcde"
I0825 10:00:01.125678   1 nodeaffinity.go:78] "Node didn't match node selector" node="node-1" pod="production/web-app-7d8f9c-abcde" reason="node-role.kubernetes.io/master in [true]"
I0825 10:00:01.126789   1 podaffinity.go:92] "Node didn't match pod affinity" node="node-2" pod="production/web-app-7d8f9c-abcde" reason="no matching pods found in zone"
I0825 10:00:01.127890   1 resource.go:56] "Node has insufficient memory" node="node-3" pod="production/web-app-7d8f9c-abcde" request=2Gi available=1.5Gi
I0825 10:00:01.128901   1 scheduler.go:512] "Filtered out nodes" pod="production/web-app-7d8f9c-abcde" filteredNodes=0

解读

  • node-1:节点反亲和规则不匹配(是 master 节点)
  • node-2:Pod 亲和规则不满足(zone 中没有匹配的 Pod)
  • node-3:内存不足
  • 最终没有节点通过过滤,Pod 将保持 Pending

Score 阶段日志(--v=4 级别)

I0825 10:00:02.123456   1 scheduler.go:530] "Scoring nodes" pod="production/web-app-7d8f9c-abcde" nodesCount=3
I0825 10:00:02.124567   1 nodeaffinity.go:120] "Scored node with node affinity preference" node="node-4" score=80
I0825 10:00:02.125678   1 nodeaffinity.go:120] "Scored node with node affinity preference" node="node-5" score=80
I0825 10:00:02.126789   1 nodeaffinity.go:120] "Scored node with node affinity preference" node="node-6" score=0
I0825 10:00:02.127890   1 podaffinity.go:150] "Scored node with pod affinity" node="node-4" score=100
I0825 10:00:02.128901   1 podaffinity.go:150] "Scored node with pod affinity" node="node-5" score=0
I0825 10:00:02.129012   1 podaffinity.go:150] "Scored node with pod affinity" node="node-6" score=0
I0825 10:00:02.130123   1 selector_score.go:45] "Total scores" pod="production/web-app-7d8f9c-abcde" scores="node-4:180, node-5:80, node-6:0"
I0825 10:00:02.131234   1 scheduler.go:550] "Selected node" pod="production/web-app-7d8f9c-abcde" node="node-4" score=180

解读

  • node-4:节点亲和 +80 分,Pod 亲和 +100 分,总分 180 → 被选中
  • node-5:节点亲和 +80 分,Pod 亲和 0 分,总分 80
  • node-6:两个软性规则都不满足,总分 0
7.5.5 补充检查:查看 Pod 的 Conditions 字段

除了 kubectl describe pod 和调度器日志,还可以直接查看 Pod 的 .status.conditions 字段,获取调度状态的详细信息。

💡 说明.status.conditions 是 Pod 状态的一部分,由调度器自动写入,不需要额外开启任何功能。它是 kubectl describe podEventsConditions 部分的数据来源。

命令

kubectl get pod <pod-name> -n <namespace> -o yaml | grep -A 20 "conditions:"

输出示例(处于 Pending 状态的 Pod)

conditions:
- lastProbeTime: null
  lastTransitionTime: "2026-08-25T10:00:00Z"
  message: '0/6 nodes are available: 3 node(s) didn''t match node selector,
           2 node(s) didn''t match pod affinity rules,
           1 node(s) Insufficient memory.'
  reason: Unschedulable
  status: "False"
  type: PodScheduled

字段解读

字段含义常见值
type条件类型PodScheduled(是否已调度)、Initialized(是否已初始化)、Ready(是否就绪)
status状态True/False/Unknown
reason状态原因Unschedulable(无法调度)、Scheduled(已调度)
message详细描述具体的失败原因,如哪些节点不满足哪些条件

适用场景

kubectl describe pod 的 Events 信息被截断或不完整时,此方法可以获取完整的 message 内容,方便复制粘贴进行分析。

与其他调试手段的关系

调试手段优势适用场景
kubectl describe pod → Events信息最全,含调度尝试记录首选,90% 问题都能定位
kubectl get pod -o yaml → conditions结构化的状态信息需要复制 message 内容、或 Events 被截断时
调度器日志(--v=4含 Filter/Score 详细过程前两者信息不足时深度排查

建议:优先使用 kubectl describe pod,只有在其信息不够详细或需要精确复制错误信息时,再使用此方法查看 .status.conditions

7.6 调试第五斧:查看调度器 Metrics 与 Profiling 接口

Kubernetes 调度器暴露了 Prometheus 格式的 Metrics,可以用于监控和问题分析。对于 v1.25+ 的集群,还可以开启 性能分析(Profiling)接口,获取更底层的调度器运行数据。

7.6.1 启用 Metrics 和 Profiling

kube-scheduler 的启动参数中添加以下配置:

# /etc/kubernetes/manifests/kube-scheduler.yaml
spec:
  containers:
  - command:
    - kube-scheduler
    - --bind-address=0.0.0.0          # 绑定所有网络接口
    - --secure-port=10259              # 安全端口(默认 10259)
    - --profiling=true                 # 启用性能分析(默认 false)
    - --v=2
    - --leader-elect=true

⚠️ 生产环境注意

  • --profiling=true 会暴露性能调试接口,建议仅在调试时开启,完成后关闭
  • --bind-address=0.0.0.0 会监听所有网络接口,生产环境中建议通过防火墙或网络策略限制访问来源
  • 修改后 kube-scheduler 会自动重启(静态 Pod)
7.6.2 获取调度器 Metrics(三种方式对比)

方式一:通过 kubectl proxy 获取完整 Metrics(推荐)

# 启动 kubectl proxy
kubectl proxy --port=8080 &

# 通过 API Server 代理访问调度器的完整 Metrics
curl http://localhost:8080/api/v1/namespaces/kube-system/services/kube-scheduler:https/proxy/metrics | grep scheduler

路径拆解

/api/v1/namespaces/kube-system/services/kube-scheduler:https/proxy/metrics
   │            │              │
   │            │              └── 调度器的 Service 名称
   │            └── kube-scheduler 所在的命名空间
   └── Kubernetes API Server 的标准代理路径

方式二:快速查看(可能不完整)

# 通过 API Server 聚合的 Metrics(数据来源不是调度器自身,可能不完整)
curl http://localhost:8080/metrics | grep scheduler

⚠️ 这种方式获取的是 API Server 聚合层的 Metrics,可能只包含部分调度器指标,仅适合快速查看,调试时建议使用方式一。

方式三:直接访问调度器 Pod(需要认证)

# 获取 kube-scheduler 的 token
TOKEN=$(kubectl get secret -n kube-system $(kubectl get sa -n kube-system kube-scheduler -o jsonpath='{.secrets[0].name}') -o jsonpath='{.data.token}' | base64 -d)

# 通过 port-forward 访问
kubectl port-forward -n kube-system <scheduler-pod> 10259:10259 &
curl -k https://localhost:10259/metrics -H "Authorization: Bearer $TOKEN" | grep scheduler

三种方式对比总结

方式命令数据来源完整度推荐场景
方式一(推荐)/api/v1/.../proxy/metrics调度器自身✅ 完整调试、排查问题
方式二(快速查看)/metricsAPI Server 聚合层⚠️ 可能不完整快速看一眼
方式三(直连 Pod)port-forward + Pod IP调度器自身✅ 完整无法使用 proxy 时
7.6.3 关键 Metrics 指标解读
Metric 名称类型含义关注阈值
scheduler_schedule_attempts_totalCounter调度尝试总次数(按结果分:scheduled/error/unschedulable)持续增长
scheduler_schedule_attempts_failedCounter调度失败总次数关注 unschedulable 比例
scheduler_pod_scheduling_attemptsHistogramPod 从入队到调度的尝试次数分布尝试次数 > 3 说明有问题
scheduler_pending_podsGauge当前 Pending 的 Pod 数量(按队列分:active/backoff/unschedulable)> 0 需要关注
scheduler_filter_duration_secondsHistogramFilter 阶段耗时P99 > 1s 说明过滤逻辑复杂
scheduler_score_duration_secondsHistogramScore 阶段耗时P99 > 1s 说明打分逻辑复杂
scheduler_goroutinesGauge调度器 Goroutine 数量持续异常增长可能有泄漏
scheduler_cache_sizeGauge调度器缓存中的 Pod/Node 数量用于判断缓存是否正常
7.6.4 查看调度器 Profiling 接口(v1.25+)

--profiling=true 启用后,可以通过以下接口获取更详细的性能数据:

# 查看调度器性能概况(注意:使用完整路径)
curl -k https://<scheduler-ip>:10259/debug/pprof/ -H "Authorization: Bearer $TOKEN"

# 或通过 kubectl proxy 访问
curl http://localhost:8080/api/v1/namespaces/kube-system/services/kube-scheduler:https/proxy/debug/pprof/

输出示例

/debug/pprof/
 
Types available:
allocs
block
cmdline
goroutine
heap
mutex
profile
threadcreate
trace

常用 Profiling 接口及用途

接口路径用途使用场景
/debug/pprof/profile?seconds=30CPU 性能采样分析调度器 CPU 使用热点
/debug/pprof/heap堆内存分配分析分析调度器内存使用和可能的泄漏
/debug/pprof/goroutine当前 Goroutine 栈信息排查 Goroutine 泄漏或死锁
/debug/pprof/block阻塞操作分析分析调度器阻塞在哪里
/debug/pprof/trace?seconds=5运行时追踪分析调度器执行轨迹
/debug/pprof/allocs内存分配采样分析内存分配热点

实战示例:采集调度器 CPU Profile

# 采集 30 秒的 CPU Profile(使用完整路径)
curl -k "https://<scheduler-ip>:10259/debug/pprof/profile?seconds=30" \
     -H "Authorization: Bearer $TOKEN" \
     -o scheduler-cpu-profile.pprof

# 使用 go tool 分析
go tool pprof -http=:8080 scheduler-cpu-profile.pprof

实战示例:采集调度器堆内存信息

# 采集堆内存信息
curl -k "https://<scheduler-ip>:10259/debug/pprof/heap" \
     -H "Authorization: Bearer $TOKEN" \
     -o scheduler-heap.pprof

# 分析内存分配情况
go tool pprof -http=:8080 scheduler-heap.pprof
7.6.5 调度器 Metrics 监控告警建议

如果使用 Prometheus + Alertmanager,建议配置以下告警规则:

groups:
- name: scheduler
  rules:
  # 告警 1:调度器有大量 Pending Pod
  - alert: SchedulerPendingPods
    expr: scheduler_pending_pods > 10
    for: 5m
    annotations:
      summary: "调度器有 {{ $value }} 个 Pod 处于 Pending 状态"
  
  # 告警 2:调度失败率过高
  - alert: SchedulerHighFailureRate
    expr: rate(scheduler_schedule_attempts_total{result="unschedulable"}[5m]) / rate(scheduler_schedule_attempts_total[5m]) > 0.1
    for: 5m
    annotations:
      summary: "调度失败率超过 10%"
  
  # 告警 3:调度器 Filter 阶段耗时过长
  - alert: SchedulerFilterSlow
    expr: histogram_quantile(0.99, rate(scheduler_filter_duration_seconds_bucket[5m])) > 1
    annotations:
      summary: "调度器 Filter 阶段 P99 耗时超过 1s"
7.6.6 调试决策树:何时使用哪种 Metrics/Profiling
问题现象推荐使用的调试手段预期获得的信息
Pod 偶尔调度失败,但不确定原因查看 scheduler_pending_podsscheduler_schedule_attempts_failed确认失败频率和趋势
调度器响应慢,Pod 长时间 Pending查看 scheduler_filter_duration_secondsscheduler_score_duration_seconds确认哪个阶段耗时最长
调度器内存持续增长采集 /debug/pprof/heap分析内存分配热点,排查内存泄漏
调度器 CPU 使用率异常高采集 /debug/pprof/profile分析 CPU 热点函数
调度器疑似死锁或卡死采集 /debug/pprof/goroutine查看所有 Goroutine 的调用栈
调度器吞吐量不足查看 scheduler_schedule_attempts_total 的速率评估调度吞吐量

7.7 快速定位问题流程图

kubectl describe pod <pod-name>
    │
    ├─ Events 有明确错误信息
    │   │
    │   ├─ 包含 "node selector" → 检查 nodeAffinity/nodeSelector
    │   │   └─ kubectl get nodes --show-labels
    │   │
    │   ├─ 包含 "pod affinity" → 检查 podAffinity/podAntiAffinity
    │   │   └─ kubectl get pods -l <label> -o wide
    │   │
    │   ├─ 包含 "topology spread" → 检查 topologySpreadConstraints
    │   │   └─ kubectl get nodes -l <topology-key>
    │   │
    │   ├─ 包含 "Insufficient" → 检查资源
    │   │   └─ kubectl describe node <node-name>(查看 Allocatable)
    │   │
    │   ├─ 包含 "volume" → 检查 PVC/PV/StorageClass
    │   │   └─ kubectl get pvc -n <ns> && kubectl get pv
    │   │
    │   ├─ 包含 "taint" → 检查污点与容忍
    │   │   └─ kubectl describe node <node-name> | grep Taints
    │   │
    │   └─ 包含 "port" → 检查端口冲突
    │       └─ kubectl describe node <node-name> | grep -A 5 "Allocated ports"
    │
    ├─ Events 信息不够详细
    │   └─ 查看调度器日志(需增加 --v=4)
    │       └─ kubectl logs <scheduler-pod> -n kube-system --tail=500
    │
    └─ 仍然无法定位
        └─ 检查调度器 Metrics
            └─ curl <scheduler-ip>:10259/metrics | grep scheduler

7.8 实战案例:逐步排查一个 Pending Pod

场景

部署了一个 Deployment,3 个副本,但只有 1 个跑起来,另外 2 个一直 Pending。

步骤 1:查看 Pod 事件
kubectl describe pod web-app-7d8f9c-defgh -n production

输出:

Events:
  Warning  FailedScheduling  30s   default-scheduler  0/6 nodes are available: 
                                                      4 node(s) didn't match pod anti-affinity rules, 
                                                      2 node(s) didn't satisfy topology spread constraint.
步骤 2:分析
  • 4 个节点被 Pod 反亲和规则过滤 → 这些节点已有 app=web-app 的 Pod
  • 2 个节点不满足拓扑分布约束 → 可用区分布不均
步骤 3:检查 Pod 分布
kubectl get pods -n production -l app=web-app -o wide

输出:

NAME                    READY   STATUS    NODE           ZONE
web-app-7d8f9c-abcde    1/1     Running   node-1         zone-a
web-app-7d8f9c-defgh    0/1     Pending   <none>         <none>
web-app-7d8f9c-ijklm    0/1     Pending   <none>         <none>
步骤 4:检查节点拓扑标签
kubectl get nodes --show-labels | grep -E "NAME|topology"

输出:

NAME     STATUS   LABELS
node-1   Ready    topology.kubernetes.io/zone=zone-a
node-2   Ready    topology.kubernetes.io/zone=zone-a
node-3   Ready    topology.kubernetes.io/zone=zone-a
node-4   Ready    topology.kubernetes.io/zone=zone-b
node-5   Ready    topology.kubernetes.io/zone=zone-b
node-6   Ready    topology.kubernetes.io/zone=zone-b
步骤 5:查看调度器日志(深度定位)
kubectl logs kube-scheduler-control-plane -n kube-system --tail=300 | grep web-app

日志显示:

I0825 10:00:01.125678   1 nodeaffinity.go:78] "Node didn't match node selector" node="node-1" reason="pod anti-affinity: already has web-app pod"
I0825 10:00:01.126789   1 nodeaffinity.go:78] "Node didn't match node selector" node="node-2" reason="pod anti-affinity: already has web-app pod"
I0825 10:00:01.127890   1 nodeaffinity.go:78] "Node didn't match node selector" node="node-3" reason="pod anti-affinity: already has web-app pod"
I0825 10:00:01.128901   1 nodeaffinity.go:78] "Node didn't match node selector" node="node-4" reason="node has taint: dedicated=ml:NoSchedule"
I0825 10:00:01.129012   1 nodeaffinity.go:78] "Node didn't match node selector" node="node-5" reason="node has taint: dedicated=ml:NoSchedule"
I0825 10:00:01.130123   1 resource.go:56] "Node has insufficient memory" node="node-6" request=2Gi available=1.2Gi

发现更多细节,最终定位到:zone-b 的 3 个节点中,有 2 个节点有污点(Taint),1 个节点资源不足,所以实际上没有可用的节点。

🔍 问题定位结论(根因分析)

通过上述排查,我们可以得出以下结论:

根本原因:集群中有 6 个节点,但没有一个节点能同时满足所有硬性调度条件,导致 2 个新 Pod 无法调度。

节点所在可用区被过滤原因问题性质
node-1zone-a已有 web-app Pod(反亲和)硬性规则
node-2zone-a已有 web-app Pod(反亲和)硬性规则
node-3zone-a已有 web-app Pod(反亲和)硬性规则
node-4zone-b有污点 dedicated=ml:NoSchedule,Pod 无对应容忍硬性规则
node-5zone-b有污点 dedicated=ml:NoSchedule,Pod 无对应容忍硬性规则
node-6zone-b内存不足(可用 1.2Gi < 请求 2Gi)资源不足

问题本质

  • zone-a 的 3 个节点都有 podAntiAffinity 的约束,已满
  • zone-b 的 3 个节点中:2 个有污点无法调度,1 个资源不足
  • 硬性拓扑分布约束(maxSkew: 1)要求副本在可用区之间均匀分布,但 zone-b 根本没有可用节点,因此无法完成调度

关键发现

表面上看是"拓扑分布约束"导致的 Pending,但根本原因是 zone-b 没有可用的节点资源(污点 + 内存不足)。拓扑分布约束只是"最后一根稻草"——它发现了 zone-b 没有可用节点后,阻止了 Pod 全部堆积到 zone-a 的行为,从而暴露了底层资源问题。

步骤 6:解决方案
  • 方案 A:放宽 topologySpreadConstraintsmaxSkew: 2
  • 方案 B:将 whenUnsatisfiableDoNotSchedule 改为 ScheduleAnyway
  • 方案 C:增加 zone-b 的节点或清理资源

7.9 调试命令速查表

目的命令
看 Pod 事件(最快)kubectl describe pod <pod-name> -n <ns>
看节点标签kubectl get nodes --show-labels
按标签筛选节点kubectl get nodes -l <key>=<value>
看节点资源情况kubectl describe node <node-name>
看节点污点`kubectl describe node
看参照 Pod 分布kubectl get pods -l <label> -n <ns> -o wide
看 PVC 状态kubectl get pvc -n <ns>
看 PV 状态kubectl get pv
看所有 Pending Pod`kubectl get pods -A
看调度器日志(默认级别)kubectl logs -n kube-system <scheduler-pod> --tail=200
看调度器日志(详细级别)`kubectl logs -n kube-system --tail=500
查看调度器 Metrics`curl :10259/metrics

八、常见陷阱与最佳实践

8.1 陷阱一:nodeSelectorTerms 多个 term 是 OR,不是 AND

错误理解

nodeSelectorTerms:
- matchExpressions:
  - key: disk-type
    operator: In
    values: ["ssd"]
- matchExpressions:      # 错误!这是第二个 term,不是第二个条件
  - key: cpu-class
    operator: In
    values: ["high"]

这会匹配 ssd high,而不是 ssd AND high

正确写法(AND)

nodeSelectorTerms:
- matchExpressions:      # 同一个 term 下
  - key: disk-type
    operator: In
    values: ["ssd"]
  - key: cpu-class       # 这是 AND 关系
    operator: In
    values: ["high"]

8.2 陷阱二:operator: Exists 不需要 values

# ✅ 正确
- key: gpu-type
  operator: Exists       # 只要有这个标签就行

# ❌ 错误(Exists 不能跟 values)
- key: gpu-type
  operator: Exists
  values: ["nvidia"]     # 会报错!

8.3 陷阱三:硬性规则过多导致 Pod 永远 Pending

生产环境中,硬性规则(required)越少越好。建议:

  • 能用 preferred 就别用 required
  • 多个硬性规则叠加时,考虑是否有节点能同时满足

8.4 陷阱四:拓扑域标签不存在

如果节点上没有 topology.kubernetes.io/zone 标签,使用它的调度规则会永远无法满足,Pod 会一直 Pending。

检查命令:

kubectl get nodes --show-labels

8.5 最佳实践总结

实践说明
优先使用软性规则避免硬性规则过多导致 Pod 无法调度
合理设置权重preferred 规则的 weight 值反映优先级,1-100 范围
检查节点标签使用拓扑域前,确认节点上有对应的标签
组合使用时注意层级硬性规则先执行,软性规则后打分
调试时查看 Eventskubectl describe pod 是定位问题最快的方式

九、完整速查小抄

9.1 逻辑关系速查

层级关系说明
matchExpressions 内部AND必须同时满足
nodeSelectorTerms 内部OR满足任意一个即可

9.2 operator 速查

operator含义是否需要 values
In标签值在列表中✅ 需要
NotIn标签值不在列表中✅ 需要
Exists标签存在(不管值)❌ 不需要
DoesNotExist标签不存在❌ 不需要
Gt标签值大于某值✅ 需要
Lt标签值小于某值✅ 需要

9.3 topologyKey 速查

topologyKey含义说明
kubernetes.io/hostname节点级别最常用,节点默认自带
topology.kubernetes.io/zone可用区级别云环境自动注入
topology.kubernetes.io/region地域级别云环境自动注入
自定义标签自定义维度需手动给节点打标签

9.4 策略对照表

策略字段名选什么反亲和写法是否需要 topologyKey硬/软支持
节点亲和nodeAffinityNodeNotIn/DoesNotExist❌ 不需要✅ 都支持
Pod 亲和podAffinityPod❌ 无必须✅ 都支持
Pod 反亲和podAntiAffinityPod✅ 本身就是"反"必须✅ 都支持
拓扑分布topologySpreadConstraints统计数量❌ 无必须✅ 通过 whenUnsatisfiable 控制

结语

本文从调度策略全景图出发,逐步深入到:

  • nodeAffinity 的节点亲和/反亲和配置
  • nodeSelectorTermsmatchExpressions 的 AND/OR 逻辑关系
  • podAffinity/podAntiAffinity 的 Pod 级调度控制
  • topologySpreadConstraints 的均匀分布策略
  • 生产级完整 YAML 案例(三者组合使用)
  • 系统化的 Pod Pending 调试方法(五斧调试法 + 调度器日志分析)

希望这篇文章能帮你彻底搞懂 Kubernetes 的调度策略体系,在生产环境中从容应对各种调度需求。如果你在实际配置中遇到问题,可以按照文中的调试流程一步步定位。


📌 本文配套资源

  • 完整 YAML 示例已包含在第六章
  • 调试命令速查表见 7.9 节
  • 常见陷阱与最佳实践见第八章

如果觉得本文对你有帮助,欢迎点赞、收藏、转发! 😊

更多推荐