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

879 阅读7分钟

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

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

上一篇文章我们已经讲了如何开发一个 Filter 扩展点的插件,这一篇我们来说说如何开发一个 score 扩展点的插件。

我们先来看下 Score 类型插件的逻辑是什么。

scheduler 在启动后,会监测调度队列里是否会有需要调度的 Pod,有的话就开始调度这个 Pod,否则一直阻塞

// pkg/scheduler/scheduler.go

func (sched *Scheduler) Run(ctx context.Context) {
	sched.SchedulingQueue.Run()
	go wait.UntilWithContext(ctx, sched.scheduleOne, 0)
	<-ctx.Done()
	sched.SchedulingQueue.Close()
}


// pkg/scheduler/schedule_one.go

func (sched *Scheduler) scheduleOne(ctx context.Context) {
	// 会一直阻塞,直到获取到到一个新的Pod
	// kubernetes/pkg/scheduler/internal/queue/scheduling_queue.go
	fmt.Println( "******here***********")
	podInfo := sched.NextPod()
	
	...
	
	scheduleResult, err := sched.SchedulePod(schedulingCycleCtx, fwk, state, pod)

}

当有 Pod 需要调度时,sched.scheduleOne被调用

// pkg/scheduler/schedule_one.go

func (sched *Scheduler) schedulePod(...){
    ...
    // 执行 preFilter 和 Filter
    feasibleNodes, diagnosis, err := sched.findNodesThatFitPod(ctx, fwk, state, pod)
    
    ...
    
    // 执行 preScore 和 Score,每个节点经过所有 Score 插件后获得得分会累加,最后总得分就是该节点得分
    priorityList, err := prioritizeNodes(ctx, sched.Extenders, fwk, state, pod, feasibleNodes)
    
    // 获取得分最高的节点
    host, err := selectHost(priorityList)
}

// 挑选得分最高的,如果得分最高的有多个,随机选择一个

func selectHost(nodeScoreList framework.NodeScoreList) (string, error) {
	if len(nodeScoreList) == 0 {
		return "", fmt.Errorf("empty priorityList")
	}
	maxScore := nodeScoreList[0].Score
	selected := nodeScoreList[0].Name
	cntOfMaxScore := 1
	for _, ns := range nodeScoreList[1:] {
		if ns.Score > maxScore {
			maxScore = ns.Score
			selected = ns.Name
			cntOfMaxScore = 1
		} else if ns.Score == maxScore {
			cntOfMaxScore++
			if rand.Intn(cntOfMaxScore) == 0 {
				// Replace the candidate with probability of 1/cntOfMaxScore
				selected = ns.Name
			}
		}
	}
	return selected, nil
}

在 Score 类型插件运行时,每个节点经过所有 Score 插件后获得得分会累加,最后总得分就是该节点得分。

我们看下 Score 类型插件的定义

type ScorePlugin interface {
	Plugin
	Score(ctx context.Context, state *CycleState, p *v1.Pod, nodeName string) (int64, *Status)
	ScoreExtensions() ScoreExtensions
}

type Plugin interface {
	Name() string
}

所以只要实现了 Name、Score、ScoreExtensions 这三个方法的类型就是一个 Score 类型的插件。

其中 Score 方法是关键,这个方法会对传入的 Pod 和 Node 计算这个 Node 在这个插件的得分,所以我们开发一个 Score 类型的插件主要就是实现 Score 方法。

Score 方法有两个返回值,第一个是一个 int64 类型的值,表示这个节点在这个插件的得分,如果插件运行失败,返回值为0;第二个值为 Status 类型的值,如果插件运行成功这个返回值为 nil,否则返回非 nil Status 值,这个值里面包含了插件失败原因。

另外我们再说一点,其实,每个插件都会有一个权重(weight)的配置,但是权重这个配置对于像 PreFilter、Filter 等插件没有作用,所以就不会去配置,它对 Score 类型的插件起作用,如果你开发的插件没指定权重,那么 scheduler 框架会默认设置为1,而且如果你把权重设置为0,scheduler 框架也会把它修正到1(如果是0意味着这个插件被关闭不起作用)。

那么这个权重有什么作用呢?

比如在你开发的插件中,满足某个条件可以得一分,如果你加了这个权重,那么可以将得分乘以权重来提升得分(如果权重大于一,默认为1),这将意味着如果节点满足这个插件的条件将得到更多的分。

在开始开发我们的自定义插件之前,先看一个内置 Score 类型插件的例子,看看它是怎么做的。

我们知道,在 k8s 中 Pod 可以对节点设置亲和和反亲和。这里我们说说亲和,亲和又分为:

  • 软性亲:preferredDuringSchedulingIgnoredDuringExecution,意思就是尽量不要将 pod 调度到匹配到的节点,但是如果没有不匹配的节点的话,也可以调度到匹配到的节点
  • 硬性亲:requiredDuringSchedulingIgnoredDuringExecution,意思就是必须调度到满足条件的节点上

