Kubernetes调度器原理与高级调度策略

深入解析 Kubernetes kube-scheduler 调度架构、Scheduler Framework 扩展点、内置调度插件、亲和性/反亲和性、污点与容忍、Pod 优先级与抢占,以及自定义调度器和生产问题排查。

Kubernetes 调度器是决定 Pod 运行在哪个节点的核心控制组件。默认调度器 kube-scheduler 每一秒都在做决策,但调度不仅是"找一个节点"这么简单——它涉及资源计算、约束满足、负载均衡、故障隔离等复杂问题。


目录


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   │  ← 绑定后清理
  └─────────────┘

各扩展点说明:

扩展点类型作用典型插件
PreFilterFilter预处理 Pod,生成后续复用的状态NodeResourcesFit(计算 Pod 资源需求)
FilterFilter判断节点是否满足硬性约束NodeResourcesFit, TaintToleration, NodeAffinity
PostFilterFilter过滤后处理,当所有节点被过滤时尝试抢占DefaultPreemption
PreScoreScore评分前准备InterPodAffinity(构建亲和性图)
ScoreScore为节点打分NodeResourcesFit, NodeAffinity, ImageLocality
NormalizeScoreScore将各插件分数归一化到统一范围每个 Score 插件自带
ReserveReserve在 Bind 前临时预留资源,防止竞态VolumeBinding
PermitPermit决策等待、批准或拒绝自定义审批流程(如 GPU 调度器)
PreBindBindBind 前执行预处理VolumeBinding(挂载卷)
BindBind执行 Pod → 节点的绑定操作DefaultBinding

2.2 评分标准化与归一化

每个 Score 插件输出自定义分数,NormalizeScore 将它们归一化到 0-100

最终节点分数 = Σ(插件权重 × 插件分数)

插件默认权重(可在调度配置中调整):

插件默认权重
NodeResourcesBalancedAllocation1
ImageLocality1
InterPodAffinity1
NodeResourcesFit1
NodeAffinity1
PodTopologySpread2
TaintToleration1

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 保持 Pending
  • preferredDuringSchedulingIgnoredDuringExecution:权重范围 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

特性PodAntiAffinityPodTopologySpread
语法复杂度较复杂(表达式匹配)简洁直观
性能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:NoExecutenode.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 无法调度时,调度器会:

  1. 寻找一个低优先级 Pod 被移除后可以容纳该 Pod 的节点
  2. 选择能让"受害者"总优先级最小(即影响最小)的节点
  3. 通过 API Server 发送删除请求给这些低优先级 Pod
  4. 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

自定义调度器部署方式:

  1. 独立二进制:基于 scheduler framework 编译新调度器
  2. 调度扩展器(Scheduler Extender):HTTP 服务,在过滤/评分阶段调用外部逻辑
  3. 调度框架插件:内联在默认调度器中,最轻量

调度框架插件示例

用 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 同时调度)
大数据调度VolcanoSpark/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 conflictPVC 绑定的 PV 在特定节点kubectl get pv检查 PV 的 nodeAffinity
PodAffinity rules not satisfiedPod 反亲和性导致无法分布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-critical2000000系统组件(etcd、kube-apiserver)
high-priority1000000核心在线服务
normal100000一般业务服务
low-priority1000批处理、数据分析
best-effort0无需保证的任务

总结

主题核心要点
调度流程Filter(硬性约束)→ Score(软性偏好)→Bind
调度框架10+ 扩展点,插件化架构支持自定义
资源计算调度看 requests,运行看 limits,内存必须设 limit
亲和性节点亲和控制节点选择,Pod 亲和/反亲和控制 Pod 间位置关系
拓扑分布PodTopologySpread 是 PodAntiAffinity 的现代替代方案
污点容忍NoSchedule(新 Pod)、NoExecute(已有 Pod 也驱逐)
优先级/抢占PriorityClass 控制调度顺序和被保护程度,PDB 保护不可被抢占
自定义调度多调度器并存、调度扩展器、框架插件三种方案

掌握调度器的运作原理,是进行集群资源规划、故障排查、性能优化的基础。在生产环境中,调度策略直接影响应用的可用性、资源利用率和运维体验。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「云原生」更多文章

  1. Kubernetes多集群联邦:Karmada、Crossplane与Istio多集群实战
  2. CNCF云原生技术全景图:从毕业项目到前沿方向
  3. 容器运行时深度解析:从runc到containerd到安全容器