本篇文章介绍一下 Kubernetes 的默认调度器 kube-scheduler 的源码实现。kubernetes 代码版本:v1.18.4-rc.0。
0. 入口
入口函数在路径 kubernetes/cmd/kube-scheduler/scheduler.go#main()
,如下
1 | func main() { |
核心逻辑就是:1. 创建一个 SchedulerCommand(第 4 行);2. 接收参数并执行(第 14 行)。我们先看一下创建 SchedulerCommand 的逻辑。
1 | // NewSchedulerCommand creates a *cobra.Command object with default parameters and registryOptions |
首先我们可以看到 NewSchedulerCommand 接收一个不定参数,registryOptions。从名字我们可以看出来首先这个参数是作用于一个 Registry 的,这个 Registry 实际上就是用来管理 kuberentes 中的 plugin 的。
1 | // Registry is a collection of all available plugins. The framework uses a |
而 registryOptions 中的 option 其实是一种函数传参的方式的使用。option 传参的方式最早由 Rob Pike 提出来的,简单来说就是将可选的 option 参数封装成多个函数传给目标函数,然后在目标函数内部通过调用 option 函数的方式来初始化。后面我们看到 RegistryOptions 初始化的部分再来介绍。对于 option 这种方式感兴趣的同学可以参考我之前的一篇文章:http://legendtkl.com/2016/11/05/code-scalability/
其次是 cmd,通过 cobra.Command 构建出来的一个 CLI 处理工具,对于命令行的输入通过第 18 行的匿名函数来处理,匿名函数内部会调用函数 runCommand 来启动 scheduler 进程。去掉一些不重要的代码逻辑,runCommand 主要做的事情就是创建 scheduler 参数,然后通过 Run 函数启动 scheduler 进程。
1 | // runCommand runs the scheduler. |
Run 函数的主要逻辑如下:
- 初始化 Registry,第 6 ~ 11 行就是 option 这种函数传参的处理逻辑。
- 创建 scheduler 实例
- 其他初始化操作,包括 EventBroadcast、健康检测、metric 等相关逻辑
- 启动 Pod Informer 来监听 Pod
- 运行调度器(分没有启动 leader 选举,但是对应的方法都是 sched.Run 方法)
1 | // Run executes the scheduler based on the given configuration. It only returns on error or when context is done. |
scheduler 实例
首先我们看一下 scheduler 的定义,路径为 pkg/scheduler/scheduler.go
。
1 | // Scheduler 监听未调度的 Pod,为其寻找适合的 Node 节点,并写回到 api server |
运行调度器
下面看一下调度器
1 | // Run begins watching and scheduling. It waits for cache to be synced, then starts scheduling and blocked until the context is done. |
在 scheduler 的 Run 函数中主要做了三件事情:
- 等待 scheduler cache 同步(scheduler 刚起来,相当于冷启动)
- 运行调度器队列的 Run 函数
- 运行 scheduler 的 scheduleOne 函数
调度队列
调度队列的 Run 函数第一次看到总是给你一点点疑惑,作为一个队列难道还需要启动吗?确实是这样,如果调度队列只是一个优先级队列,那么确实不需要启动。kubernetes 中的调度队列是由三个队列组成,分别是:
- activeQueue:待调度的 pod 队列,scheduler 会监听这个队列
- backoffQueue:在 kubernetes 中,如果调度失败了,就相当于一次 backoff。backoffQueue 专门用来存放 backoff 的 pod。一般会有一个 backoffLimit 的限制就是最多容忍多少次 backoff。其次每次 backoff 之间的时间成倍增长。
- unschedulableQueue:调度过程被终止的 pod 存放的队列。
调度队列的 Run 函数做的事情就是将 backoffQueue 和 unschedulableQueue 中 pod 定期移动到 activeQueue 中。
1 | // Run starts the goroutine to pump from podBackoffQ to activeQ |
其中 wait.Until
实际上就是一个类似 Cron 的定时调度器。细节实现暂时不细说了。
1 | // Until loops until stop channel is closed, running f every period. |
我们再来看一下两个 flush 函数的逻辑。首先是 flushBackoffQCompleted()
,主要逻辑如下:
- 取出 BackoffQueue 中优先级最高的 Pod,或者说下一次调度时间最近的 Pod。这里感兴趣的同学可以看一下 BackoffQueue 的初始化过程,其中有一个 lessFunc 用来做 compare Operator 的,这个 lessFunc 也是复用了下面代码片段中的
getBackoffTime
函数。 - 计算 Pod 的 backoffTime,实际上就是下一次应该调度的时间。
- backoffTime 的计算逻辑在函数
calculateBackoffDuration()
中,我们可以看到 backoff duration 是依次递增为上一次的 2 倍,并且有一个上限值。
1 | // flushBackoffQCompleted Moves all pods from backoffQ which have completed backoff in to activeQ |
下面我们看一下 unschedulableQueue 中的 pod 是如何 flush 的,也就是函数 flushUnschedulableQLeftover
的实现逻辑。逻辑非常简单,如果 pod 在 unschedulableQueue 中停留时间超过了 60s,就会被移除到 activeQueue。
1 | // flushUnschedulableQLeftover moves pod which stays in unschedulableQ longer than the unschedulableQTimeInterval |
scheduler cache
//未完待续