kube-scheduler深度剖析与开发(五)

6 篇文章 0 订阅
6 篇文章 0 订阅
本文介绍了如何从源码和实战角度学习kube-scheduler,通过开发一个Filter插件来实现过滤节点创建时长大于15天的Pod。首先克隆kubernetes源码,创建自定义插件目录并编写Go代码,实现Filter和Name方法。当节点创建时间超过15天时,Filter方法返回错误状态阻止Pod调度。然后在scheduler配置中启用这个插件,并通过部署测试验证插件功能。
摘要由CSDN通过智能技术生成

为了深入学习 kube-scheduler,本系列从源码和实战角度深度学习 kube-scheduler,该系列一共分6篇文章,如下:

  • kube-scheduler 整体架构
  • 初始化一个 scheduler
  • 一个 Pod 是如何调度的
  • 如何开发一个属于自己的scheduler插件
  • 开发一个 Filter 扩展点的插件
  • 开发一个 socre 扩展点的插件

在前面的文章中我们已经详细说明了 kube-scheduler 的原理和开发一个自定义插件的流程,本文就通过开发一个 Filter 扩展点的插件对前面的理论进行实践,加深理解。

假设我们有这样一个需求:一个节点创建时长超过15天,Pod 将不再往上面调度。

这个需求很简单,只要在调度器的 Filter 扩展点插入自定义的插件,这个插件过滤掉创建时长超过15天的节点即可,但是也有一些细节需要注意。下面我们开始完成这个需求。

首先,你需要 clone kubernetes 的源码,这里我们下载 v1.20.2 版本的

git clone https://github.com/kubernetes/kubernetes.git -b v1.20.2

然后进入 plugins 目录下创建一个我们自己的插件目录,创建存放插件代码的 go 文件

cd kubernetes/pkg/scheduler/framework/plugins

mkdir nodeexpiration

cd nodeexpiration

touch node_expiration.go

下面我们就在 node_expiration.go 文件下编写我们插件的代码逻辑

我们先看下 Filter 类型的插件需要实现哪些方法

// kubernetes/pkg/scheduler/framework/interface.go

type FilterPlugin interface {
	Plugin
	Filter(ctx context.Context, state *CycleState, pod *v1.Pod, nodeInfo *NodeInfo) *Status
}

type Plugin interface {
	Name() string
}

所以在我们的自定义插件中,只需要实现 Filter、Name 这两个方法就实现了 Filter 类型的插件

Filter 有一个 Status 类型的返回值,定义如下:


pkg/scheduler/framework/interface.go

type Status struct {
	code    Code
	reasons []string
	err     error
	// failedPlugin is an optional field that records the plugin name a Pod failed by.
	// It's set by the framework when code is Error, Unschedulable or UnschedulableAndUnresolvable.
	failedPlugin string
}


func (s *Status) IsSuccess() bool {
	return s.Code() == Success
}

func (s *Status) Code() Code {
	if s == nil {
		return Success
	}
	return s.code
}

我们前面说过 Filter 插件就是要过滤掉那些不符合 Pod 的 Node,留下符合的 Node,Filer 方法就是通过返回值 Status 来标识某个 Node 是否通过了这个插件的过滤,从上面的源码我们看到,只要插件返回 nil 就表示 Node 通过了这个插件的过滤。如果 Node 无法通过这个插件的过滤,插件可以调用 framework.NewStatus 方法返回过滤失败的原因息。

下面我们就来实现下这个插件

package nodeexpiration

import (
	"context"
	"time"

	v1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/kubernetes/pkg/scheduler/framework"
	"k8s.io/klog/v2"
)

// 1. 定义这个插件的结构体
type NodeExpiration struct{

}

// 2. 实现 Name 方法
func (pl *NodeExpiration) Name() string {
	return "nodeExpiration"
}

// 3. 实现 Filter 方法
func (pl *NodeExpiration) Filter(ctx context.Context, _ *framework.CycleState, pod *v1.Pod, nodeInfo *framework.NodeInfo) *framework.Status {
	tsCreation := nodeInfo.Node().ObjectMeta.CreationTimestamp.Second()
	tsCurrent := time.Now().Second()
	if tsCurrent - tsCreation > 15 * 3600 * 24 {
		klog.InfoS("node failed to pass nodeExpiration filter", "pod_name", pod.Name, "current node", nodeInfo.Node().Name)
		return framework.NewStatus(framework.UnschedulableAndUnresolvable, "node expired")
	}
	klog.InfoS("node pass nodeExpiration filter", "pod_name", pod.Name, "current node", nodeInfo.Node().Name)
	return nil
}

