简介
在 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 计算框架。
通过这方面的学习,能在自定义调度器的开发中更加得心应手。