简介
在 Kubernetes 中,调度是指将 Pod 放置到合适的 Node 上,然后对应 Node 上的 kubelet 才能够运行这些 Pod。K8s scheduler 就是用来调度 pod 的一个组件。
本文主要是通过源码了解调度器的部分工作流程。

源码分析
Based on Kubernetes v1.19.11.
K8s scheduler 主要的数据结构是:
- Scheduler。
- SchedulingQueue。
相关的代码流程主要分为两个部分:
cmd/kube-scheduler,这里是我们调度器的起始处,主要是读取配置,初始化并启动调度器。pkg/scheduler,这里是调度器的核心代码。
数据结构
Scheduler
1 | // pkg/scheduler/scheduler.go |
SchedulerCache,保存了调度所需的 podStates 和 nodeInfos。Algorithm,会使用该对象的Schedule方法来运行调度逻辑。SchedulingQueue,调度队列。Profiles,调度器配置。
SchedulingQueue
Interface
1 | // pkg/scheduler/internal/queue/scheduling_queue.go |
Implementation
1 | // PriorityQueue implements a scheduling queue. |
- PodNominator:调度算法调度的结果,保存了 Pod 和 Node 的关系。
- cond:用来控制调度队列的 Pop 操作。
- activeQ:用堆维护的优先队列,保存着待调度的 pod,其中优先级默认是根据 Pod 的优先级和创建时间来排序。
- podBackoffQ:同样是用堆维护的优先队列,保存着运行失败的 Pod,优先级是根据
backOffTime来排序,backOffTime受podInitialBackoffDuration以及podMaxBackoffDuration两个参数影响。 - unschedulableQ:是一个 Map 结构,保存着暂时无法调度(可能是资源不满足等情况)的 Pod。
cmd/kube-scheduler
调度器的入口 main
最开始,scheduler 在 cmd/kube-scheduler/scheduler.go 使用 NewSchedulerCommand() 初始化命令并执行命令。
1 | // cmd/kube-scheduler/scheduler.go |
初始化调度器命令 NewSchedulerCommand
NewSchedulerCommand() 会读取配置文件和参数,初始化调度命令,其中最主要的函数是 runCommand()。
1 | func NewSchedulerCommand(registryOptions ...Option) *cobra.Command { |
执行调度器命令 runCommand
runCommand 主要分为两个重要步骤:
Setup:读取配置文件以及参数,初始化调度器。这里的配置文件包括 Profiles 配置等。Run:运行调度器所需的组件,例如健康检查服务,Informer 等。然后使用Setup得到的调度器运行调度的主流程。
1 | func runCommand(cmd *cobra.Command, opts *options.Options, registryOptions ...Option) error { |
创建调度器 Setup
Setup 会根据配置文件和参数创建 scheduler。这里个人觉得最主要的是 Profiles,里面定义了调度器的名字,以及 scheduling framework 的插件配置。还有一些可以用来调优的参数,例如 PercentageOfNodesToScore, PodInitialBackoffSeconds , PodMaxBackoffSeconds 等。
并且 scheduler.New() 中会有一个 addAllEventHandlers(sched, informerFactory, podInformer) 函数,启动所有资源对象的事件监听,来根据情况调用对应的回调函数,这些回调函数同时也会影响调度队列的运行过程。
1 | func Setup(ctx context.Context, opts *options.Options, outOfTreeRegistryOptions ...Option) (*schedulerserverconfig.CompletedConfig, *scheduler.Scheduler, error) { |
运行调度器 Run
Run 主要是启动一些组件,然后调用 sched.Run(ctx) 进行调度的主流程。
1 | func Run(ctx context.Context, cc *schedulerserverconfig.CompletedConfig, sched *scheduler.Scheduler) error { |
pkg/scheduler
运行调度器主流程
Run 会启动 scheduling queue,并不断调用 sched.scheduleOne() 进行调度。
1 | // Run begins watching and scheduling. It waits for cache to be synced, then starts scheduling and blocked until the context is done. |
运行调度队列
1 | // Run starts the goroutine to pump from podBackoffQ to activeQ |
调度队列的运行逻辑:
- 每隔 1s 检查
podBackoffQ是否有 pod 可以放入activeQ中。检查的逻辑是判断backOffTime是否已经到期。 - 每隔 30s 检查
unschedulableQ是否有 pod 可以放入activeQ中。
单个 Pod 的调度 scheduleOne
在介绍 scheduleOne 之前,看这张 pod 调度流程图能有助于我们理清整个过程。同时这也是 k8s v1.15 开始支持的 Scheduling Framework 的 Plugin 扩展点。

1 | // scheduleOne does the entire scheduling workflow for a single pod. It is serialized on the scheduling algorithm's host fitting. |
ScheduleOne 是调度器的主流程,主要包括以下几步:
- 调用
sched.NextPod()拿到下一个需要调度的 pod。后面会对这个过程进行更详细的介绍。 - 调用
sched.profileForPod(pod),根据 pod 中的 schedulerName 拿到针对该 pod 调度的 Profiles。这些 Profiles 就包括了调度插件的配置等。 - 进行上图中的 Scheduling Cycle 部分,这部分是单线程运行的。
- 调用
sched.Algorithm.Schedule()。此处包括好几个步骤,其中PreFilter,Filter被称为 Predicate,是对节点进行过滤,这里面考虑了节点资源,Pod Affinity,以及 Node Volumn 等情况。而PreScore,Score,Nomalize Score又被称为 Priorities,是对节点进行优选打分,这里会得到一个适合当前 Pod 分配上去的 Node。 - 进行
Reserve操作,将调度结果缓存。当后面的调度流程执行失败,会进行Unreserve进行数据回滚。 - 进行
Permit操作,这里是用户自定义的插件,可以使 Pod 进行 allow(允许 Pod 通过 Permit 阶段)、reject(Pod 调度失败)和 wait(可设置超时时间)这三种操作。对于 Gang Scheduling (一批 pod 同时创建成功或同时创建失败),可以在Permit对 Pod 进行控制。
- 调用
- 进行图中的 Binding Cycle 部分,这部分是起了一个 Goroutine 去完成工作的,不会阻塞调度主流程。
- 最开始会进行
WaitOnPermit操作,这里会阻塞判断 Pod 是否 Permit,直到 Pod Permit 状态为 allow 或者 reject 再往下继续运行。 - 进行
PreBind,Bind,PostBind操作。这里会调用 k8s apiserver 提供的接口b.handle.ClientSet().CoreV1().Pods(binding.Namespace).Bind(ctx, binding, metav1.CreateOptions{}),将待调度的 Pod 与选中的节点进行绑定,但是可能会绑定失败,此时会做Unreserve操作,将节点上面 Pod 的资源解除预留,然后重新放置到失败队列中。
- 最开始会进行
当 Pod 与 Node 绑定成功后,Node 上面的 kubelet 会 watch 到对应的 event,然后会在节点上创建 Pod,包括创建容器 storage、network 等。等所有的资源都准备完成,kubelet 会把 Pod 状态更新为Running。
SchedulingQueue 细节
获取下一个运行的 Pod
调度的时候,需要获取一个调度的 pod,即 sched.NextPod() ,其中调用了 SchedulingQueue 的 Pop() 方法。
当 activeQ 中没有元素,会通过 p.cond.Wait() 阻塞,直到 podBackoffQ 或者 unschedulableQ 将元素加入 activeQ 并通过 cond.Broadcast() 来唤醒。
1 | // Pop removes the head of the active queue and returns it. It blocks if the |
将 Pod 加入 activeQ
当 pod 加入 activeQ 后,还会从 unschedulableQ 以及 podBackoffQ 中删除对应 pod 的信息,并使用 cond.Broadcast() 来唤醒阻塞的 Pop。
1 | // Add adds a pod to the active queue. It should be called only when a new pod |
当 Pod 调度失败时进入失败队列
当 pod 调度失败时,会调用 sched.Error() ,其中调用了 p.AddUnschedulableIfNotPresent() .
决定 pod 调度失败时进入 podBackoffQ 还是 unschedulableQ :如果 moveRequestCycle 大于 podSchedulingCycle ,则进入 podBackoffQ ,否则进入 unschedulableQ .
1 | // AddUnschedulableIfNotPresent inserts a pod that cannot be scheduled into |
何时 moveRequestCycle >= podSchedulingCycle :
- 我们在集群资源变更的时候(例如添加 Node 或者删除 Pod),会有回调函数尝试将
unschedulableQ中之前因为资源不满足需求的 pod 放入activeQ或者podBackoffQ,及时进行调度。 - 调度队列会每隔 30s 定时运行
flushUnschedulableQLeftover,尝试调度unschedulableQ中的 pod。
这两者都会调用 movePodsToActiveOrBackoffQueue 函数,并将 moveRequestCycle 设为 p.schedulingCycle.
1 | func (p *PriorityQueue) movePodsToActiveOrBackoffQueue(podInfoList []*framework.QueuedPodInfo, event string) { |
podBackoffQ 中 pod 的生命周期
加入 podBackoffQ
有两种情况会让 pod 加入 podBackoffQ:
- 调度失败。如果调度失败,并且集群资源发生变更,即
moveRequestCycle >= podSchedulingCycle,pod 就会加入到 podBackoffQ 中。 - 从 unschedulableQ 中转移。当集群资源发生变化的时候,最终会调用
movePodsToActiveOrBackoffQueue将 unschedulableQ 的 pod 转移到 podBackoffQ 或者 activeQ 中。转移到 podBackoffQ 的条件是p.isPodBackingoff(pInfo),即 pod 仍然处于 backoff 状态。
退出 podBackoffQ
调度器会定时让 pod 从 podBackoffQ 转移到 activeQ 中。
在 sched.SchedulingQueue.Run 中运行的 flushBackoffQCompleted cronjob 会每隔 1s 按照优先级(优先级是按照 backoffTime 排序)依次将满足 backoffTime 条件的 pod 从 podBackoffQ 转移到 activeQ 中,直到遇到一个不满足 backoffTime 条件的 pod。
unschedulableQ 中 pod 的生命周期
加入 unschedulableQ
只有一种情况会让 pod 加入 unschedulableQ,那就是调度失败。如果调度失败,并且集群资源没有发生变更,即 moveRequestCycle < podSchedulingCycle ,那么 pod 就会加入到 unschedulableQ 中。
退出 unschedulableQ
调度器会同样定时让 pod 从 unschedulableQ 转移到 podBackoffQ 或者 activeQ 中。
在 sched.SchedulingQueue.Run 中运行的 flushUnschedulableQLeftover 最终会调用 movePodsToActiveOrBackoffQueue 将 pod 分别加入到 podBackoffQ 或者 activeQ 中。
总结
Kubernetes scheduler 是 kubernetes 中相当重要的组件,基本上各个云平台都会根据自己的业务模型和需求自定义调度器,例如 华为的 Volcano 计算框架。
通过这方面的学习,能在自定义调度器的开发中更加得心应手。