为了深入学习 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.Unix()
tsCurrent := time.Now().Unix()
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步:
-
定义这个插件的结构体,在这个结构体上去实现需要实现的方法
-
实现 Name 方法
-
实现 Filter 方法,这个方法里面就是 Filter 插件的逻辑了,这里我们通过读取传入的 Node 的信息获取节点创建时间,然后和当前时间比较,如果大于15天,那么返回 framework.Status 类型的值;如果小于15天,返回 nil,表示这个节点在这个插件上过滤成功。 为了能够直观体现节点确实经过了这个插件的过滤,我在代码中加了日志。
-
编写 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 类型的插件,这个插件需要考虑其他一些问题,我们下篇见。