简介

调度框架为kubernetes的调度器二次开发提供了丰富的扩展接口,基本上只需要实现特点扩展点的逻辑,并配合默认扩展点插件,进行组合,即可实现各种丰富的调度策略。关于调度器和调度框架,可以参考本博客前几篇介绍。

本文介绍基于kubernetes调度框架,实现一个调度插件的主要流程,并对其中的细节进行说明。该插件最终可以实现按照节点可用内存量作为唯一依据对pod进行调度。依赖的kubernetes版本以及开发测试集群的版本均为v1.21.6。

项目代码仓库:lwabish/k8s-scheduler: implements some custom scheduler based on kubernetes scheduler framework (github.com)

依赖问题

因为插件最终的运行方式是独立于默认的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为单位的整数。因此打分的核心逻辑是要实现内存可用量到分数的单调递增映射。

  1. 从prometheus api获取节点的实际剩余内存(此处需要一个外部配置:prometheus的endpoint)
  2. 将内存值进行归一化处理(此处需要一个外部配置:归一化时的内存最大值)
  3. 将归一化后的值作为参数进行sigmoid函数运算
  4. 将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

集群部署

  1. 仓库makefile可自动构建二进制,镜像,并将helm chart部署到集群。
  2. 集群部署不需要在调度器配置里指定kubeconfig。调度器会自动发现集群配置。
  3. 由于偷懒,本文直接给调度器的sa绑定了集群最高权限cluster-role:cluster-admin,不够安全。
  4. 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)

Next

基于kubernetes调度框架的自定义调度器实现-优化篇

文章目录