乐于分享
好东西不私藏

深入 K8s 调度器:架构设计、核心源码与 GPU 调度实战

深入 K8s 调度器:架构设计、核心源码与 GPU 调度实战

背景:基于 Kubernetes v1.28+ 代码库关键词:K8s Scheduler, Golang, 源码分析, Scheduling Framework, GPU 调度, Preemption

前言

Kubernetes 调度器(kube-scheduler)是集群资源调度的核心大脑。作为一名 Golang 后台开发者,深入理解其源码不仅能掌握高性能分布式系统的设计精髓,更是开发AI 训练平台高性能计算平台(如 GPU 调度)的基础。

本文将以代码走读的视角,从工程目录结构核心架构关键链路扩展实战(GPU 调度),带你彻底吃透 K8s 调度器。我们还将深入探讨 Scheduling Framework 的设计哲学以及 Profiles 多配置文件的强大能力。


1. 工程结构与架构设计

在深入逻辑之前,先通过目录结构理解其模块划分。K8s 调度器的代码主要分布在 pkg/scheduler 和 cmd/kube-scheduler 下。

1.1 核心代码目录

  • • cmd/kube-scheduler/: 程序的入口 main 函数,负责参数解析、配置加载和实例化。
  • • pkg/scheduler/: 调度器的核心逻辑库。
    • • scheduler.go: 定义了 Scheduler 结构体和主循环 Run()
    • • schedule_one.go: 单个 Pod 的完整调度工作流,包含核心的 schedulePod 逻辑。
    • • framework/Scheduling Framework 的核心实现,定义了所有扩展点接口(Interface)和运行时(Runtime)。
      • • plugins/: 内置的调度插件(如 NodeResourcesFit, TaintToleration, DefaultPreemption)。
      • • runtime/: 负责管理插件注册、初始化和执行的运行时环境。
    • • internal/queue/: 调度队列(SchedulingQueue)的实现,包含 ActiveQ, BackoffQ, UnschedulableQ 三级队列设计。
    • • internal/cache/: 调度器本地缓存(SchedulerCache),缓存 Node 和 Pod 的状态,优化查询性能。
    • • apis/config/: 调度器配置结构体定义,包括 KubeSchedulerProfile

1.2 架构设计

kube-scheduler 采用了典型的生产者-消费者模型,并结合了观察者模式(Informer)。值得注意的是,它引入了 Profiles 的概念,允许在同一个调度器实例中运行多个调度配置。

Watch

Add/Update

Add

Pop

Select Profile

Run Plugins

Filter/Score

Bind

API_Server

Informer

本地缓存

优先级队列

调度主循环

调度框架运行时

调度插件集

节点选择


2. 核心入口与初始化

2.1 启动入口

一切始于 cmd/kube-scheduler/scheduler.go 的 main 函数,它调用了 app.NewSchedulerCommand

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

// cmd/kube-scheduler/app/server.gofunc Run(ctx context.Context, cc *schedulerserverconfig.CompletedConfig) error {    // 1. 初始化 Scheduler 实例    // 这里会加载配置文件中定义的 Profiles,每个 Profile 对应一套 Framework    sched, err := scheduler.New(cc.Client,         scheduler.WithProfiles(cc.ComponentConfig.Profiles...),        scheduler.WithFrameworkOutOfTreeRegistry(outOfTreeRegistry),    )    // 2. 启动事件处理器 (EventHandlers),监听 Pod/Node 变化    // 这里会将 Informer 回调函数注册到队列操作中    addAllEventHandlers(sched, cc.InformerFactory, dynInformerFactory, unionedGVKs(sched))    // 3. 运行调度器    if err := sched.Run(ctx); err != nil {        return fmt.Errorf("non-nil error: %v", err)    }    return nil}

2.2 核心结构体 Scheduler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

// pkg/scheduler/scheduler.gotype Scheduler struct {    // 调度缓存,存储 Node 信息、Pod 分布等快照    Cache internalcache.Cache    // 调度队列,决定下一个调度哪个 Pod    SchedulingQueue internalqueue.SchedulingQueue    // 调度框架集合,Key 是 Profile Name (默认是 "default-scheduler")    // 每个 Profile 都有独立的 Framework 实例,包含不同的插件配置    Profiles profile.Map    // ...}


3. 核心逻辑:ScheduleOne 深度走读

调度器的主循环在 pkg/scheduler/scheduler.go 的 Run 方法中启动,核心是 scheduleOne 函数。

3.1 步骤一:从队列获取 Pod

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

// pkg/scheduler/schedule_one.gofunc (sched *Scheduler) scheduleOne(ctx context.Context) {    // 阻塞式获取,直到有 Pod 需要调度    podInfo, err := sched.NextPod()    // ...    // 根据 Pod 的 Spec.SchedulerName 选择对应的 Profile (Framework)    fwk, err := sched.profileForPod(pod)    if err != nil {        // ...        return    }    // 进入核心调度逻辑    scheduleResult, err := sched.SchedulePod(ctx, fwk, state, pod)    // ...}

