简介
调度框架为kubernetes的调度器二次开发提供了丰富的扩展接口,基本上只需要实现特点扩展点的逻辑,并配合默认扩展点插件,进行组合,即可实现各种丰富的调度策略。关于调度器和调度框架,可以参考本博客前几篇介绍。
本文介绍基于kubernetes调度框架,实现一个调度插件的主要流程,并对其中的细节进行说明。该插件最终可以实现按照节点可用内存量作为唯一依据对pod进行调度。依赖的kubernetes版本以及开发测试集群的版本均为v1.21.6。
依赖问题
因为插件最终的运行方式是独立于默认的scheduler的,所以需要一个main函数作为调度器的入口。正如参考资料里大佬所介绍的那样,我们不需要自己完整地山寨一个scheduler,只需要引用kubernetes scheduler里的部分函数作为入口即可。
但在动手时发现一个问题,kubernetes在设计上会独立发布各个模块,并对外屏蔽部分不稳定的代码,避免主仓库作为一个整体被外部引用,所以将部分模块的依赖版本写成了v0.0.0,然后再用replace替换成了代码仓库里的相对路径,即staging目录里的独立模块。如此一来,直接使用kubernetes代码是没问题的,但是使用go get或者go mod去获取kubernetes主仓库作为依赖时会遇到诸如此类的错误:k8s.io/api@v0.0.0: reading k8s.io/api/go.mod at revision v0.0.0: unknown revision
解决方法是用shell脚本,指定一个kubernetes版本,一方面直接下载官方仓库里被写成v0.0.0版本的各个模块的该版本的代码到本地作为依赖,另一方面修改go mod,将依赖replace成指定的版本,这样版本的需求和供给即实现了匹配。
该shell脚本仓库路径为:hack/get-k8s-as-dep.sh
核心实现
ScorePlugin接口实现
为了完成依据节点内存剩余量进行调度的功能,本插件主要修改调度器的打分阶段逻辑,即我们的逻辑会写在Score扩展点。因此我们的插件需要实现相应的接口
// pkg/scheduler/framework/interface.go
type ScorePlugin interface {
Plugin
// Score is called on each filtered node. It must return success and an integer
// indicating the rank of the node. All scoring plugins must return success or
// the pod will be rejected.
Score(ctx context.Context, state *CycleState, p *v1.Pod, nodeName string) (int64, *Status)
// ScoreExtensions returns a ScoreExtensions interface if it implements one, or nil if does not.
ScoreExtensions() ScoreExtensions
}
type Plugin interface {
Name() string
}
-
Name() string
方法简单返回插件名称即可 -
Score(ctx context.Context, state *CycleState, p *v1.Pod, nodeName string) (int64, *Status)
方法负责为每个待调度的pod+node进行打分 -
ScoreExtensions() ScoreExtensions
方法本文暂未用到,直接返回nil即可
打分逻辑
调度器打分时要求打分值为[0,100]范围内的整数,而节点的实际可用内存是以byte为单位的整数。因此打分的核心逻辑是要实现内存可用量到分数的单调递增映射。
- 从prometheus api获取节点的实际剩余内存(此处需要一个外部配置:prometheus的endpoint)
- 将内存值进行归一化处理(此处需要一个外部配置:归一化时的内存最大值)
- 将归一化后的值作为参数进行sigmoid函数运算
- 将sigmoid计算的结果转化为[0,100]范围内的整数,作为打分结果返回
参数解析
上面的打分逻辑里,prometheus的endpoint和内存最大值需要由用户配置输入。
在plugin的New方法中,使用k8s.io/kubernetes/pkg/scheduler/framework/runtime
包里的DecodeInto
方法,将runtime.Object
转为自定义的Arg结构体,然后便可以在扩展点打分时使用。
本地调试
代码编写完成后,可以在本地连接一个集群,实际测试一下调度器的效果。
调度器的配置文件对调度器的行为至关重要,在调试前需要先编写合适的配置文件,代码仓库有一个示例配置文件hack/config-sample.yml
供参考。
在以前的文章中,曾经观察过调度器的默认配置文件。我们仅需要在配置文件中写明需要修改的部分即可,未写的部分,会自动使用默认值。
scheduler名称
schedulerName: mem-scheduler
这里我们指定一个调度器名称,后面为pod指定调度器时指定这个名称即可
plugin组合
对于Score以外的扩展点,我们全部保存默认值,因此不需要在配置文件中体现
对于Score扩展点,为了简化打分逻辑,以本插件的打分逻辑作为唯一打分插件,可以在配置文件中禁用k8s默认的score扩展点插件,然后仅启用本插件,并设置权重为1
plugins:
score:
disabled:
- name: NodeResourcesBalancedAllocation
- name: ImageLocality
- name: InterPodAffinity
- name: NodeResourcesLeastAllocated
- name: NodeAffinity
- name: NodePreferAvoidPods
- name: PodTopologySpread
- name: TaintToleration
enabled:
- name: NodeAvailableMemory
weight: 1
选举行为
因为scheduler默认开启了leader election以支持高可用,默认配置下,选举过程中多个调度器副本会同时抢占kube-system命名空间下的lease对象。
对于我们的自定义调度器,需要指定一个不同于默认调度器的租约对象。如果不指定额外的租约对象,我们的调度器将成为默认调度器的后备军,会迟迟因为抢不到leader租约而不生效。
leaderElection:
resourceName: lwabish-scheduler
集群部署
- 仓库makefile可自动构建二进制,镜像,并将helm chart部署到集群。
- 集群部署不需要在调度器配置里指定kubeconfig。调度器会自动发现集群配置。
- 由于偷懒,本文直接给调度器的sa绑定了集群最高权限cluster-role:cluster-admin,不够安全。
-
hack/scheduler-test.yml
该deployment指定了我们的自定义调度器,用作测试。
参考
How to import 'any' Kubernetes package into your project? - Suraj Deshmukh
kubernetes 代码中的 k8s.io 是怎么回事?By李佶澳 (lijiaocn.com)
自定义 Kubernetes 调度器-阳明的博客|Kubernetes|Istio|Prometheus|Python|Golang|云原生 (qikqiak.com)