1. 概述

Kubernetes 调度 Pod 时 kube-scheduler 基于 Kubernetes 管理的资源进行调度,这些资源包括 CPU、内存、节点运行 Pod 数量等。 但是,某些场景中用户需要根据某些特定资源进行上下文调度,kube-scheduler 无法获取这些信息,此时我们需要根据需要对 Kubernetes Scheduler 进行扩展。

当前 Kubernetes 提供了以下几种不同的方式扩展调度器:

  • Multi Scheduling Profiles
  • Scheduler Extender
  • Scheduling Framework
  • 修订 Kube Scheduler 编译二进制文件

上述扩展方式中 Scheduling Framework 是 Kubernetes 提供一种新的扩展架构,它在现有的 kube-scheduler 代码中添加了一组扩展点(Extension points),用户可以在不改动 Kubernetes 核心代码的情况下添加自己的 Plugin。

通过 Scheduling Framework 方式进行扩展时,需要重新编译 kube-scheduler。严格说来这对 Kubernetes 产生了一点侵入性,但考虑到性能、灵活性、便利性的优势,目前官方倡导用户使用 Scheduling Framework 扩展调度器功能。

2. Scheduling Framework

Kubernetes Scheduler 调度一个 Pod 到某个 Node 过程分为 Scheduling Cycle 和 Binding Cycle 两个阶段,我们通常将它们统称为 Scheduling Context(调度上下文)。

其中,Scheduling Cycle 为 Pod 选择一个节点,Binding Cycle 将该 Pod 和节点绑定(更新 ETCD)。在同一个调度器中,只有一个 Pod 处于 Scheduling Cycle 阶段,而多个 Binding Cycle 可以并行运行。

当 Scheduling Cycle 或 Binding Cycle 不可调度或者过程中发生内部错误时,调度流程将被终止,被调度的 Pod 将会被放回调度队列,等待下回重试。

Scheduling Framework 框架中,预留了非常多的扩展点(Extension points),下图中所有类型扩展点用户都可以在 pkg/scheduler/framework/interface.go 文件中找到对应的接口定义。

本文不再一一介绍每个扩展点的具体功能,有兴趣的读者可以自行阅读官方文档

img.png

3. 开发示例

为了演示如何通过 Scheduling Framework 扩展调度器,本文将基于 PostBind 扩展点实现一个自定义调度插件。

1. 接口和业务逻辑实现

从 pkg/scheduler/framework/interface.go 文件接口定义可以知道,基于 PostBind 扩展调度器时,我们需要实现 Name() 和 PostBind(),其中 Name() 接口用于自定义插件的注册和配置,而 PostBind() 用于实现我们的业务逻辑。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16

// Plugin is the parent type for all the scheduling framework plugins.
type Plugin interface {
    Name() string
}

// PostBindPlugin is an interface that must be implemented by "PostBind" plugins.
// These plugins are called after a pod is successfully bound to a node.
type PostBindPlugin interface {
	Plugin
	// PostBind is called after a pod is successfully bound. These plugins are
	// informational. A common application of this extension point is for cleaning
	// up. If a plugin needs to clean-up its state after a pod is scheduled and
	// bound, PostBind is the extension point that it should register.
	PostBind(ctx context.Context, state *CycleState, p *v1.Pod, nodeName string)
}

参考 Kubernetes 代码现有的目录结构,我们在 pkg/scheduler/framework/plugins/ 路径下创建新目录,用于存放自定义插件的代码,代码框架如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// pkg/scheduler/framework/plugins/spscheduler/postbind.go

package spscheduler

import (
	"context"
	v1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/runtime"
	clientset "k8s.io/client-go/kubernetes"
	"k8s.io/klog/v2"
	"k8s.io/kubernetes/pkg/scheduler/framework"
)

type SPPostBind struct {
	apiclient clientset.Interface
}

var _ framework.PostBindPlugin = &SPPostBind{}

// Name is the name of the plugin used in Registry and configurations.
const (
	Name = "SPPostBind"
)

func (s *SPPostBind) Name() string {
	return Name
}

func (s *SPPostBind) PostBind(ctx context.Context, state *framework.CycleState, p *v1.Pod, nodeName string) {

	// TODO: just log information
	klog.Infof("[SPPostBind] scheduler <%s> on node<%s>", p.Name, nodeName)
	return

}

// New initializes a new plugin and returns it.
func New(args runtime.Object, fh framework.Handle) (framework.Plugin, error) {
	return &SPPostBind{}, nil
}

2. 注册插件和配置文件

定义完成调度插件后,我们需要在 kube-scheduler 代码中注册 SPPostBind 插件。

为了尽量减少对 Kubernetes 源码的侵入,我们选择编辑 cmd/kube-scheduler/scheduler.go 文件注册插件(PS:当然你可以选择改其他地方)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main

