为了深入学习 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步:
-
定义这个插件的结构体,在这个结构体上去实现需要实现的方法
-
实现 Name 方法
-
实现 Score 方法,这个方法里面就是 Score 插件的逻辑了,这里我们通过读取传入的 Node 的信息获取节点创建时间,然后和当前时间比较,如果小于15天则给节点加一分,否则不得分。 为了能够直观体现节点得分情况日志,代码中加了日志。
-
编写 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插件得分了的节点。
所以到这里我们的插件已经工作正常了。