概述
Kubernetes 调度器是控制平面中最核心的组件之一——它决定每个 Pod 运行在哪个节点上。调度质量直接影响集群的资源利用率、应用性能和可靠性。理解调度器的工作原理,是做好 K8s 生产运维的基本功。
从调度流程、过滤打分机制、亲和性、污点容忍、优先级抢占到自定义调度器,详细梳理调度器的原理与调优实践。
本文基于 Kubernetes v1.30。参考 Kubernetes 调度器文档
调度流程
整体流程
Pod 创建 → API Server → etcd → 调度器 Watch → 调度决策 → 绑定到节点 → kubelet 创建容器
调度器的核心工作分为两个阶段:
1. 过滤(Filter):排除不满足条件的节点 → 候选节点集
2. 打分(Score):对候选节点打分 → 选择最高分节点
详细调度流程
┌──────────────┐
│ Pod 入队 │
└──────┬───────┘
▼
┌──────────────┐
│ 调度周期开始 │
└──────┬───────┘
▼
┌────────────────────────┐
│ 扩展点:PreFilter │ ← 预过滤(检查 Pod 是否可调度)
└────────────┬────────────┘
▼
┌────────────────────────┐
│ 扩展点:Filter │ ← 过滤不满足条件的节点
│ (排除不可行节点) │
└────────────┬────────────┘
▼
┌────────────────────────┐
│ 扩展点:PostFilter │ ← 过滤后无节点?(触发抢占)
└────────────┬────────────┘
▼
┌────────────────────────┐
│ 扩展点:Score │ ← 对候选节点打分
│ (选择最优节点) │
└────────────┬────────────┘
▼
┌────────────────────────┐
│ 扩展点:Reserve │ ← 预留资源
└────────────┬────────────┘
▼
┌────────────────────────┐
│ 扩展点:Permit │ ← 允许/延迟/拒绝
└────────────┬────────────┘
▼
┌────────────────────────┐
│ 扩展点:PreBind │ ← 绑定前处理
└────────────┬────────────┘
▼
┌────────────────────────┐
│ 扩展点:Bind │ ← 绑定到节点
└────────────┬────────────┘
▼
┌────────────────────────┐
│ 扩展点:PostBind │ ← 绑定后处理
└────────────────────────┘
调度队列
调度器内部维护三个队列:
| 队列 | 说明 | 优先级 |
|---|---|---|
activeQueue | 待调度的 Pod,按优先级排序 | 高优先级先调度 |
backoffQueue | 调度失败的 Pod,等待重试 | 指数退避 |
unschedulableQueue | 不可调度的 Pod,等条件变化 | 定期检查 |
Pod 进入 → activeQueue → 调度成功 → Bind
↓ 调度失败
backoffQueue → 等待退避时间 → activeQueue(重试)
↓ 多次失败
unschedulableQueue → 条件变化时 → activeQueue
过滤与打分
过滤阶段(Filter)
过滤阶段排除不满足 Pod 需求的节点,涉及多个插件:
| 过滤插件 | 功能 |
|---|---|
PodFitsResources | 节点资源是否满足 Pod 的 requests |
PodFitsHostPorts | 节点是否有空闲的 hostPort |
NodeSelector | 节点是否匹配 nodeSelector |
NodeAffinity | 节点是否匹配节点亲和性 |
TaintToleration | Pod 是否容忍节点的污点 |
VolumeBinding | 节点是否能绑定 Pod 请求的 PV |
VolumeZone | PV 的拓扑约束是否匹配 |
PodTopologySpread | 是否满足拓扑分布约束 |
NodeUnschedulable | 节点是否被 cordon |
NodeMemoryPressure | 节点是否有内存压力 |
NodeDiskPressure | 节点是否有磁盘压力 |
NodePIDPressure | 节点是否有 PID 压力 |
NetworkFilter | 网络是否可用 |
打分阶段(Score)
打分阶段对每个候选节点打分(0-100),分数最高者胜出:
| 打分插件 | 功能 | 权重 |
|---|---|---|
NodeResourcesFit | 资源利用率打分 | 高 |
InterPodAffinity | Pod 亲和性打分 | 中 |
NodeAffinity | 节点亲和性偏好打分 | 中 |
PodTopologySpread | 拓扑分布打分 | 中 |
NodeUnschedulable | 不可调度惩罚 | - |
ImageLocality | 节点已有镜像优先 | 低 |
TaintToleration | 污点容忍偏好打分 | 低 |
资源打分策略
NodeResourcesFit 支持三种打分策略:
# 通过调度器配置指定
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: default-scheduler
pluginConfig:
- name: NodeResourcesFit
args:
scoringStrategy:
type: LeastAllocated # 打分策略
resources:
- name: cpu
weight: 1
- name: memory
weight: 1
| 策略 | 打分逻辑 | 效果 |
|---|---|---|
LeastAllocated | 优先选择资源分配最少的节点 | 均匀分布 |
MostAllocated | 优先选择资源分配最多的节点 | 紧凑分布 |
RequestedToCapacityRatio | 按 resource ratio 打分 | 自定义权重 |
# LeastAllocated(默认):Pod 分散到不同节点
# 适合:通用场景,负载均匀
# MostAllocated:Pod 集中到同一节点
# 适合:节省节点(缩容场景)
节点亲和性
节点选择器 vs 节点亲和性
| 特性 | nodeSelector | nodeAffinity |
|---|---|---|
| 匹配方式 | 精确匹配 | 支持多种操作符 |
| 硬约束 | 是 | required |
| 软约束 | 否 | preferred |
| 推荐 | 不推荐 | 推荐 |
required(硬约束)
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/arch
operator: In
values:
- amd64
- key: node.kubernetes.io/instance-type
operator: In
values:
- c5.2xlarge
- c5.4xlarge
- key: topology.kubernetes.io/zone
operator: NotIn
values:
- us-east-1a # 排除该可用区
preferred(软约束)
spec:
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100 # 权重 1-100
preference:
matchExpressions:
- key: ssd
operator: In
values:
- "true"
- weight: 50
preference:
matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- us-east-1b
操作符
| 操作符 | 说明 | 示例 |
|---|---|---|
In | 值在列表中 | key In [a, b] |
NotIn | 值不在列表中 | key NotIn [a, b] |
Exists | 键存在 | key Exists |
DoesNotExist | 键不存在 | key DoesNotExist |
Gt | 大于(数值) | key Gt 10 |
Lt | 小于(数值) | key Lt 10 |
节点标签管理
# 添加标签
kubectl label nodes node-1 disktype=ssd
kubectl label nodes node-1 zone=east
kubectl label nodes node-1 gpu=true
# 查看标签
kubectl get nodes --show-labels
kubectl get nodes -l disktype=ssd
# 删除标签
kubectl label nodes node-1 disktype-
Pod 亲和性与反亲和性
Pod 亲和性
Pod 亲和性让 Pod 倾向于调度到已经有某些 Pod 的节点:
spec:
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- cache
topologyKey: kubernetes.io/hostname
# Pod 要和 app=cache 的 Pod 在同一节点
Pod 反亲和性
Pod 反亲和性让 Pod 倾向于调度到没有某些 Pod 的节点:
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- web
topologyKey: kubernetes.io/hostname
# web Pod 之间不能在同一节点(高可用)
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- web
topologyKey: topology.kubernetes.io/zone
# 尽量让 web Pod 分布在不同可用区
常见使用模式
| 场景 | 配置 | 说明 |
|---|---|---|
| 同节点部署 | podAffinity + topologyKey: hostname | 前端和缓存同节点 |
| 不同节点部署 | podAntiAffinity + topologyKey: hostname | 同服务 Pod 分散 |
| 不同可用区 | podAntiAffinity + topologyKey: zone | 跨可用区高可用 |
| 机架感知 | podAntiAffinity + topologyKey: rack | 跨机架分布 |
反亲和性的性能问题
注意:
requiredDuringSchedulingIgnoredDuringExecution的 Pod 反亲和性在大集群中有性能问题。调度器需要遍历所有节点上的所有 Pod 检查标签匹配,当节点数 > 1000 时调度延迟明显增加。
替代方案是 podTopologySpread,它在设计上考虑了性能:
spec:
topologySpreadConstraints:
- maxSkew: 1 # 各拓扑域之间的最大差值
topologyKey: kubernetes.io/hostname # 拓扑域:节点
whenUnsatisfiable: DoNotSchedule # 不满足时拒绝调度
labelSelector:
matchLabels:
app: web
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone # 拓扑域:可用区
whenUnsatisfiable: ScheduleAnyway # 尽量满足
labelSelector:
matchLabels:
app: web
污点与容忍
污点(Taint)
污点标记节点,阻止不容忍该污点的 Pod 调度:
# 添加污点
kubectl taint nodes node-1 dedicated=gpu:NoSchedule
# 查看污点
kubectl describe node node-1 | grep Taint
# 删除污点
kubectl taint nodes node-1 dedicated=gpu:NoSchedule-
三种污点效果
| 效果 | 说明 | 适用场景 |
|---|---|---|
NoSchedule | 不调度新 Pod,已有 Pod 不受影响 | 专用节点 |
PreferNoSchedule | 尽量不调度,非强制 | 软隔离 |
NoExecute | 不调度新 Pod,已有 Pod 不容忍则驱逐 | 维护模式 |
容忍(Toleration)
spec:
tolerations:
- key: "dedicated"
operator: "Equal"
value: "gpu"
effect: "NoSchedule"
# 简写:只匹配 key 和 effect
- key: "dedicated"
operator: "Exists"
effect: "NoSchedule"
# 容忍所有污点(慎用)
- operator: "Exists"
# 容忍 NoExecute 并设置驱逐延迟
- key: "node.kubernetes.io/not-ready"
operator: "Exists"
effect: "NoExecute"
tolerationSeconds: 300 # 节点NotReady后300秒才驱逐
系统自动添加的污点
| 污点 | 说明 | 默认容忍 |
|---|---|---|
node.kubernetes.io/not-ready | 节点 NotReady | 核心组件容忍 300s |
node.kubernetes.io/unreachable | 节点不可达 | 核心组件容忍 300s |
node.kubernetes.io/memory-pressure | 内存压力 | 不调度新 Pod |
node.kubernetes.io/disk-pressure | 磁盘压力 | 不调度新 Pod |
node.kubernetes.io/pid-pressure | PID 压力 | 不调度新 Pod |
node.kubernetes.io/network-unavailable | 网络不可用 | 核心组件容忍 |
node.kubernetes.io/unschedulable | 不可调度(cordon) | 不调度新 Pod |
常见使用场景
# 1. GPU 专用节点
kubectl taint nodes gpu-node-1 nvidia.com/gpu=:NoSchedule
# 只有容忍该污点的 Pod(GPU 任务)才会调度
# 2. 维护模式
kubectl taint nodes node-1 maintenance=true:NoExecute
# 所有 Pod 被驱逐(除了容忍的),用于节点维护
# 3. 特殊节点
kubectl taint nodes special-node-1 dedicated=special-team:NoSchedule
kubectl label nodes special-node-1 team=special-team
优先级与抢占
优先级类
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: high-priority
value: 1000000
globalDefault: false
description: "高优先级工作负载"
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: low-priority
value: 100
globalDefault: false
description: "低优先级工作负载"
系统优先级类
| 优先级类 | 值 | 说明 |
|---|---|---|
system-node-critical | 2000001000 | 节点关键组件 |
system-cluster-critical | 2000000000 | 集群关键组件 |
user-created | 0-1000000000 | 用户定义 |
Pod 使用优先级
apiVersion: apps/v1
kind: Deployment
metadata:
name: critical-app
spec:
template:
spec:
priorityClassName: high-priority
containers:
- name: app
image: myapp:v1
抢占机制
当高优先级 Pod 无法调度时,调度器会尝试驱逐低优先级 Pod 来腾出空间:
1. 高优先级 Pod 无法调度(资源不足)
2. 调度器寻找可以驱逐的低优先级 Pod
3. 驱逐低优先级 Pod,释放资源
4. 高优先级 Pod 调度成功
5. 低优先级 Pod 进入 Pending 状态
抢占防护
# 低优先级 Pod 配置 PDB 防止被驱逐
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: low-priority-pdb
spec:
minAvailable: 1 # 至少保持1个可用
selector:
matchLabels:
app: low-priority-app
注意:PDB 只能限制自愿中断(Voluntary Disruption),抢占属于自愿中断。但 PDB 的
minAvailable约束仍会被尊重——如果驱逐会导致低于 minAvailable,抢占不会发生。
拓扑分布约束
基本用法
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: web
minDomains: 2 # 至少2个拓扑域才生效
matchLabelKeys:
- pod-template-hash # 配合滚动更新
分布效果示例
3个可用区,6个 web Pod,maxSkew=1
zone-a: [web-0, web-1]
zone-b: [web-2, web-3]
zone-c: [web-4, web-5]
最大差值 = 0 ≤ 1 ✓
如果 zone-c 不可用:
zone-a: [web-0, web-1, web-2]
zone-b: [web-3, web-4, web-5]
最大差值 = 0 ≤ 1 ✓
whenUnsatisfiable 选项
| 值 | 行为 | 适用场景 |
|---|---|---|
DoNotSchedule | 不满足约束时拒绝调度 | 硬约束(高可用必须) |
ScheduleAnyway | 尽量满足,不满足也调度 | 软约束(尽力而为) |
调度器配置
多调度器
K8s 支持运行多个调度器,Pod 可以指定使用哪个:
# 部署自定义调度器
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-scheduler
namespace: kube-system
spec:
replicas: 1
selector:
matchLabels:
app: my-scheduler
template:
metadata:
labels:
app: my-scheduler
spec:
containers:
- name: my-scheduler
image: registry.k8s.io/kube-scheduler:v1.30.0
command:
- kube-scheduler
- --config=/etc/kubernetes/my-scheduler-config.yaml
- --scheduler-name=my-scheduler
volumeMounts:
- name: config
mountPath: /etc/kubernetes
volumes:
- name: config
configMap:
name: my-scheduler-config
---
# Pod 使用自定义调度器
apiVersion: v1
kind: Pod
metadata:
name: my-pod
spec:
schedulerName: my-scheduler # 指定调度器
containers:
- name: app
image: myapp:v1
调度器配置文件
# /etc/kubernetes/my-scheduler-config.yaml
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: my-scheduler
plugins:
filter:
enabled:
- name: NodeResourcesFit
- name: NodeAffinity
- name: TaintToleration
disabled:
- name: VolumeBinding # 禁用某个默认插件
score:
enabled:
- name: NodeResourcesFit
weight: 10
- name: InterPodAffinity
weight: 5
pluginConfig:
- name: NodeResourcesFit
args:
scoringStrategy:
type: LeastAllocated
resources:
- name: cpu
weight: 2
- name: memory
weight: 1
调度器扩展
调度框架(Scheduling Framework)
K8s v1.19+ 的调度框架允许在不修改调度器源码的情况下扩展调度行为:
扩展点:
PreFilter → Filter → PostFilter → PreScore → Score → Reserve → Permit → PreBind → Bind → PostBind
扩展方式
| 方式 | 复杂度 | 灵活性 | 适用场景 |
|---|---|---|---|
| 调度器配置 | 低 | 中 | 调整插件权重和策略 |
| 调度框架 | 中 | 高 | 自定义插件逻辑 |
| 多调度器 | 高 | 极高 | 完全不同的调度策略 |
| Scheduler Extender | 中 | 中 | HTTP 扩展(旧方式) |
自定义调度插件示例
// 自定义 Filter 插件示例
package myplugin
import (
"context"
v1 "k8s.io/api/core/v1"
"k8s.io/kubernetes/pkg/scheduler/framework"
)
const Name = "MyPlugin"
type MyPlugin struct {
handle framework.Handle
}
func (p *MyPlugin) Name() string { return Name }
func (p *MyPlugin) Filter(
ctx context.Context,
state *framework.CycleState,
pod *v1.Pod,
nodeInfo *framework.NodeInfo,
) *framework.Status {
// 自定义过滤逻辑
if nodeInfo.Node().Labels["custom-label"] != "allowed" {
return framework.NewStatus(framework.Unschedulable, "node not allowed")
}
return framework.NewStatus(framework.Success, "")
}
func (p *MyPlugin) Score(
ctx context.Context,
state *framework.CycleState,
pod *v1.Pod,
nodeName string,
) (int64, *framework.Status) {
// 自定义打分逻辑
return 100, framework.NewStatus(framework.Success, "")
}
// New 初始化插件
func New(
ctx context.Context,
configuration runtime.Object,
f framework.Handle,
) (framework.Plugin, error) {
return &MyPlugin{handle: f}, nil
}
# 注册自定义插件
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: my-scheduler
plugins:
filter:
enabled:
- name: MyPlugin
score:
enabled:
- name: MyPlugin
weight: 10
生产实践
高可用部署调度
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
spec:
replicas: 6
template:
metadata:
labels:
app: web
spec:
# 跨节点 + 跨可用区分布
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: web
- maxSkew: 2
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: web
# 节点亲和性:选择合适的节点
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node-role.kubernetes.io/worker
operator: Exists
- key: kubernetes.io/arch
operator: In
values:
- amd64
# 容忍污点
tolerations:
- key: "node.kubernetes.io/not-ready"
operator: "Exists"
effect: "NoExecute"
tolerationSeconds: 30
- key: "node.kubernetes.io/unreachable"
operator: "Exists"
effect: "NoExecute"
tolerationSeconds: 30
containers:
- name: web
image: myapp:v1
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: 1000m
memory: 1Gi
GPU 调度
# GPU 节点打污点
kubectl taint nodes gpu-node nvidia.com/gpu=:NoSchedule
kubectl label nodes gpu-node accelerator=nvidia
---
# GPU Pod
apiVersion: v1
kind: Pod
metadata:
name: gpu-task
spec:
tolerations:
- key: "nvidia.com/gpu"
operator: "Exists"
effect: "NoSchedule"
nodeSelector:
accelerator: nvidia
containers:
- name: gpu-container
image: tensorflow/tensorflow:latest-gpu
resources:
limits:
nvidia.com/gpu: 2 # 请求2块GPU
专用节点
# 创建专用节点池
kubectl label nodes node-pool-a dedicated=team-a
kubectl taint nodes node-pool-a dedicated=team-a:NoSchedule
# Team A 的 Pod
spec:
nodeSelector:
dedicated: team-a
tolerations:
- key: "dedicated"
operator: "Equal"
value: "team-a"
effect: "NoSchedule"
调度问题排查
Pod 处于 Pending 状态
# 查看 Pod 调度失败原因
kubectl describe pod <pod-name> -n <namespace>
# 关注 Events 部分:
# Events:
# Type Reason Age From Message
# ---- ------ ---- ---- -------
# Warning FailedScheduling 10s default-scheduler 0/10 nodes are available:
# 3 Insufficient cpu, 2 Insufficient memory,
# 3 node(s) had untolerated taint,
# 2 node(s) didn't match Pod's node affinity
常见 Pending 原因
| 原因 | 解决 |
|---|---|
Insufficient cpu/memory | 加节点或减少 requests |
had untolerated taint | 添加 toleration |
didn't match node affinity | 检查 nodeSelector/affinity |
node(s) had volume node affinity conflict | PV 拓扑约束冲突 |
Insufficient nvidia.com/gpu | GPU 资源不足 |
调度器日志
# 查看调度器日志
kubectl logs -n kube-system -l component=kube-scheduler --tail=100
# 查看调度决策详情
# 需要开启调度器详细日志
kube-scheduler --v=5 # 调高日志级别
模拟调度
# 使用 kubectl debug 模拟调度
kubectl debug node/<node-name> -it --image=busybox
# 使用 scheduler simulator 测试调度策略
# 参考 https://github.com/kubernetes-sigs/kube-scheduler-simulator
总结
K8s 调度器是一个高度可扩展的组件,核心要点:
- 两阶段调度:理解过滤和打分两个阶段,过滤排除不可行节点,打分选择最优节点。
- 亲和性选对类型:节点亲和性控制 Pod 调度到哪些节点,Pod 亲和性控制 Pod 之间的关系。
- 污点做隔离:用污点 + 容忍实现节点专用,比 nodeSelector 更灵活。
- 拓扑分布做高可用:
topologySpreadConstraints是比 Pod 反亲和性更高效的高可用方案。 - 优先级做抢占:核心业务用高优先级,非核心用低优先级,资源紧张时自动保障核心业务。
- 配置优于自定义:大多数调度需求通过调度器配置文件就能满足,不需要写自定义插件。
- 资源 requests 必须配:调度器基于 requests 调度,不配 requests 的 Pod 会干扰调度决策。
调度器的调优是一个持续过程。建议定期 review Pod 调度分布、节点资源利用率,根据实际情况调整亲和性规则和拓扑约束。
参考资料与致谢
本文在撰写过程中参考了以下资料,感谢原作者的贡献:
- Kubernetes 调度器文档 — Kubernetes 官方,参考了Kubernetes 调度器文档相关内容