概述

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节点是否匹配节点亲和性
TaintTolerationPod 是否容忍节点的污点
VolumeBinding节点是否能绑定 Pod 请求的 PV
VolumeZonePV 的拓扑约束是否匹配
PodTopologySpread是否满足拓扑分布约束
NodeUnschedulable节点是否被 cordon
NodeMemoryPressure节点是否有内存压力
NodeDiskPressure节点是否有磁盘压力
NodePIDPressure节点是否有 PID 压力
NetworkFilter网络是否可用

打分阶段(Score)

打分阶段对每个候选节点打分(0-100),分数最高者胜出:

打分插件功能权重
NodeResourcesFit资源利用率打分
InterPodAffinityPod 亲和性打分
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 节点亲和性

特性nodeSelectornodeAffinity
匹配方式精确匹配支持多种操作符
硬约束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-pressurePID 压力不调度新 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-critical2000001000节点关键组件
system-cluster-critical2000000000集群关键组件
user-created0-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 ExtenderHTTP 扩展(旧方式)

自定义调度插件示例

// 自定义 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 conflictPV 拓扑约束冲突
Insufficient nvidia.com/gpuGPU 资源不足

调度器日志

# 查看调度器日志
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 调度器是一个高度可扩展的组件,核心要点:

  1. 两阶段调度:理解过滤和打分两个阶段,过滤排除不可行节点,打分选择最优节点。
  2. 亲和性选对类型:节点亲和性控制 Pod 调度到哪些节点,Pod 亲和性控制 Pod 之间的关系。
  3. 污点做隔离:用污点 + 容忍实现节点专用,比 nodeSelector 更灵活。
  4. 拓扑分布做高可用topologySpreadConstraints 是比 Pod 反亲和性更高效的高可用方案。
  5. 优先级做抢占:核心业务用高优先级,非核心用低优先级,资源紧张时自动保障核心业务。
  6. 配置优于自定义:大多数调度需求通过调度器配置文件就能满足,不需要写自定义插件。
  7. 资源 requests 必须配:调度器基于 requests 调度,不配 requests 的 Pod 会干扰调度决策。

调度器的调优是一个持续过程。建议定期 review Pod 调度分布、节点资源利用率,根据实际情况调整亲和性规则和拓扑约束。

参考资料与致谢

本文在撰写过程中参考了以下资料,感谢原作者的贡献:

  1. Kubernetes 调度器文档 — Kubernetes 官方,参考了Kubernetes 调度器文档相关内容