硬亲和就是一锤子买卖,成就是成,一票否决,必须得满足条件才行,它是在调度的 Filter 阶段起作用的;而软亲和则是通过加分来完成的,它是在调度的 Score 阶段起作用。下面是软性亲和在 Score 类型插件的实现:

func (pl *NodeAffinity) Score(ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeName string) (int64, *framework.Status) {
	nodeInfo, err := pl.handle.SnapshotSharedLister().NodeInfos().Get(nodeName)
	if err != nil {
		return 0, framework.AsStatus(fmt.Errorf("getting node %q from Snapshot: %w", nodeName, err))
	}

	node := nodeInfo.Node()

	var count int64
	if pl.addedPrefSchedTerms != nil {
		count += pl.addedPrefSchedTerms.Score(node)
	}

	s, err := getPreScoreState(state)
	if err != nil {
		// Fallback to calculate preferredNodeAffinity here when PreScore is disabled.
		preferredNodeAffinity, err := getPodPreferredNodeAffinity(pod)
		if err != nil {
			return 0, framework.AsStatus(err)
		}
		s = &preScoreState{
			preferredNodeAffinity: preferredNodeAffinity,
		}
	}

	if s.preferredNodeAffinity != nil {
		count += s.preferredNodeAffinity.Score(node)
	}

	return count, nil
}

func (t *PreferredSchedulingTerms) Score(node *v1.Node) int64 {
	var score int64
	nodeLabels := labels.Set(node.Labels)
	nodeFields := extractNodeFields(node)
	for _, term := range t.terms {
		// parse errors are reported in NewPreferredSchedulingTerms.
		if ok, _ := term.match(nodeLabels, nodeFields); ok {
			score += int64(term.weight)
		}
	}
	return score
}

可以看到,当 Pod 的软亲和配置和节点标签能够匹配时,就能得分,这个插件的总得分为每个匹配的软性亲和乘以权重的和。

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

我们可以开发一个 Score 的插件,如果节点创建时长超过 15天 则不得分,创建时长低于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 node_age

cd node_age

touch node_age.go

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

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

package node_age

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 NodeAge struct{

}

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

// 3. 实现 Score 方法
func (pl *NodeAge) Score(ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeName string) (int64, *framework.Status) {
	nodeInfo, err := pl.handle.SnapshotSharedLister().NodeInfos().Get(nodeName)
	if err != nil {
		return 0, framework.AsStatus(fmt.Errorf("getting node %q from Snapshot: %w", nodeName, err))
	}
	tsCreation := nodeInfo.Node().ObjectMeta.CreationTimestamp.Second()
	tsCurrent := time.Now().Second()
	score := 0
	if tsCurrent - tsCreation < 15 * 3600 * 24 {
		score += 1
		klog.InfoS("node get score", "pod_name", pod.Name, "current node", nodeInfo.Node().Name, "node age(days)", (tsCurrent-tsCreation)/3600/24)
	} else {
		klog.InfoS("node can't get score", "pod_name", pod.Name, "current node", nodeInfo.Node().Name, "node age(days)", (tsCurrent-tsCreation)/3600/24)
	}
	return int64(score), nil
}


func (pl *NodeAge) ScoreExtensions() framework.ScoreExtensions {
	return pl
}


func (pl *NodeAge) NormalizeScore(ctx context.Context, state *framework.CycleState, pod *v1.Pod, scores framework.NodeScoreList) *framework.Status {
	return helper.DefaultNormalizeScore(framework.MaxNodeScore, false, scores)
}


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

分4步:

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

  2. 实现 Name 方法

  3. 实现 Score 方法,这个方法里面就是 Score 插件的逻辑了,这里我们通过读取传入的 Node 的信息获取节点创建时间,然后和当前时间比较,如果小于15天则给节点加一分,否则不得分。 为了能够直观体现节点得分情况日志,代码中加了日志。

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

接着我们来修改下 main 函数所在文件

package main

import (
	"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"
	"k8s.io/kubernetes/pkg/scheduler/framework/plugins/nodeage"
)

func main() {
	myPlugin := app.WithPlugin("nodeAge", nodeage.New)

	command := app.NewSchedulerCommand(myPlugin)
	code := cli.Run(command)
	os.Exit(code)
}

最后我们创建一个配置文件,把我们开发的插件配置进去

apiVersion: kubescheduler.config.k8s.io/v1beta1
leaderElection:
  leaderElect: true
clientConnection:
  kubeconfig: "/etc/kubernetes/scheduler.conf"
kind: KubeSchedulerConfiguration
profiles:
  - schedulerName: my-scheduler
    plugins:
      score:
        enabled:
        - name: nodeAge

我们编译一下

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 运行的终端能够打印如下日志

我们发现了终端打印了我们插件中写的日志,创建时长为9天的节点得分了,创建时长为28天的节点没有得分。我们再看下 Pod 最终的调度情况

可以看到 Pod 最后调度到了bms-cyrusone-da-h6v3-app-10-68-21-83 节点,而这个节点正好是创建时长为9天,在nodeAge插件得分了的节点。

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