// 4. 编写 New 函数
func New(_ runtime.Object, _ framework.Handle) (framework.Plugin, error) {
	return &NodeExpiration{}, nil
}

分4步:

  1. 定义这个插件的结构体,在这个结构体上去实现需要实现的方法

  2. 实现 Name 方法

  3. 实现 Filter 方法,这个方法里面就是 Filter 插件的逻辑了,这里我们通过读取传入的 Node 的信息获取节点创建时间,然后和当前时间比较,如果大于15天,那么返回 framework.Status 类型的值;如果小于15天,返回 nil,表示这个节点在这个插件上过滤成功。
    为了能够直观体现节点确实经过了这个插件的过滤,我在代码中加了日志。

  4. 编写 New 函数,这个函数会初始化的时候被注册在 framework 中,告诉 framework 怎么创建这个插件对象

好了,到了这里我们就开发完了一个简单的 Filter 类型插件。但是这个插件是怎么嵌入到 Filter 扩展点中去的呢,这个插件和内置的 Filter 扩展点插件相比,哪个会先被执行呢?

还需要需要做两件事

  • 在 main 函数中传递我们定义的插件的 New 函数(上一篇文章中有说明具体怎么传递)
  • 在配置文件中配置我们的插件

我们先看第一点

package main

import (
	"math/rand"
	"os"
	"time"

	"github.com/spf13/pflag"

	cliflag "k8s.io/component-base/cli/flag"
	"k8s.io/component-base/logs"
	_ "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"
	"k8s.io/kubernetes/pkg/scheduler/framework/plugins/nodeexpiration"
)

func main() {
	rand.Seed(time.Now().UnixNano())
	myPlugin := app.WithPlugin("nodeExpiration", nodeexpiration.New)
	command := app.NewSchedulerCommand(myPlugin)
	logs.InitLogs()
	defer logs.FlushLogs()

	if err := command.Execute(); err != nil {
		os.Exit(1)
	}
}
}

我们使用了 app.WithPlugin 方法返回一个 Option 类型的对象,Option 类型正好是 NewSchedulerCommand 方法的参数类型,这样就传递了我们自定义的插件对象新建方法。在后续初始化 scheduler 的过程中,这个 New 方法会被调用, New 返回的结果存放在 frameworkImpl 对象的 Filter 扩展点插件数组中,以便后续遍历 Filter 扩展点插件数组时调用插件对象上的 Filter 方法。

再来看看第二点
kube-scheduler 启动命令可以带上配置文件路径,如下

./kube-scheduler --config config.yaml

而我们的插件配置就可以放在这个配置文件里面

apiVersion: kubescheduler.config.k8s.io/v1beta1
kind: KubeSchedulerConfiguration
profiles:
  - schedulerName: my-scheduler
    plugins:
      filter:
        enabled:
        - name: nodeExpiration

我们编译一下

cd cmd/kube-scheduler

CGO_ENABLED=0  GOOS=linux  GOARCH=amd64  go  build 
 

然后把二进制和配置文件都拷贝到k8s集群中去,然后运行

./kube-scheduler --authentication-kubeconfig=/etc/kubernetes/scheduler.conf --authorization-kubeconfig=/etc/kubernetes/scheduler.conf --bind-address=0.0.0.0 --kubeconfig=/etc/kubernetes/scheduler.conf --config=scheduler_config.yaml --secure-port 19999 --leader-elect=false

接下去我们再打开一个终端并创建一个 nginx 的 deployment,并且把 deployment 的 schedulerName 设置为配置文件中指定的my-scheduler,如下:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: nginx
    spec:
      schedulerName: my-scheduler
      containers:
      - name: nginx
        image: nginx:1.15.4
        ports:
        - containerPort: 80

然后我们能够在 scheduler 运行的终端能够打印如下日志

我们发现了终端打印了我们插件中写的日志,部分节点未通过该插件的过滤,部分节点顺利通过过滤。我们再看下 Pod 最终的调度情况

可以看到 Pod 最后调度到了 bms-cyrusone-da-h6v3-app-10-68-23-146 节点,而这个节点正好是通过了我们插件过滤的节点。

所以我们的插件已经工作正常了。

但是这种方式运行不是很优雅,一般我们会把二进制文件打包成 docker 镜像,然后以 deployment 的方式部署 scheduler,配置文件以 configmap 的保存,并且以 volume 的方式将 configmap 挂载到容器内部供 scheduler 使用。

好了,本文就到这里,下一篇我们会开发一个 score 类型的插件,这个插件需要考虑其他一些问题,我们下篇见。


在这里插入图片描述
关注公众号,学习更多k8s知识,二次开发

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值