代码亮点SchedulingQueue (internal/queue/scheduling_queue.go) 使用了Condition Variable (sync.Cond) 实现阻塞等待,避免了轮询(Polling)带来的 CPU 空转。同时实现了三级队列机制:

  1. 1. activeQ: 存放待调度的 Pod(堆结构,按优先级排序)。
  2. 2. backoffQ: 存放调度失败需退避重试的 Pod。
  3. 3. unschedulableQ: 存放因资源不足等原因无法调度的 Pod(等待事件触发移动到 activeQ)。

3.2 步骤二:调用调度框架 (Framework) – 调度周期

SchedulePod 方法是调度周期的核心,它依次执行 Filter 和 Score 阶段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

// pkg/scheduler/schedule_one.gofunc (sched *Scheduler) SchedulePod(ctx context.Context, fwk framework.Framework, state *framework.CycleState, pod *v1.Pod) (result ScheduleResult, err error) {    // 1. 获取节点快照    nodeInfoSnapshot, err := sched.Cache.UpdateSnapshot(sched.nodeInfoSnapshot)    // 2. 运行 PreFilter 插件    // 可以在这里拒绝 Pod,或者计算后续 Filter 需要的共享状态    status := fwk.RunPreFilterPlugins(ctx, state, pod)    // 3. 运行 Filter 插件 (核心:findNodesThatPassFilters)    // 并行过滤所有节点,排除不满足条件的节点    feasibleNodes, status := fwk.RunFilterPlugins(ctx, state, pod, nodeInfoSnapshot.NodeInfos)    if len(feasibleNodes) == 0 {        return ScheduleResult{}, &framework.FitError{}    }    // 4. 运行 PreScore 插件    status = fwk.RunPreScorePlugins(ctx, state, pod, feasibleNodes)    // 5. 运行 Score 插件 (核心:prioritizeNodes)    // 并行给剩余节点打分,并进行归一化 (NormalizeScore)    priorityList, status := fwk.RunScorePlugins(ctx, state, pod, feasibleNodes)    // 6. 选择最高分节点 (selectHost)    host, err := selectHost(priorityList)    return ScheduleResult{SuggestedHost: host, ...}, err}

并行优化:在 Filter 和 Score 阶段,调度器使用了 ParallelizeUntil (pkg/util/workqueue/parallelizer.go) 来并发处理节点,大幅提升了大集群下的调度吞吐量。

3.3 步骤三:异步绑定 (Assume & Bind) – 绑定周期

K8s 为了高吞吐,采用了乐观绑定(Optimistic Binding)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

    // Assume: 仅在内存 Cache 中更新,认为 Pod 已经绑定到了 Node    // 这一步非常快,无需网络 IO,防止同一 Node 资源被过度分配    err := sched.assume(pod, scheduleResult.SuggestedHost)    // 异步执行真正的 Bind 操作    gofunc() {        // 运行 Permit 插件 (允许等待,如 Gang Scheduling)        status := fwk.RunPermitPlugins(ctx, state, pod, scheduleResult.SuggestedHost)        // 运行 PreBind 插件 (如挂载云盘)        fwk.RunPreBindPlugins(ctx, state, pod, scheduleResult.SuggestedHost)        // 执行 Bind 插件 (调用 API Server)        fwk.RunBindPlugins(ctx, state, pod, scheduleResult.SuggestedHost)        // 运行 PostBind 插件        fwk.RunPostBindPlugins(ctx, state, pod, scheduleResult.SuggestedHost)    }()


4. 调度器扩展机制:Scheduling Framework

Scheduling Framework 将调度周期划分为多个扩展点(Extension Points),开发者只需实现对应的 Interface 即可。这是 K8s 调度器高度可扩展的基石。

4.1 关键扩展点全景图

扩展点
阶段
作用
典型应用
PreEnqueue
入队前
决定 Pod 是否可以进入队列
Scheduling Gates
QueueSort
入队
决定 Pod 在队列中的顺序
优先级排序 (PrioritySort)
PreFilter
调度前
预处理 Pod 信息,检查前置条件
校验 Pod PVC 是否存在
Filter
过滤
排除不合适的节点
NodeResourcesFit, TaintToleration
PostFilter
过滤后
处理调度失败的情况 (Preemption) DefaultPreemption (抢占)
PreScore
打分前
预计算打分所需信息
缓存公共数据
Score
打分
给节点打分 (0-100)
NodeAffinity, ImageLocality
Reserve
选中后
预留资源 (内存记账)
资源配额管理
Permit
绑定前
阻止或延迟绑定
Gang Scheduling (Coscheduling)
PreBind
绑定前
执行绑定前的准备工作
挂载云盘 (VolumeBinding)
Bind
绑定
执行绑定动作
DefaultBinder
PostBind
绑定后
绑定后的清理或通知
信息上报

4.2 Profiles 与多配置

通过 KubeSchedulerProfile,我们可以为不同的 Pod 启用不同的插件组合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

apiVersion: kubescheduler.config.k8s.io/v1kind: KubeSchedulerConfigurationprofiles:  - schedulerName: default-scheduler    plugins:      score:        disabled:        - name: NodeResourcesBalancedAllocation  - schedulerName: batch-scheduler    plugins:      queueSort:        enabled:        - name: Coscheduling      permit:        enabled:        - name: Coscheduling

