调度框架介绍
调度框架是面向 Kubernetes 调度器的一种插件架构, 它为现有的调度器添加了一组新的“插件” API。插件会被编译到调度器之中。 这些 API 允许大多数调度功能以插件的形式实现,同时使调度“核心”保持简单且可维护。
框架工作流程
调度框架定义了一些扩展点。调度器插件注册后在一个或多个扩展点处被调用。 这些插件中的一些可以改变调度决策,而另一些仅用于提供信息。每次调度一个 Pod 的尝试都分为两个阶段,即 调度周期 和 绑定周期。
调度周期和绑定周期
调度周期为 Pod 选择一个节点,绑定周期将该决策应用于集群。 调度周期和绑定周期一起被称为“调度上下文”。调度周期是串行运行的,而绑定周期可能是同时运行的。如果确定 Pod 不可调度或者存在内部错误,则可以终止调度周期或绑定周期。 Pod 将返回队列并重试。
扩展点
以下为可以可扩展的11个扩展点,需要在哪个阶段自定义一些操作,就根据自己的需求实现哪个阶段的扩展点即可
源码在[email protected]/pkg/scheduler/framework/interface.go
//插件的名字,必须实现此接口
type Plugin interface {
Name() string
}
1. 队列排序 QueueSort
//队列排序插件用于对调度队列中的 Pod 进行排序。 队列排序插件本质上提供 less(Pod1, Pod2) 函数。 一次只能启动一个队列插件。
type QueueSortPlugin interface {
Plugin
Less(*QueuedPodInfo, *QueuedPodInfo) bool
}
//比较pod的优先级
type LessFunc func(podInfo1, podInfo2 *QueuedPodInfo) bool
2. 前置过滤 PreFilter
//前置过滤插件用于预处理 Pod 的相关信息,或者检查集群或 Pod 必须满足的某些条件。 如果 PreFilter 插件返回错误,则调度周期将终止。
type PreFilterPlugin interface {
Plugin
PreFilter(ctx context.Context, state *CycleState, p *v1.Pod) *Status
PreFilterExtensions() PreFilterExtensions
}
type EnqueueExtensions interface {
EventsToRegister() []ClusterEvent
}
type PreFilterExtensions interface {
AddPod(ctx context.Context, state *CycleState, podToSchedule *v1.Pod, podInfoToAdd *PodInfo, nodeInfo *NodeInfo) *Status
RemovePod(ctx context.Context, state *CycleState, podToSchedule *v1.Pod, podInfoToRemove *PodInfo, nodeInfo *NodeInfo) *Status
}
3. 过滤 Filter
//过滤插件用于过滤出不能运行该 Pod 的节点。对于每个节点, 调度器将按照其配置顺序调用这些过滤插件。如果任何过滤插件将节点标记为不可行, 则不会为该节点调用剩下的过滤插件。节点可以被同时进行评估。
type FilterPlugin interface {
Plugin
Filter(ctx context.Context, state *CycleState, pod *v1.Pod, nodeInfo *NodeInfo) *Status
}
4. 后置过滤
//这些插件在筛选阶段后调用,但仅在该 Pod 没有可行的节点时调用。 插件按其配置的顺序调用。
//如果任何后过滤器插件标记节点为“可调度”, 则其余的插件不会调用。典型的后筛选实现是抢占,
//试图通过抢占其他 Pod 的 资源使该 Pod 可以调度。
type PostFilterPlugin interface {
Plugin
PostFilter(ctx context.Context, state *CycleState, pod *v1.Pod, filteredNodeStatusMap NodeToStatusMap) (*PostFilterResult, *Status)
}
5. 前置评分
//前置评分插件用于执行 “前置评分” 工作,即生成一个可共享状态供评分插件使用。 如果 PreScore 插件返回错误,则调度周期将终止。
type PreScorePlugin interface {
Plugin
PreScore(ctx context.Context, state *CycleState, pod *v1.Pod, nodes []*v1.Node) *Status
}
6. 评分
//评分插件用于对通过过滤阶段的节点进行排名。调度器将为每个节点调用每个评分插件。
//将有一个定义明确的整数范围,代表最小和最大分数。
//在标准化评分阶段之后,调度器将根据配置的插件权重 合并所有插件的节点分数。
type ScorePlugin interface {
Plugin
Score(ctx context.Context, state *CycleState, p *v1.Pod, nodeName string) (int64, *Status)
ScoreExtensions() ScoreExtensions
}
type ScoreExtensions interface {
NormalizeScore(ctx context.Context, state *CycleState, p *v1.Pod, scores NodeScoreList) *Status
}
7. Reserve
//这是一个信息扩展点,当资源已经预留给 Pod 时,会通知插件。 管理运行时状态的插件(也成为“有状态插件”)应该使用此扩展点,以便 调度器在节点给指定 Pod 预留了资源时能够通知该插件。 这是在调度器真正将 Pod 绑定到节点之前发生的,并且它存在是为了防止 在调度器等待绑定成功时发生竞争情况。
//这个是调度周期的最后一步。 一旦 Pod 处于保留状态,它将在绑定周期结束时触发不保留 插件 (失败时)或 绑定后 插件(成功时)。
type ReservePlugin interface {
Plugin
Reserve(ctx context.Context, state *CycleState, p *v1.Pod, nodeName string) *Status
Unreserve(ctx context.Context, state *CycleState, p *v1.Pod, nodeName string)
}
8. PreBind
//这些插件在 Pod 绑定节点之前执行
type PreBindPlugin interface {
Plugin
PreBind(ctx context.Context, state *CycleState, p *v1.Pod, nodeName string) *Status
}
9. PostBind
//这是一个信息扩展点,在 Pod 绑定了节点之后调用。
type PostBindPlugin interface {
Plugin
PostBind(ctx context.Context, state *CycleState, p *v1.Pod, nodeName string)
}
10. Permit
// 这些插件可以阻止或延迟 Pod 绑定, 一个允许插件可以做以下三件事之一:
//批准:一旦所有 Permit 插件批准 Pod 后,该 Pod 将被发送以进行绑定。
//拒绝:如果任何 Permit 插件拒绝 Pod,则该 Pod 将被返回到调度队列。 这将触发Unreserve 插件。
//等待(带有超时):如果一个 Permit 插件返回 “等待” 结果,则 Pod 将保持在一个内部的 “等待中” 的 //Pod 列表,同时该 Pod 的绑定周期启动时即直接阻塞直到得到 批准。如果超时发生,等待 变成 拒绝,并且 Pod 将返回调度队列,从而触发 Unreserve 插件。
type PermitPlugin interface {
Plugin
Permit(ctx context.Context, state *CycleState, p *v1.Pod, nodeName string) (*Status, time.Duration)
}
11. Bind
// 这个插件将 Pod 与节点绑定。绑定插件是按顺序调用的,只要有一个插件完成了绑定,其余插件都会跳过。绑定插件至少需要一个。
type BindPlugin interface {
Plugin
Bind(ctx context.Context, state *CycleState, p *v1.Pod, nodeName string) *Status
}
官方有给出demo
https://github.com/kubernetes-sigs/scheduler-plugins
仿照官方的demo实现一个小功能,比如在调度的时候需要判断将pod调度到label为gpu=true的节点
Demo
注册
package main
import (
"k8s.io/kubernetes/cmd/kube-scheduler/app"
"math/rand"
"os"
my_scheduler "sche/my-scheduler"
"time"
)
func main() {
rand.Seed(time.Now().UnixNano())
// 注册,可以注册多个
command := app.NewSchedulerCommand(
app.WithPlugin(my_scheduler.Name, my_scheduler.New),
// app.WithPlugin(my_scheduler2.Name, my_scheduler2.New),
)
if err := command.Execute(); err != nil {
os.Exit(1)
}
}
实现
package my_scheduler
import (
"context"
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/kubernetes/pkg/scheduler/framework"
"log"
)
const Name = "sample"
type sample struct{}
// ide(goland)可以自动识别要实现的接口长啥样,implement missing method会一键创建对应方法
var _ framework.FilterPlugin = &sample{}
var _ framework.PreScorePlugin = &sample{}
/**
根据k8s版本,此处可能有些许不同
经测试在1.19版本需要如此导入framework包
framework "k8s.io/kubernetes/pkg/scheduler/framework/v1alpha1"
New函数为
func New(_ *runtime.Object, _ framework.FrameworkHandle) (framework.Plugin, error) {
return &Sample{}, nil
}
*/
func New(_ runtime.Object, _ framework.Handle) (framework.Plugin, error) {
return &sample{}, nil
}
// 必须实现
func (pl *sample) Name() string {
return Name
}
func (pl *sample) Filter(ctx context.Context, state *framework.CycleState, pod *v1.Pod, node *framework.NodeInfo) *framework.Status {
log.Printf("filter pod: %v, node: %v", pod.Name, node)
// 如果节点的label不包含gpu=true,返回不可调度
if node.Node().Labels["gpu"] != "true" {
return framework.NewStatus(framework.Unschedulable, "Node: "+node.Node().Name)
}
return framework.NewStatus(framework.Success, "Node: "+node.Node().Name)
}
func (pl *sample)PreScore(ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodes []*v1.Node) *framework.Status {
// 没做具体实现,此处只为演示,只做打印
log.Println(nodes)
log.Println(pod)
return framework.NewStatus(framework.Success, "Pod: "+pod.Name)
}
打包镜像
FROM ubuntu:18.04
ADD sche /usr/bin/
RUN chmod a+x /usr/bin/sche
CMD ["sche"]
问题
k8s下载依赖包的一些问题
直接go mod init xx
然后go mod tidy
会出现unknown revision v0.0.0问题,可用以下脚本解决
#!/bin/sh
set -euo pipefail
VERSION=${1#"v"}
if [ -z "$VERSION" ]; then
echo "Must specify version!"
exit 1
fi
MODS=($(
curl -sS https://raw.githubusercontent.com/kubernetes/kubernetes/v${VERSION}/go.mod |
sed -n 's|.*k8s.io/\(.*\) => ./staging/src/k8s.io/.*|k8s.io/\1|p'
))
for MOD in "${MODS[@]}"; do
V=$(
go mod download -json "${MOD}@kubernetes-${VERSION}" |
sed -n 's|.*"Version": "\(.*\)".*|\1|p'
)
go mod edit "-replace=${MOD}=${MOD}@${V}"
done
go get "k8s.io/[email protected]${VERSION}"
使用方法为:
go mod init xxx
bash mod.sh v1.19.2 // 假设上面的脚本保存为mod.sh 需要的k8s版本为1.19.2
插件配置
配置多个调度器 | Kubernetes
调度器配置 | Kubernetes
集群开启了RBAC,所以需要给权限,然后通过configmap的方式将配置文件传给自定义的scheduler,使用一个deployment将自定义的scheduler运行起来
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: sample-scheduler-clusterrole
rules:
- apiGroups:
- coordination.k8s.io
resources:
- leases
verbs:
- get
- update
- create
- apiGroups:
- ""
resources:
- endpoints
verbs:
- delete
- get
- patch
- update
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: sample-scheduler-sa
namespace: kube-system
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: sample-scheduler-clusterrolebinding
namespace: kube-system
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: sample-scheduler-clusterrole
subjects:
- kind: ServiceAccount
name: sample-scheduler-sa
namespace: kube-system
---
apiVersion: v1
kind: ConfigMap
metadata:
name: scheduler-config
namespace: kube-system
data:
scheduler-config.yaml: |
apiVersion: kubescheduler.config.k8s.io/v1beta1
kind: KubeSchedulerConfiguration
leaderElection:
leaderElect: false
clientConnection:
kubeconfig: "/etc/kubernetes/ssl/kubecfg-kube-scheduler.yaml"
profiles:
- schedulerName: sample-scheduler # 新的调度器的名字,随便取
plugins:
filter: # 表示在filter这个扩展点
enabled: # 开启
- name: sample # sample这个插件定义的filter,同时还会执行默认的插件的filter
preScore:
enabled:
- name: sample
disabled: #禁用
- name: "*" # 表示禁用默认的preScore,只使用sample的preScore,除了默认的调度器,其他扩展的调度器必须显示enable才会执行,默认的调度器必须显示disabled才会不执行
# 注意:所有配置文件必须在 QueueSort 扩展点使用相同的插件,并具有相同的配置参数(如果适用)。 这是因为调度器只有一个保存 pending 状态 Pod 的队列。意思要么用默认的queueSort不用扩展的queueSort,要么用扩展的queueSort,把默认的queueSort禁用掉
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: sample-scheduler
namespace: kube-system
labels:
component: sample-scheduler
spec:
replicas: 1
selector:
matchLabels:
component: sample-scheduler
template:
metadata:
labels:
component: sample-scheduler
spec:
hostNetwork: true
serviceAccount: sample-scheduler-sa
priorityClassName: system-cluster-critical
volumes:
- name: scheduler-config
configMap:
name: scheduler-config
- name: kubeconfigdir
hostPath:
path: /etc/kubernetes/ssl # 本地的scheduler-config文件所在位置,在我的环境中该文件名为kubecfg-kube-scheduler.yaml,挂载到deployment内供deployment用到的configmap使用
type: DirectoryOrCreate
containers:
- name: scheduler-ctrl
image: sche:v1
imagePullPolicy: IfNotPresent
args:
- /usr/bin/sche
- --config=/etc/kubernetes/scheduler-config.yaml # configmap挂载的文件
- --v=3
resources:
requests:
cpu: "50m"
volumeMounts:
- name: scheduler-config
mountPath: /etc/kubernetes
- name: kubeconfigdir
mountPath: /etc/kubernetes/ssl
readOnly: true
测试
apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment-example
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
schedulerName: sample-scheduler # 使用自定义的scheduler,如果不指定则会使用默认的scheduler
containers:
- name: nginx
image: nginx
调度时可使用kubectl logs查看sample-scheduler的日志
在未给节点打上gpu=true时,pod一直处于pending状态,当打上标签之后pod成功调度
未完成事项
官方文档中有以下一段:
要在启用了 leader 选举的情况下运行多调度器,你必须执行以下操作:
首先,更新上述 Deployment YAML(my-scheduler.yaml)文件中的以下字段:
- --leader-elect=true
- --lock-object-namespace=<lock-object-namespace>
- --lock-object-name=<lock-object-name>
配置之后pod无法调度,日志中立即打印Add event for unscheduled pod,只有在scheduler-config.yaml中
leaderElection:
leaderElect: false
设置为false的时候能成功调度