深入 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
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. activeQ: 存放待调度的 Pod(堆结构,按优先级排序)。 -
2. backoffQ: 存放调度失败需退避重试的 Pod。 -
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 |
|
|
|
| QueueSort |
|
|
|
| PreFilter |
|
|
|
| Filter |
|
|
|
| PostFilter |
|
处理调度失败的情况 (Preemption) | DefaultPreemption (抢占) |
| PreScore |
|
|
|
| Score |
|
|
|
| Reserve |
|
|
|
| Permit |
|
|
Gang Scheduling (Coscheduling) |
| PreBind |
|
|
|
| Bind |
|
|
|
| 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. 检查资格: 只有高优先级的 Pod 才能抢占低优先级的 Pod。 -
2. 筛选节点: 找到那些”只要移除部分低优先级 Pod,就能容纳当前 Pod”的节点。 -
3. 选择受害者: 在候选节点中,选择”牺牲代价最小”的节点(考虑 PDB、优先级等)。 -
4. 执行驱逐: 删除被选中的低优先级 Pod(Victims)。 -
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. 拓扑感知 (Topology Awareness): 深度学习训练需要 GPU 之间高速互联,需将 Pod 调度到同一 NUMA 节点或 NVLink 组的 GPU 上。 -
2. Binpacking (装箱): 优先填满一个节点,通过碎片整理腾出空闲的大块资源给大模型训练。 -
3. 共享 GPU: 多个小任务共享一张 GPU 显存。
6.2 扩展方案:NodeResourceTopology
目前社区推荐的方案是结合 NFD (Node Feature Discovery) 和 Scheduler Plugin。
-
• CRD: NodeResourceTopology(由 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 使用者”进阶为”云原生平台开发者”的关键一步。
夜雨聆风