Pod 通过 spec.schedulerName: batch-scheduler 即可使用第二套配置,启用 Coscheduling 插件。


5. 高级特性:抢占 (Preemption)

当 Filter 阶段结束后,如果没有找到合适的节点,调度器会进入 PostFilter 阶段。默认实现的插件是 DefaultPreemption

5.1 抢占流程

  1. 1. 检查资格: 只有高优先级的 Pod 才能抢占低优先级的 Pod。
  2. 2. 筛选节点: 找到那些”只要移除部分低优先级 Pod,就能容纳当前 Pod”的节点。
  3. 3. 选择受害者: 在候选节点中,选择”牺牲代价最小”的节点(考虑 PDB、优先级等)。
  4. 4. 执行驱逐: 删除被选中的低优先级 Pod(Victims)。
  5. 5. 更新队列: 被抢占的 Pod 会进入 backoffQ 或 unschedulableQ 等待重新调度。
1
2
3
4
5
6
7
8
9
10
11
12
13
14

// pkg/scheduler/framework/plugins/defaultpreemption/default_preemption.gofunc (pl *DefaultPreemption) PostFilter(ctx context.Context, state *framework.CycleState, pod *v1.Pod, m framework.NodeToStatusMap) (*framework.PostFilterResult, *framework.Status) {    // 1. 尝试寻找可以抢占的候选节点    candidates, err := pl.FindCandidates(ctx, state, pod, m)    // 2. 选择最佳候选节点    bestCandidate := pl.SelectCandidate(candidates)    // 3. 执行抢占操作 (删除 Victims)    status := pl.PerformPreemption(ctx, pod, bestCandidate, state)    return &framework.PostFilterResult{NominatedNodeName: bestCandidate.Name()}, status}


6. 实战:GPU 调度扩展

原生 K8s 调度器对 GPU 的支持仅限于”计数”(Scalar Resource),即只知道 Node 上有几张卡,不知道卡的型号、拓扑连接(NVLink/PCIe)等。这在 AI 训练场景下往往不够用。

6.1 需求场景

  1. 1. 拓扑感知 (Topology Awareness): 深度学习训练需要 GPU 之间高速互联,需将 Pod 调度到同一 NUMA 节点或 NVLink 组的 GPU 上。
  2. 2. Binpacking (装箱): 优先填满一个节点,通过碎片整理腾出空闲的大块资源给大模型训练。
  3. 3. 共享 GPU: 多个小任务共享一张 GPU 显存。

6.2 扩展方案:NodeResourceTopology

目前社区推荐的方案是结合 NFD (Node Feature Discovery) 和 Scheduler Plugin

  • • CRDNodeResourceTopology (由 NFD-Topology-Updater 上报)。
  • • Plugin: 在 Filter 和 Score 阶段读取 CRD 数据。

简单的 GPU Filter 插件代码示意

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

func (pl *GPUTopologyPlugin) Filter(ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeInfo *framework.NodeInfo) *framework.Status {    // 1. 检查 Pod 是否申请了 GPU    if !requiresGPU(pod) {        return framework.NewStatus(framework.Success)    }    // 2. 获取 Node 的 GPU 拓扑信息 (从 Informer 缓存获取 NodeResourceTopology CRD)    topology, err := pl.topologyLister.Get(nodeInfo.Node().Name)    if err != nil {        return framework.NewStatus(framework.Unschedulable, "Topology not found")    }    // 3. 检查拓扑约束 (例如要求 4 卡 NVLink 全互联)    // 这里需要复杂的算法来匹配 PCIe 树或 NVLink 矩阵    if !topology.Satisfies(pod.Requirements) {        return framework.NewStatus(framework.Unschedulable, "GPU topology mismatch")    }    return framework.NewStatus(framework.Success)}


7. 总结

Kubernetes 调度器的强大之处在于其分层架构插件机制

  • • ScheduleOne 主循环: 清晰定义的调度周期和绑定周期。
  • • Scheduling Framework: 提供了 12 个扩展点,覆盖了从入队到绑定的全生命周期。
  • • Profiles: 实现了单集群多调度策略的隔离。
  • • Preemption: 保证了高优先级任务的资源获取。

对于想要深入 K8s 调度的开发者,建议从编写一个简单的 Score 插件开始,逐步挑战复杂的 GPU 拓扑感知或 Gang Scheduling 插件。

掌握调度器源码,是你从”K8s 使用者”进阶为”云原生平台开发者”的关键一步。

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » 深入 K8s 调度器:架构设计、核心源码与 GPU 调度实战

评论 抢沙发

5 + 4 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
本站作品均采用知识共享署名-非商业性使用-相同方式共享 4.0进行许可,资源收集于网络仅供用于学习和交流,本站一切资源不代表本站立场,我们尊重软件和教程作者的版权,如有不妥请联系本站处理!

 沪ICP备2023009708号

© 2017-2026 夜雨聆风   | sitemap | 网站地图