引言

Kubernetes 作为云原生时代的操作系统,其调度器(kube-scheduler)是整个集群的"决策大脑"。当你执行 kubectl apply -f pod.yaml 时,背后发生的一系列精密计算往往被开发者忽视。本文将从源码级别深入剖析 Kubernetes Pod 调度的完整生命周期,涵盖调度框架、预置策略的底层实现,以及如何编写自定义调度器来满足生产环境的复杂需求。

1. 调度器架构总览

kube-scheduler 本质上是一个监听 API Server 中 spec.nodeName 为空 Pod 的控制器。其核心架构经历了从 "scheduler" 到 "scheduling framework" 的演进:

  • v1.15 之前:单体调度逻辑,所有策略硬编码在 pkg/scheduler/ 目录
  • v1.15+:引入 Scheduling Framework,将调度流程拆分为 Queue、Filter、Score、Bind 等可插拔阶段
  • v1.22+:Plugin API 稳定,支持 Multi-Scheduling Profiles

一个完整的 Pod 调度经历以下阶段:

  1. SchedulingQueue:未调度 Pod 入队,按优先级排序(支持 Pod Priority 和 FIFO 子队列)
  2. Scheduling Cycle:为单个 Pod 选择最优节点,包括 Filter + Score + Select 三步
  3. Binding Cycle:将选定节点写入 Pod 的 spec.nodeName,异步执行以降低延迟

2. 调度框架的插件机制

Scheduling Framework 定义了 13 个扩展点(Extension Points),每个插件可注册到一个或多个扩展点:

扩展点类型作用
QueueSortInQueue排序调度队列中的 Pod
PreFilterPreFilter预处理 Pod 信息(如计算 Pod 的资源需求)
FilterFilter过滤不满足条件的节点(硬性约束)
PostFilterPostFilter全部节点被过滤后的后置处理(如抢占逻辑)
PreScorePreScore评分前的预处理
ScoreScore为通过过滤的节点打分
Reserve / UnreserveBind预留/释放节点资源
PermitPermit延迟绑定(等待或拒绝绑定)
PreBind / PostBindBind绑定前后的钩子(如存储卷挂载通知)
BindBind执行节点绑定(默认是 API Server 写入)

这套插件机制使得我们可以用极小的代码量实现自定义逻辑。

3. 预置策略的源码级解析

3.1 NodeResourcesFit(资源适配)

这是最常用的 Filter 插件,位于 pkg/scheduler/framework/plugins/noderesources/fit.go。它实现了三种检查策略:

  • LeastAllocated:选择剩余资源最少的节点(有利于资源集中,减少碎片)
  • MostAllocated:选择剩余资源最多的节点(适合批处理作业,方便装箱)
  • RequestedToCapacityRatio:基于请求/容量的综合比值打分

NodeResourcesFit 的 Score 函数实现非常优雅——它不是简单地用 used/total,而是使用线性归一化公式:

score = (1 - used/total) * maxScore

3.2 NodeAffinity(节点亲和性)

该插件实现了 nodeAffinity 的过滤和打分逻辑,对应 API 字段 spec.affinity.nodeAffinity。关键实现位于 pkg/scheduler/framework/plugins/nodeaffinity/node_affinity.go:

  • Filter 阶段:只保留满足 requiredDuringSchedulingIgnoredDuringExecution 的节点
  • Score 阶段:匹配 preferredDuringSchedulingIgnoredDuringExecution 中的权重规则加分

3.3 TaintToleration(污点容忍)

Taint 是节点主动"拒绝"Pod 的机制。TaintToleration 插件的 Filter 逻辑是:遍历 Pod 的所有 Toleration,检查是否能匹配节点的 Taint。这里有一个容易被忽略的细节:tolerationSeconds 不仅控制 Pod 在节点上的容忍时长,还影响调度器是否会将 running 的 Pod 驱逐——这个驱逐逻辑由 TaintManager controller 实现。

3.4 InterPodAffinity(Pod 间亲和/反亲和)

这是最复杂的预置插件之一。它实现了:

  • 跨节点计数:计算同 Pod 标签匹配的节点数量,实现拓扑域(Topology Domain)约束
  • 加权反亲和:通过 AntiAffinityTermsByTargetNode 实现将副本分散到不同故障域
  • 优化计算:使用 topologyPairToPodCounts 缓存避免重复遍历所有 Pod

4. 自定义调度器实战

当预置策略无法满足需求时,有三种扩展方式(从简到繁):

方案一:Scheduler Extender(外部扩展器)

在 kube-scheduler 启动时通过 --config 指定 extender URL,调度器会在 Filter/Score 阶段将候选节点转发到外部 HTTP 服务。优点是不需要重新编译调度器,缺点是引入网络调用增加延迟。

方案二:Scheduling Profile + 自定义插件

编写一个实现 Plugin 接口的 Go 包,编译成独立的调度器二进制文件,以独立 Deployment 运行:

package main

import (
    "context"
    "k8s.io/kubernetes/pkg/scheduler/framework"
)

type CustomScheduler struct {}

func (cs *CustomScheduler) Name() string { return "CustomScheduler" }

func (cs *CustomScheduler) Filter(ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeInfo *framework.NodeInfo) *framework.Status {
    // 自定义过滤逻辑:只选择带有特定标签的节点
    if nodeInfo.Node().Labels["region"] != "us-east-1" {
        return framework.NewStatus(framework.Unschedulable, "node not in target region")
    }
    return framework.NewStatus(framework.Success)
}

然后注册到调度框架配置中:

apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
  - schedulerName: my-custom-scheduler
    plugins:
      filter:
        enabled:
          - name: CustomScheduler
        disabled:
          - name: NodeResourcesFit

方案三:独立调度器(完全自主实现)

对于最复杂的场景,可以完全放弃 kube-scheduler,自己实现一个调度器:

  • 监听 API Server 获取待调度 Pod
  • 执行自定义调度算法
  • 通过 Patch Pod 的 spec.nodeName 完成绑定

这种方式在机器学习训练场景(如 Volcano、Koordinator 等批调度器)中非常常见。

5. 调度性能优化实践

在大规模集群(5000+ 节点)中,调度器可能成为瓶颈。以下是一些生产级优化策略:

  • PercentageOfNodesToScore:预置在 50%(可调整到 10%-100%),当节点数超过 thresholdNodesPercentage(默认 50 节点)时,只对一定比例的节点打分
  • Parallelism:控制同一时刻并行执行的调度 Goroutine 数(默认 16,建议根据集群规模调整)
  • Informer 优化:使用 SharedInformer 减少 API Server 压力,本地缓存 Node/Topology 数据
  • 事件驱动重试:Node/Pod 变更事件入队时采用 AddAfter 延迟而非立即重试,避免抖动

6. 总结

Kubernetes 调度器的设计体现了"渐进式暴露复杂性"的理念:默认情况下你只需要定义资源的 requests/limits,高级场景则可以通过 Pod Affinity、Topology Spread Constraints、自定义调度器等方式精确控制 Pod 的分布。理解 Scheduling Framework 的插件机制,是掌握 Kubernetes 调度的关键一步。

下一篇文章我们将深入 kube-scheduler 的抢占(Preemption)机制——当一个高优先级 Pod 因资源不足无法调度时,系统如何驱逐低优先级 Pod 来腾出空间。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部