基于kubernetes调度框架的自定义调度器实现
kube-scheduler
是 kubernetes 的核心组件之一,主要负责整个集群资源的调度功能,根据特定的调度算法和策略,将 Pod 调度到最优的工作节点上面去,从而更加合理、更加充分的利用集群的资源,这也是我们选择使用 kubernetes 一个非常重要的理由。如果一门新的技术不能帮助企业节约成本、提供效率,我相信是很难推进的。
调度流程
默认情况下,kube-scheduler
提供的默认调度器能够满足我们绝大多数的要求,我们前面和大家接触的示例也基本上用的默认的策略,都可以保证我们的 Pod 可以被分配到资源充足的节点上运行。但是在实际的线上项目中,可能我们自己会比 kubernetes 更加了解我们自己的应用,比如我们希望一个 Pod 只能运行在特定的几个节点上,或者这几个节点只能用来运行特定类型的应用,这就需要我们的调度器能够可控。
kube-scheduler
的主要作用就是根据特定的调度算法和调度策略将 Pod 调度到合适的 Node 节点上去,是一个独立的二进制程序,启动之后会一直监听 API Server,获取到 PodSpec.NodeName
为空的 Pod,对每个 Pod 都会创建一个 binding。
kube-scheduler structrue
这个过程在我们看来好像比较简单,但在实际的生产环境中,需要考虑的问题就有很多了:
-
如何保证全部的节点调度的公平性?要知道并不是所有节点资源配置一定都是一样的
-
如何保证每个节点都能被分配资源?
-
集群资源如何能够被高效利用?
-
集群资源如何才能被最大化使用?
-
如何保证 Pod 调度的性能和效率?
-
用户是否可以根据自己的实际需求定制自己的调度策略?
考虑到实际环境中的各种复杂情况,kubernetes 的调度器采用插件化的形式实现,可以方便用户进行定制或者二次开发,我们可以自定义一个调度器并以插件形式和 kubernetes 进行集成。
kubernetes 调度器的源码位于 kubernetes/pkg/scheduler
中,大体的代码目录结构如下所示:(不同的版本目录结构可能不太一样)
kubernetes/pkg/scheduler
-- scheduler.go //调度相关的具体实现
|-- algorithm
| |-- predicates //节点筛选策略
| |-- priorities //节点打分策略
|-- algorithmprovider
| |-- defaults //定义默认的调度器
其中 Scheduler 创建
和运行的核心程序,对应的代码在 pkg/scheduler/scheduler.go
,如果要查看kube-scheduler
的入口程序,对应的代码在 cmd/kube-scheduler/scheduler.go
。
自定义调度器
一般来说,我们有4种扩展 Kubernetes 调度器的方法。
-
一种方法就是直接 clone 官方的 kube-scheduler 源代码,在合适的位置直接修改代码,然后重新编译运行修改后的程序,当然这种方法是最不建议使用的,也不实用,因为需要花费大量额外的精力来和上游的调度程序更改保持一致。
-
第二种方法就是和默认的调度程序一起运行独立的调度程序,默认的调度器和我们自定义的调度器可以通过 Pod 的
spec.schedulerName
来覆盖各自的 Pod,默认是使用 default 默认的调度器,但是多个调度程序共存的情况下也比较麻烦,比如当多个调度器将 Pod 调度到同一个节点的时候,可能会遇到一些问题, - 因为很有可能两个调度器都同时将两个 Pod 调度到同一个节点上去,但是很有可能其中一个 Pod 运行后其实资源就消耗完了;
- 并且维护一个高质量的自定义调度程序也不是很容易的,因为我们需要全面了解默认的调度程序,整体 Kubernetes 的架构知识以及各种 Kubernetes API 对象的各种关系或限制。
-
第三种方法是调度器扩展程序,这个方案目前是一个可行的方案,可以和上游调度程序兼容,所谓的调度器扩展程序其实就是一个可配置的 Webhook 而已,里面包含
过滤器
和优先级
两个端点,分别对应调度周期中的两个主要阶段(过滤和打分)。在我们现在的 v1.16 版本中上面的调度器扩展程序
也已经被废弃了,2022年4月1日后也被移除了。
Kubernetes 开始只提供了 Extender ,通过部署一个 Web 服务实现无侵入式扩展 scheduler插件,但其存在以下几个问题:
- **Extender 扩展点的数量是有限的:**在调度期间只有“Filter”和“Prioritize”扩展点。 “Preempt”扩展点在运行默认抢占机制后被调用。“Bind”扩展点用于绑定Pod,且 Bind 扩展点只能绑定一个扩展程序,扩展点会替换掉调度器的 bind 操作。Extender 不能在其他点被调用,例如,不能在运行 predicate 函数之前被调用。
- **性能问题:**对扩展程序的每次调用都涉及 JSON 的编解码。调用 webhook(HTTP 请求)也比调用原生函数慢。
- **调度器无法通知 Extender 已中止 Pod 的调度。**例如,如果一个 Extender 提供了一个集群资源,而调度器联系了 Extender 并要求它为正在调度的 pod 提供资源的实例,然后调度器在调度 pod 时遇到错误并决定中止调度,那么将很难与扩展程序沟通错误并要求它撤消资源的配置。
- 由于当前的Extender作为一个单独的进程运行,因此不能使用调度器的缓存。要么从 API Server构建自己的缓存,要么只处理他们从调度器接收到的信息。
-
第四种方法是通过调度框架(Scheduling Framework),Kubernetes v1.15 版本中引入了可插拔架构的调度框架,使得定制调度器这个任务变得更加的容易。调库框架向现有的调度器中添加了一组插件化的 API,该 API 在保持调度程序“核心”简单且易于维护的同时,使得大部分的调度功能以插件的形式存在,而且在我们现在的 v1.16 版本中上面的
调度器扩展程序
也已经被废弃了,2022年4月1日后也被移除了,所以以后调度框架才是自定义调度器的核心方式。Scheduling Framework 调度框架是一组新的“插件”API,通过这些 API 允许将许多调度功能使用插件实现,同时保持调度器“核心”简单和可维护。调度器插件必须用 Go 编写,并使用 Kubernetes 调度器代码编译,有一定学习成本。
这里我们可以简单介绍下调度框架(Scheduling Framework)方式的实现。