Kubernetes 调度策略完全指南:亲和性/反亲和性/拓扑分布从入门到生产实战
一文搞懂 nodeAffinity、podAffinity、podAntiAffinity、topologySpreadConstraints,附完整 YAML 案例 + 调度失败调试方法
前言
在 Kubernetes 集群中,调度策略是决定 Pod 落在哪台机器上的核心机制。你是否遇到过这些问题:
- Pod 一直
Pending,但不知道是哪个规则卡住了? nodeSelectorTerms和matchExpressions的逻辑到底是 AND 还是 OR?podAffinity和podAntiAffinity到底有什么区别,什么时候用哪个?- 拓扑域(
topologyKey)到底是个什么东西?
本文将带你彻底搞懂 Kubernetes 调度策略的全貌,从基础概念到生产级完整配置,再到系统化的调试方法,一篇足够。
一、调度策略全景图
Kubernetes 的调度策略可以归纳为以下四个维度:
调度策略
├── 选机器(Node)
│ ├── nodeSelector(简单版:只能"要")
│ └── nodeAffinity(增强版:能"要"也能"不要")
│
├── 选 Pod
│ ├── podAffinity(要跟谁在一起)
│ └── podAntiAffinity(不要跟谁在一起)
│
└── 均匀分布
└── topologySpreadConstraints(只看数量是否均匀)
1.1 核心概念速览
| 策略 | 作用 | 类比 |
|---|---|---|
nodeSelector | 必须调度到有特定标签的节点 | “我只坐靠窗位” |
nodeAffinity | 灵活选择节点,支持"要"和"不要" | “最好靠窗,不要过道” |
podAffinity | 和特定 Pod 待在同一个拓扑域 | “我要和同事坐一起” |
podAntiAffinity | 不和特定 Pod 待在同一个拓扑域 | “我不要和领导坐一起” |
topologySpreadConstraints | 在拓扑域内均匀分布 | “每个车厢人数差不多” |
二、节点亲和与反亲和(nodeAffinity)
2.1 基本概念
nodeAffinity 是 nodeSelector 的升级版,它提供了更灵活的表达式匹配,并且支持"硬性"和"软性"两种模式。
关键字段:
| 字段 | 含义 | 类比 |
|---|---|---|
requiredDuringSchedulingIgnoredDuringExecution | 硬性规则,不满足就不调度 | “非靠窗不坐” |
preferredDuringSchedulingIgnoredDuringExecution | 软性规则,尽量满足,不行就算了 | “最好靠窗,没有也行” |
⚠️ 注意:Kubernetes 没有独立的
nodeAntiAffinity字段。节点反亲和是通过operator: NotIn或operator: DoesNotExist在nodeAffinity中实现的。
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)详解
拓扑域定义了"在什么范围内算在一起/避开"。它是 podAffinity、podAntiAffinity 和 topologySpreadConstraints 的核心坐标系统。
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 与反亲和性的关键区别
| 对比维度 | PodTopologySpread | PodAntiAffinity |
|---|---|---|
| 核心目标 | 均匀分布数量 | 决定跟谁同节点/避开谁 |
| 是否关心 Pod 身份 | 只关心数量,不关心对方是谁 | 非常关心对方是谁(通过 labelSelector 精确匹配) |
| 配置复杂度 | 简单,只有 maxSkew、topologyKey、whenUnsatisfiable | 复杂,可设置硬/软规则、权重、多种操作符 |
| 典型场景 | 保证每个节点/可用区承载的 Pod 数量差不多 | 保证主从不跑一起,或缓存和业务跑一起 |
五、调度器执行顺序(理解决策流程)
当 Pod 被创建后,调度器按以下顺序决策:
第 1 步:nodeSelector / nodeAffinity(硬性)
↓ 筛选出"能去哪些节点"
第 2 步:podAffinity / podAntiAffinity(硬性)
↓ 在这些节点中,剔除不满足 Pod 关系的节点
第 3 步:topologySpreadConstraints(硬性 DoNotSchedule)
↓ 再筛掉"会导致分布不均"的节点
第 4 步:preferred 软性规则(打分)
↓ 在剩余节点中,按权重打分排序
最终:选择分数最高的节点调度
关键原则:硬性规则(
required)是"一票否决",软性规则(preferred)只是"打分偏好"。
六、生产级完整案例(三者组合 + 拓扑分布)
6.1 场景需求
部署一个高可用的 Web 应用(3 副本),要求:
- 节点要求:只跑在 Linux 节点上,最好是高性能节点,但绝对不要跑在 master 节点
- Pod 反亲和:3 个副本必须分散到不同的节点(高可用)
- Pod 亲和:如果集群中有 Redis 缓存 Pod,尽量和它在同一个可用区(低延迟)
- 均匀分布:在可用区层面尽量均匀分布
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-performance) | 80 | +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 selector | nodeSelector/nodeAffinity 硬性规则不满足 | 检查节点标签是否匹配 |
didn't match pod affinity/anti-affinity | Pod 亲和/反亲和硬性规则不满足 | 检查参照 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 conflict | PV 的节点亲和性与 Pod 冲突 | 检查 PV 配置的 nodeAffinity |
failed to find available persistent volumes to bind | PVC 找不到匹配的 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 pod中Events和Conditions部分的数据来源。
命令
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 | 调度器自身 | ✅ 完整 | 调试、排查问题 |
| 方式二(快速查看) | /metrics | API Server 聚合层 | ⚠️ 可能不完整 | 快速看一眼 |
| 方式三(直连 Pod) | port-forward + Pod IP | 调度器自身 | ✅ 完整 | 无法使用 proxy 时 |
7.6.3 关键 Metrics 指标解读
| Metric 名称 | 类型 | 含义 | 关注阈值 |
|---|---|---|---|
scheduler_schedule_attempts_total | Counter | 调度尝试总次数(按结果分:scheduled/error/unschedulable) | 持续增长 |
scheduler_schedule_attempts_failed | Counter | 调度失败总次数 | 关注 unschedulable 比例 |
scheduler_pod_scheduling_attempts | Histogram | Pod 从入队到调度的尝试次数分布 | 尝试次数 > 3 说明有问题 |
scheduler_pending_pods | Gauge | 当前 Pending 的 Pod 数量(按队列分:active/backoff/unschedulable) | > 0 需要关注 |
scheduler_filter_duration_seconds | Histogram | Filter 阶段耗时 | P99 > 1s 说明过滤逻辑复杂 |
scheduler_score_duration_seconds | Histogram | Score 阶段耗时 | P99 > 1s 说明打分逻辑复杂 |
scheduler_goroutines | Gauge | 调度器 Goroutine 数量 | 持续异常增长可能有泄漏 |
scheduler_cache_size | Gauge | 调度器缓存中的 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=30 | CPU 性能采样 | 分析调度器 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_pods 和 scheduler_schedule_attempts_failed | 确认失败频率和趋势 |
| 调度器响应慢,Pod 长时间 Pending | 查看 scheduler_filter_duration_seconds 和 scheduler_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-1 | zone-a | 已有 web-app Pod(反亲和) | 硬性规则 |
| node-2 | zone-a | 已有 web-app Pod(反亲和) | 硬性规则 |
| node-3 | zone-a | 已有 web-app Pod(反亲和) | 硬性规则 |
| node-4 | zone-b | 有污点 dedicated=ml:NoSchedule,Pod 无对应容忍 | 硬性规则 |
| node-5 | zone-b | 有污点 dedicated=ml:NoSchedule,Pod 无对应容忍 | 硬性规则 |
| node-6 | zone-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:放宽
topologySpreadConstraints的maxSkew: 2 - 方案 B:将
whenUnsatisfiable从DoNotSchedule改为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 范围 |
| 检查节点标签 | 使用拓扑域前,确认节点上有对应的标签 |
| 组合使用时注意层级 | 硬性规则先执行,软性规则后打分 |
| 调试时查看 Events | kubectl 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 | 硬/软支持 |
|---|---|---|---|---|---|
| 节点亲和 | nodeAffinity | Node | NotIn/DoesNotExist | ❌ 不需要 | ✅ 都支持 |
| Pod 亲和 | podAffinity | Pod | ❌ 无 | ✅ 必须 | ✅ 都支持 |
| Pod 反亲和 | podAntiAffinity | Pod | ✅ 本身就是"反" | ✅ 必须 | ✅ 都支持 |
| 拓扑分布 | topologySpreadConstraints | 统计数量 | ❌ 无 | ✅ 必须 | ✅ 通过 whenUnsatisfiable 控制 |
结语
本文从调度策略全景图出发,逐步深入到:
nodeAffinity的节点亲和/反亲和配置nodeSelectorTerms和matchExpressions的 AND/OR 逻辑关系podAffinity/podAntiAffinity的 Pod 级调度控制topologySpreadConstraints的均匀分布策略- 生产级完整 YAML 案例(三者组合使用)
- 系统化的 Pod Pending 调试方法(五斧调试法 + 调度器日志分析)
希望这篇文章能帮你彻底搞懂 Kubernetes 的调度策略体系,在生产环境中从容应对各种调度需求。如果你在实际配置中遇到问题,可以按照文中的调试流程一步步定位。
📌 本文配套资源:
- 完整 YAML 示例已包含在第六章
- 调试命令速查表见 7.9 节
- 常见陷阱与最佳实践见第八章
如果觉得本文对你有帮助,欢迎点赞、收藏、转发! 😊
更多推荐


所有评论(0)