import (
	"k8s.io/kubernetes/pkg/scheduler/framework/plugins/spscheduler"
	"os"

	"k8s.io/component-base/cli"
	_ "k8s.io/component-base/logs/json/register" // for JSON log format registration
	_ "k8s.io/component-base/metrics/prometheus/clientgo"
	_ "k8s.io/component-base/metrics/prometheus/version" // for version metric registration
	"k8s.io/kubernetes/cmd/kube-scheduler/app"
)

func main() {

	command := app.NewSchedulerCommand(app.WithPlugin(spscheduler.Name, spscheduler.New))
	code := cli.Run(command)
	os.Exit(code)
}

完成上述修订后,我们编译 kube-scheduler 并定义以下配置文件(如何编译请参考官方文档,建议配置****减轻复杂度),将 SPPostBind 加入默认调度器中。 需要注意的是 kube-scheduler 指定 –config 参数时会覆盖命令行中的 –kubeconfig 参数,因此我们需要在 KubeSchedulerConfiguration 中定义该参数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
apiVersion: kubescheduler.config.k8s.io/v1beta3
kind: KubeSchedulerConfiguration
clientConnection:
  kubeconfig: /etc/kubernetes/kube-scheduler.conf
profiles:
  - schedulerName: default-scheduler
    plugins:
      postBind:
        enabled:
        - name: SPPostBind

3. 自定义参数

通常一些项目中需要为自定义插件提供可配置的参数,参考 Scheduler Framework Plugins,可以实现该功能。

在以下文件参数 SPPostBindArgs 作为上述插件的自定义配置 struct(PS:需要注意 struct 的名称格式为 <插件名>+Args):

  • pkg/scheduler/apis/config/types_pluginargs.go
  • staging/src/k8s.io/kube-scheduler/config/v1beta3/types_pluginargs.go
1
2
3
4
5
6
7
8
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

// SPPostBindArgs holds arguments used to configure the
// SPPostBindPlugin plugin.
type SPPostBindArgs struct {
	metav1.TypeMeta `json:",inline"`
	Kubeconfig      string `json:"kubeconfig,omitempty"`  // kubeconfig 作为自定义配置项
}

定义完 SPPostBindArgs 对象后,在以下文件的 addKnownTypes() 中注册 SPPostBindArgs 对象。

  • staging/src/k8s.io/kube-scheduler/config/v1beta3/register.go
  • pkg/scheduler/apis/config/register.go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// addKnownTypes registers known types to the given scheme
func addKnownTypes(scheme *runtime.Scheme) error {
	scheme.AddKnownTypes(SchemeGroupVersion,
		&KubeSchedulerConfiguration{},
		&DefaultPreemptionArgs{},
		&InterPodAffinityArgs{},
		&NodeResourcesBalancedAllocationArgs{},
		&NodeResourcesFitArgs{},
		&PodTopologySpreadArgs{},
		&VolumeBindingArgs{},
		&NodeAffinityArgs{},
		&SPPostBindArgs{},  // 注册 SPPostBindArgs 对象
	)
	return nil
}

在 pkg/scheduler/apis/config/v1beta3/defaults.go 文件中可以添加 SetDefaults_SPPostBindArgs() 函数,来自动生成 SPPostBindArgs 的默认值。

1
2
3
4
5
func SetDefaults_SPPostBindArgs(obj *v1beta3.SPPostBindArgs) {
	if obj.Kubeconfig == "" {
		obj.Kubeconfig = "/etc/kubernetes/admin.conf"
	}
}

执行以下命令重新生成代码,此时会自动更新两处 zz_generated.conversion.go 文件和一处 zz_generated.defaults.go 文件(PS: 更新完成后可以执行 go mod vendor 更行 vendor 目录下的文件):

1
2
./hack/update-codegen.sh
 make generated_files

更新 postbind.go 中的 New() 和配置文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package spscheduler
// New initializes a new plugin and returns it.
func New(args runtime.Object, fh framework.Handle) (framework.Plugin, error) {
    postBindArgs, isOK := args.(*config.SPPostBindArgs)
    if !isOK {
        return nil, fmt.Errorf("got args of type %T, values %v, want *SPPostBindArgs", args, args)
    }
    klog.Infof("[SPScheduler] Read user kubeconfig: %v", postBindArgs.Kubeconfig)
    return &SPPostBind{}, nil
}

完成上述修改后重新编译 kube-scheduler,并修改 KubeSchedulerConfiguration 定义如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
apiVersion: kubescheduler.config.k8s.io/v1beta3
kind: KubeSchedulerConfiguration
clientConnection:
  kubeconfig: /etc/kubernetes/kube-scheduler.conf
profiles:
  - schedulerName: default-scheduler
    plugins:
      postBind:
        enabled:
        - name: SPPostBind
    pluginConfig:
    - name: SPPostBind
      args:
        kubeconfig: "/etc/kubeconfig/custom.conf"

4. 总结

从上述描述来看,通过 scheduling-framework 扩展调度器并不复杂,但由于需要重新编译 kubernetes 代码,导致和内部 CICD 工具集成不太方便。

参考