Kubernetes 调度器是决定 Pod 运行在哪个节点的核心控制组件。默认调度器 kube-scheduler 每一秒都在做决策,但调度不仅是"找一个节点"这么简单——它涉及资源计算、约束满足、负载均衡、故障隔离等复杂问题。
目录
- 1. 调度器架构与流程
- 2. Scheduler Framework:可扩展的调度框架
- 3. 内置调度插件详解
- 4. 资源计算模型
- 5. 亲和性与反亲和性
- 6. 污点与容忍
- 7. Pod 优先级与抢占
- 8. 自定义调度器
- 9. 调度问题排查
- 10. 生产调度最佳实践
1. 调度器架构与流程
kube-scheduler 是 Kubernetes 控制平面的独立组件,通过 API Server 的 Watch 机制 获取未调度的 Pod,执行调度决策。
调度流程概览
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Pod 创建 │────→│ 调度队列 │────→│ PreFilter │
│ (Pending) │ │ (Scheduling │ │ 预处理 │
└──────────────┘ │ Queue) │ └──────────────┘
└──────────────┘ ↓
Filter
┌─────────────────┐
↓ ↓
┌──────────┐ ┌──────────┐
│ 节点1 │ │ 节点3 │ ← ─ 过滤后剩余节点
│ 不满足 │ │ 满足 │
└──────────┘ └──────────┘
↓
PreScore + Score
↓
┌──────────┐
│ 节点3 │ ← ─ 最高分
│ Score: 95│
└──────────┘
↓
Reserve → Bind → 通知 APIServer
调度过程分为两个主要阶段:
| 阶段 | 目的 | 关键动作 |
|---|---|---|
| 过滤(Filtering) | 找出满足所有硬性约束的节点 | PreFilter → Filter → PostFilter |
| 评分(Scoring) | 在可行节点中选择最优节点 | PreScore → Score → NormalizeScore |
调度队列
未调度 Pod 进入 Scheduling Queue,存在三类队列:
- Active Queue:等待调度的 Pod,按优先级排序
- Backoff Queue:调度失败进入冷却,防止频繁重试
- Unschedulable Pods:不可调度,等待集群状态变化后重试
# 查看调度队列中的 Pod
kubectl get pods --field-selector status.phase=Pending \
-o custom-columns='NAME:.metadata.name,NODE:.spec.nodeName'
2. Scheduler Framework:可扩展的调度框架
K8s 1.18+ 引入 Scheduler Framework,将调度过程拆分为可扩展的插件点。任何符合接口的代码都可以插入到调度流程中。
2.1 扩展点详解
Scheduling Queue
↓
┌─────────────┐
│ PreFilter │ ← 预处理 Pod 信息,计算资源总量
└─────────────┘
↓
┌─────────────┐
│ Filter │ ← 硬性过滤(资源不足/污点不匹配/亲和性不满足)
└─────────────┘
↓
┌─────────────┐
│ PostFilter │ ← 过滤后处理(如抢占逻辑)
└─────────────┘
↓
┌─────────────┐
│ PreScore │ ← 评分前准备
└─────────────┘
↓
┌─────────────┐
│ Score │ ← 软性评分(资源均衡性、亲和性权重)
└─────────────┘
↓
┌─────────────────┐
│ NormalizeScore │ ← 分数归一化到 0-100 范围
└─────────────────┘
↓
┌─────────────┐
│ Reserve │ ← 预留节点资源(在 Bind 前临时锁定)
└─────────────┘
↓
┌─────────────┐
│ Permit │ ← 最终决策等待/拒绝/通过(等待外部审批)
└─────────────┘
↓
┌─────────────┐
│ PreBind │ ← Bind 前准备(如分配存储)
└─────────────┘
↓
┌─────────────┐
│ Bind │ ← 将 Pod 绑定到节点
└─────────────┘
↓
┌─────────────┐
│ PostBind │ ← 绑定后清理
└─────────────┘
各扩展点说明:
| 扩展点 | 类型 | 作用 | 典型插件 |
|---|---|---|---|
PreFilter | Filter | 预处理 Pod,生成后续复用的状态 | NodeResourcesFit(计算 Pod 资源需求) |
Filter | Filter | 判断节点是否满足硬性约束 | NodeResourcesFit, TaintToleration, NodeAffinity |
PostFilter | Filter | 过滤后处理,当所有节点被过滤时尝试抢占 | DefaultPreemption |
PreScore | Score | 评分前准备 | InterPodAffinity(构建亲和性图) |
Score | Score | 为节点打分 | NodeResourcesFit, NodeAffinity, ImageLocality |
NormalizeScore | Score | 将各插件分数归一化到统一范围 | 每个 Score 插件自带 |
Reserve | Reserve | 在 Bind 前临时预留资源,防止竞态 | VolumeBinding |
Permit | Permit | 决策等待、批准或拒绝 | 自定义审批流程(如 GPU 调度器) |
PreBind | Bind | Bind 前执行预处理 | VolumeBinding(挂载卷) |
Bind | Bind | 执行 Pod → 节点的绑定操作 | DefaultBinding |
2.2 评分标准化与归一化
每个 Score 插件输出自定义分数,NormalizeScore 将它们归一化到 0-100:
最终节点分数 = Σ(插件权重 × 插件分数)
插件默认权重(可在调度配置中调整):
| 插件 | 默认权重 |
|---|---|
| NodeResourcesBalancedAllocation | 1 |
| ImageLocality | 1 |
| InterPodAffinity | 1 |
| NodeResourcesFit | 1 |
| NodeAffinity | 1 |
| PodTopologySpread | 2 |
| TaintToleration | 1 |
3. 内置调度插件详解
NodeResourcesFit
最核心的插件,判断节点是否有足够资源:
# 调度配置示例
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: default-scheduler
plugins:
score:
disabled:
- name: NodeResourcesFit
enabled:
- name: NodeResourcesFit
weight: 5
pluginConfig:
- name: NodeResourcesFit
args:
# 评分策略:LeastAllocated 或 MostAllocated
scoringStrategy:
type: LeastAllocated
resources:
- name: cpu
weight: 1
- name: memory
weight: 1
评分策略对比:
| 策略 | 行为 | 适用场景 |
|---|---|---|
LeastAllocated | 优先分配到资源占用少的节点 | 通用场景,保持资源均衡 |
MostAllocated | 优先分配到资源占用多的节点 | 提高节点利用率,减少碎片 |
RequestedToCapacityRatio | 自定义利用率到分数的映射 | 混合部署、特定优化场景 |
TaintToleration
为节点上的污点匹配 Pod 的容忍度打分,容忍度越多可选节点越灵活。若存在未容忍的 NoSchedule 污点,则在 Filter 阶段直接排除该节点。
PodTopologySpread
控制 Pod 在拓扑域(如 zone、node)之间的分布均衡性:
apiVersion: v1
kind: Pod
spec:
topologySpreadConstraints:
- maxSkew: 1 # 拓扑域间最多相差 1 个 Pod
topologyKey: topology.kubernetes.io/zone # 可用区级别
whenUnsatisfiable: DoNotSchedule # 或 ScheduleAnyway
labelSelector:
matchLabels:
app: api-server
- maxSkew: 1
topologyKey: kubernetes.io/hostname # 节点级别
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: api-server
maxSkew 解读:如果某 zone 有 3 个 Pod,其他 zone 最少有 1 个,maxSkew = 2。设置 maxSkew: 1 会强制分布到更多 zone,DoNotSchedule 若无法满足则保持 Pending。
4. 资源计算模型
Request vs Limit 在调度中的影响
resources:
requests:
cpu: 250m # 调度判定依据
memory: 256Mi # 调度判定依据
limits:
cpu: 1000m # CFS 配额限制
memory: 512Mi # OOM Killer 阈值
调度器只看 requests:即使节点剩余可用资源(Allocatable - Sum(requests))只有 100m CPU,Pod requests 200m 就会因资源不足被拒绝。但 limit 不影响调度——Pod 可能在节点上突发使用到 1000m,超出节点的物理 CPU 时通过 CFS 限流。
内存的特殊性:
- requests 内存是调度依据
- limits 内存是硬限制,超过即触发 OOM Killer
- Pod 使用内存不受 requests 限制,只受 limits 限制
- 如果节点内存被耗尽(超过物理内存),即使没有到 limit,也会触发系统级 OOM
Overcommit 策略
K8s 默认允许资源超售(Overcommit):所有 Pod 的 requests 总和可以超过节点实际资源。这带来了风险:
# 节点压力驱逐:Kubelet 在资源不足时按优先级驱逐 Pod
kubectl describe node <node> | grep -A5 "Allocated resources"
系统级驱逐阈值(Kubelet 配置):
# /var/lib/kubelet/config.yaml
evictionHard:
memory.available: "100Mi" # 可用内存 < 100Mi 触发驱逐
nodefs.available: "10%" # 节点文件系统 < 10% 触发驱逐
imagefs.available: "15%" # 镜像文件系统 < 15% 触发驱逐
evictionSoft:
memory.available: "500Mi" # 软阈值,有宽限期
evictionSoftGracePeriod:
memory.available: "1m"
5. 亲和性与反亲和性
5.1 节点亲和性
将 Pod 调度到满足特定条件的节点:
apiVersion: v1
kind: Pod
spec:
affinity:
nodeAffinity:
# 必须满足(硬性要求)
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node-type
operator: In
values: ["compute"]
- key: topology.kubernetes.io/zone
operator: In
values: ["cn-beijing-a", "cn-beijing-b"]
# 尽量满足(软性偏好)
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference:
matchExpressions:
- key: disk-type
operator: In
values: ["ssd"]
- weight: 50
preference:
matchExpressions:
- key: node.kubernetes.io/instance-type
operator: In
values: ["ecs.g7.xlarge"]
operator 类型:In / NotIn / Exists / DoesNotExist / Gt / Lt
必须 vs 尽量:
requiredDuringSchedulingIgnoredDuringExecution:不满足则 Pod 保持 PendingpreferredDuringSchedulingIgnoredDuringExecution:权重范围 1-100,不满足也能调度
5.2 Pod 亲和性与反亲和性
根据其他 Pod 的位置决定调度位置:
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
affinity:
# Pod 反亲和性:副本分散到不同节点
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values: ["api-server"]
topologyKey: kubernetes.io/hostname # 不同节点
# Pod 亲和性:Redis 跟随 API Server
podAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: api-server
topologyKey: kubernetes.io/hostname # 同一节点
性能警告:Pod 亲和性/反亲和性需要遍历所有节点上的所有 Pod,在大规模集群中计算开销极高(O(n²))。谨慎使用 required 级别的 Pod 亲和性。
5.3 拓扑分布约束
PodTopologySpread 是 Pod 反亲和性的增强和替代方案,设计更合理、性能更好:
apiVersion: apps/v1
kind: Deployment
spec:
replicas: 6
template:
spec:
topologySpreadConstraints:
- maxSkew: 1
minDomains: 3 # 域名至少需要 3 个,不足则等待
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: api-server
matchLabelKeys: # K8s 1.27+ 新增
- pod-template-hash # 自动引用 Pod 标签
PodTopologySpread vs PodAntiAffinity:
| 特性 | PodAntiAffinity | PodTopologySpread |
|---|---|---|
| 语法复杂度 | 较复杂(表达式匹配) | 简洁直观 |
| 性能 | O(n²),大规模集群慎用 | O(n),更高效 |
| 灵活度 | 强(任意标签匹配) | 中等(仅按标签分散) |
| 推荐场景 | 精细控制 | 通用高可用分布 |
6. 污点与容忍
污点(Taint)
节点上的"排斥力",阻止 Pod 调度到该节点:
# 给节点添加污点
kubectl taint nodes gpu-node-1 dedicated=gpu:NoSchedule
# 节点维护时驱逐所有 Pod
kubectl taint nodes node-1 maintenance=true:NoExecute
# 移除污点
kubectl taint nodes gpu-node-1 dedicated=gpu:NoSchedule-
污点效果:
| 效果 | 行为 | 典型场景 |
|---|---|---|
NoSchedule | 新 Pod 不能调度,已有 Pod 不受影响 | 专用节点(GPU) |
PreferNoSchedule | 尽量不调度,非强制执行 | 不推荐的旧节点 |
NoExecute | 新 Pod 不能调度,已有 Pod 被驱逐 | 节点维护、硬件故障 |
容忍(Toleration)
Pod 声明"我可以容忍这个污点":
apiVersion: v1
kind: Pod
spec:
tolerations:
# 完全匹配污点
- key: dedicated
operator: Equal
value: gpu
effect: NoSchedule
# 匹配任意值
- key: dedicated
operator: Exists
effect: NoSchedule
# 匹配所有 NoExecute 污点(带宽限期)
- operator: Exists
effect: NoExecute
tolerationSeconds: 3600 # 被驱逐前等待 1 小时
# 匹配特定节点(绕过调度器直接绑定)
- key: node-role.kubernetes.io/master
operator: Exists
effect: NoSchedule
tolerationSeconds 作用:对于 NoExecute 污点,Pod 可以选择延迟被驱逐的时间。这对于需要优雅关闭的应用很有用——污点出现后,Pod 有 3600 秒窗口完成请求处理。
常用污点来源
- 控制平面节点:
node-role.kubernetes.io/control-plane:NoSchedule - NVIDIA GPU:默认无污点,但通常手动标定
- Spot/抢占式实例:
node.kubernetes.io/lifecycle=spot:NoSchedule - 节点条件:
node.kubernetes.io/not-ready:NoExecute、node.kubernetes.io/unreachable:NoExecute(系统自动添加)
7. Pod 优先级与抢占
PriorityClass
定义 Pod 的优先级,影响调度顺序和被驱逐时的保护:
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: high-priority
value: 1000000 # 越大越优先,数值无上限但建议有范围
preemptionPolicy: PreemptLowerPriority # 或 Never
globalDefault: false
description: "核心业务服务,可驱逐低优先级 Pod"
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: low-priority
value: 1000
preemptionPolicy: Never # 永不抢占,但可被高优先级抢占
description: "批处理任务,不重要"
在 Pod 中引用:
apiVersion: v1
kind: Pod
spec:
priorityClassName: high-priority
抢占(Preemption)机制
当高优先级 Pod 无法调度时,调度器会:
- 寻找一个低优先级 Pod 被移除后可以容纳该 Pod 的节点
- 选择能让"受害者"总优先级最小(即影响最小)的节点
- 通过 API Server 发送删除请求给这些低优先级 Pod
- Pending 的高优先级 Pod 进入调度队列等待节点释放
高优先级 Pod A 需要 4 CPU,所有节点都满
└─→ 节点 X:低优先级 Pod B(1 CPU) + Pod C(2 CPU) + 空闲 1 CPU = 4 CPU
驱逐 B 和 C 后 → 空闲 4 CPU → Pod A 调度成功
关键约束:
- 只能抢占同一 Namespace 吗?不是,抢占是集群级别的
- 会触发 PDB 的低优先级 Pod 可以被抢占吗?不行,PDB 保护的 Pod 不会被抢占
- 被驱逐的 Pod 会收到 SIGTERM,有默认 30 秒优雅终止时间
非抢占模式
设置 preemptionPolicy: Never 后,高优先级 Pod 仅影响调度顺序(在队列中排在前面),不会驱逐低优先级 Pod。适合在线负载:我们希望重要服务先调度,但不想抢占已在运行的服务。
8. 自定义调度器
多调度器部署
在同一个集群中运行多个调度器:
# Pod 指定调度器
apiVersion: v1
kind: Pod
spec:
schedulerName: gpu-scheduler # 不是默认的 default-scheduler
自定义调度器部署方式:
- 独立二进制:基于 scheduler framework 编译新调度器
- 调度扩展器(Scheduler Extender):HTTP 服务,在过滤/评分阶段调用外部逻辑
- 调度框架插件:内联在默认调度器中,最轻量
调度框架插件示例
用 Go 编写自定义评分插件(示例框架):
package myscheduler
import (
"context"
"k8s.io/kubernetes/pkg/scheduler/framework"
)
type CustomPlugin struct{}
var _ framework.ScorePlugin = &CustomPlugin{}
func (p *CustomPlugin) Name() string { return "CustomPlugin" }
func (p *CustomPlugin) Score(ctx context.Context, state *framework.CycleState,
pod *v1.Pod, nodeName string) (int64, *framework.Status) {
// 自定义评分逻辑:如节点上的 GPU 利用率、网络延迟
return 50, nil // 0-100
}
func (p *CustomPlugin) ScoreExtensions() framework.ScoreExtensions {
return nil
}
注册插件后重新编译调度器,或通过配置文件加载动态插件。
使用场景
| 场景 | 方案 | 说明 |
|---|---|---|
| GPU 调度 | NVIDIA GPU Operator + custom scheduler | 考虑 GPU 拓扑(NVLink、PCIe 距离) |
| 批处理调度 | Volcano / Yunikorn | 支持 Gang Scheduling(一组 Pod 同时调度) |
| 大数据调度 | Volcano | Spark/Flink 作业的队列和优先级 |
| 电信边缘 | 自定义调度器 | 考虑网络延迟、地理位置 |
9. 调度问题排查
Pod 一直 Pending
# 1. 查看 Pod 事件(核心)
kubectl describe pod <pod-name>
# 2. 典型输出分析
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 10s scheduler 0/3 nodes are available:
1 node(s) had taint {dedicated=gpu:NoSchedule}
1 node(s) had insufficient memory
1 node(s) had volume node affinity conflict
常见问题与解决方案
| 现象 | 原因 | 排查命令 | 解决方案 |
|---|---|---|---|
| 0/3 nodes available | 资源不足或约束不匹配 | describe pod | 检查资源请求/污点/亲和性 |
| volume node affinity conflict | PVC 绑定的 PV 在特定节点 | kubectl get pv | 检查 PV 的 nodeAffinity |
| PodAffinity rules not satisfied | Pod 反亲和性导致无法分布 | kubectl get pods -o wide | 增加节点或减少副本数 |
| Insufficient cpu/memory | 节点资源耗尽 | kubectl describe node | 扩容节点或降低 requests |
| Taints not tolerated | 节点有污点且 Pod 无容忍 | kubectl describe node | 添加 tolerations 或清理污点 |
查看调度器日志
# 原生 K8s
kubectl logs -n kube-system -l component=kube-scheduler
# 特定调度器 Pod
kubectl logs -n kube-system kube-scheduler-master-1
模拟调度
# K8s 1.28+ 的 dry-run 调度(不实际创建 Pod)
kubectl apply -f pod.yaml --dry-run=server
10. 生产调度最佳实践
原则一:合理使用 Requests 与 Limits
resources:
requests:
cpu: "100m" # 保证调度时使用
memory: "128Mi" # 保证调度时使用
limits:
cpu: "500m" # CFS 配额,可选设置
memory: "256Mi" # 必须设置,防止 OOM 导致节点不稳定
- CPU limits 可选:在 CPU 充足时允许突发使用
- Memory limits 必须:内存是无挤压资源,超过即 OOM
- Requests 设置依据:Prometheus 采集的历史平均使用量
- Limits 设置依据:历史 p95/p99 峰值
原则二:高可用调度策略
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels: { app: api-server }
topologyKey: topology.kubernetes.io/zone # 跨可用区
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname # 跨节点
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels: { app: api-server }
原则三:专用节点管理
# GPU 节点专用
kubectl taint nodes gpu-node-1 nvidia.com/gpu=true:NoSchedule
kubectl label nodes gpu-node-1 node-type=gpu
# Pod 配置
tolerations:
- key: nvidia.com/gpu
operator: Equal
value: "true"
effect: NoSchedule
nodeSelector:
node-type: gpu
原则四:优先级分层设计
| 优先级类 | value | 用途 | 是否可抢占 |
|---|---|---|---|
| system-critical | 2000000 | 系统组件(etcd、kube-apiserver) | 否 |
| high-priority | 1000000 | 核心在线服务 | 是 |
| normal | 100000 | 一般业务服务 | 是 |
| low-priority | 1000 | 批处理、数据分析 | 是 |
| best-effort | 0 | 无需保证的任务 | 是 |
总结
| 主题 | 核心要点 |
|---|---|
| 调度流程 | Filter(硬性约束)→ Score(软性偏好)→Bind |
| 调度框架 | 10+ 扩展点,插件化架构支持自定义 |
| 资源计算 | 调度看 requests,运行看 limits,内存必须设 limit |
| 亲和性 | 节点亲和控制节点选择,Pod 亲和/反亲和控制 Pod 间位置关系 |
| 拓扑分布 | PodTopologySpread 是 PodAntiAffinity 的现代替代方案 |
| 污点容忍 | NoSchedule(新 Pod)、NoExecute(已有 Pod 也驱逐) |
| 优先级/抢占 | PriorityClass 控制调度顺序和被保护程度,PDB 保护不可被抢占 |
| 自定义调度 | 多调度器并存、调度扩展器、框架插件三种方案 |
掌握调度器的运作原理,是进行集群资源规划、故障排查、性能优化的基础。在生产环境中,调度策略直接影响应用的可用性、资源利用率和运维体验。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。