kubernetes 新版调度器框架

新版本调度器框架用来解决的问题

  1. 当前版本的调度器,越来越多的特性被加入到调度器,让调度器的代码越来越多,逻辑越来越复杂,不容易维护,不容易发现问题
  2. 当前版本的调度器extender机制扩展点有限,对于”Filter”类型的扩展,只能在默认的预选函数之后执行,”Preempt”类型的扩展只能在默认的抢占函数之后调用,只能有一个Binding 的扩展,而且这个Bind 扩展会代替默认的扩展功能,extender扩展机制不能在任意点调用,例如无法在默认的 预选函数之前进行调用
  3. 每次调用Extender时,都需要对json数据进行解析,调用http 服务的速度慢与直接本地的api调用
  4. 没有默认调度器与extender的交互机制,例如当extender 扩展出现异常时,无法通知默认的调度器
  5. extender的调度器是以一个单独的进程运行,不能共享默认调度器的cache,需要extender 构建自己的cache

    新版本调度器框架机制

    在调度一个Pod时,分为调度部分和绑定部分,其中调度部分用于为Pod选择一个节点,绑定部分将调度部分的决策作用于集群,一个调度Cycle和一个绑定Cycle 被称为一个调度上下文,其中多个调度Cycle是串行运行,绑定Cycle 是并行运行,对于一个Pod,当出现内部错误时或者该Pod无法被调度时,调度Cycle 和绑定Cycle就会终止,这个Pod 被重新放到调度队列

插件扩展点:

avatar

  1. Queue Sort

    在这个插件点的插件 用于对调度队列中的Pod进行排序

  2. PreFilter

    在这个插件点的插件,用于对Pod进行预处理、检查集群、Pod,当该插件出现错误是,该调度会结束,PreFilter在调度周期内只会调用一次

  3. Filter

    过滤出不能运行Pod的节点,对于每一个Node,调度器都会按照顺序调用filter插件,都某个一个Filter插件判断Node不符合要求时,不会再对该节点调用其他插件。多个Node会并行进行评估,在一个调度周期内,Filter可能会被调用多次

  4. PostFilter

    在Filter插件点后进行调用,只有当没有为Pod找到Node时,才会调用该PostFilter节点的插件,当其中一个PostFilter节点为Pod找到了一个节点,其他的PostFilter插件不会再调用

  5. PreScore

    用于更新内部状态、产生日志、监控信息等

  6. Scoring

    和原调度器的打分机制相同,包含打分和归一化两部分

  7. Reserve

    包含Reserve和Unreserve两部分,用于通知为Pod在Node上预留节点或者不为Pod预留资源,该阶段会在Bind之前进行调用,这个地方在做gang scheduling 的时候比较有用

  8. Permit

    用于限制或者推迟Pod的绑定,有三种结果,Approve:当所有的Permit 插件都是Approve时,这个Pod将会被Bind。 Deny:如果任意一个Permit插件Deny,则这个Pod就会重新被放入到调度队列。Wait:如果一个Permit插件提示Wait,则需要在timeout时间内等待其他的Permit插件提示Approve,如果timeout时间内没有等到Approve,则该Pod会被重新放入到调度队列

  9. PreBind

    做一些Pod bind之前的准备工作,例如创建网络存储的卷,将卷挂载到主机,如果任意一个PreBind 插件报错,该Pod就会重新被放到调度队列中,重新进行调度

  10. Bind

    将Pod 绑定到Node,可能会存在多个Bind plugin,不同的Bind Plugin 可能会选择不同的Pod进行Bind

  11. PostBind

    这里主要用于信息扩展,当一个Pod被成功Bind到一个节点时,意味着一个调度周期结束,可以用于清理一些相关的资源

插件生命周期

  1. 初始化
    需要两步完成插件的初始化,首先,插件需要被注册,其次调度器会根据配置选择初始化的插件,如果一个插件在多个扩展点注册,该插件只会被初始化一次
  1. 并发

    在编写插件时,需要考虑两种类型的插件并发,当在并发评估每个节点时,一个插件可能会被调用多次,在多个调度的上下文,一个插件也可能能被并发调用,但是在一个调度的上下文内,插件是被顺序调用。

    如下图所示,调度器的Main thread。一次只处理一个调度周期,任意的在Permit扩展点之上的扩展点,包括Permit扩展点,会在下一个调度周期开始之前全部结束,Permit 之后,Bind Thread会并发执行,并发执行,就是扩展插件可能会被两个不同的上下文执行,其中Reserver插件的Unreserve函数可能会在main thread或者 bind thread中被调用

avatar

使用方式

  1. 定义插件的对象和构造函数
1
2
3
4
5
6
// QoSSort is a plugin that implements QoS class based sorting.
type Sort struct{}
// New initializes a new plugin and returns it.
func New(_ *runtime.Unknown, _ framework.FrameworkHandle) (framework.Plugin, error) {
return &Sort{}, nil
}
  1. 根据插件对应的插件点,实现对应的接口函数
1
2
3
4
5
6
7
8
// QueueSortPlugin is an interface that must be implemented by "QueueSort" plugins.
// These plugins are used to sort pods in the scheduling queue. Only one queue sort
// plugin may be enabled at a time.
type QueueSortPlugin interface {
Plugin
// Less are used to sort pods in the scheduling queue.
Less(*PodInfo, *PodInfo) bool
}
  1. 插件注册

在调度器的启动的main 函数添加自定义的调度插件

1
2
3
4
5
6
7
8
9
10
// cmd/main.go
func main() {
rand.Seed(time.Now().UnixNano())
command := app.NewSchedulerCommand(
app.WithPlugin(qos.Name, qos.New),
)
if err := command.Execute(); err != nil {
os.Exit(1)
}
}
  1. 编译启动
1
$ bin/kube-scheduler --kubeconfig=scheduler.conf --config=./manifests/qos/scheduler-config.yaml

Use Case

Coscheduling 批调度

Coscheduling 和之前理解的gang scheduling 有些区别,引用这里的一些描述

Wikipedia对 Coscheduling的定义是“在并发系统中将多个相关联的进程调度到不同处理器上同时运行的策略”。在Coscheduling的场景中,最主要的原则是保证所有相关联的进程能够同时启动。防止部分进程的异常,导致整个关联进程组的阻塞。这种导致阻塞的部分异常进程,称之为“碎片(fragement)”。
在Coscheduling的具体实现过程中,根据是否允许“碎片”存在,可以细分为Explicit Coscheduling,Local Coscheduling和Implicit Coscheduling。 其中Explicit Coscheduling就是大家常听到的Gang Scheduling。Gang Scheduling要求完全不允许有“碎片”存在, 也就是“All or Nothing”。维基百科的具体解释

Explicit coscheduling requires all processing to actually take place at the same time, and is typically implemented by global scheduling across all processors. A specific algorithm is known as gang scheduling.
Local coscheduling allows individual processors to schedule the processing independently.
Dynamic (or implicit) coscheduling is a form of coscheduling where individual processors can still schedule processing independently, but they make scheduling decisions in cooperation with other processors.

我们将上述定义的概念对应到Kubernetes中,就可以理解Kubernetes调度系统支持批任务Coscheduling的含义了。 一个批任务(关联进程组)包括了N个Pod(进程),Kubernetes调度器负责将这N个Pod调度到M个节点(处理器)上同时运行。如果这个批任务需要部分Pod同时启动即可运行,我们称需启动Pod的最小数量为min-available。特别地,当min-available=N时,批任务要求满足Gang Scheduling。

这里基于Kubernetes新版本的调度框架开发的Coscheduling 借鉴了kube-batch调度器的思想,也有PodGroup的概念,只是把kube-batch的一些功能拆为了多个调度器插件
项目地址

avatar

用到的几个插件扩展点

QueueSort插件:这里主要是将调度队列中 属于同一Podgroup到的Pod 放在一起,并安装PodGroup的创建时间/优先级,对Pod进行排序,来决定Pod进入调度队列的先后顺序。

Prefilter插件:判断该PodGroup中的已经运行的Pod是否已经满足最小的Pod 运行数量,如果已经满足,则不再对该PodGroup中Pennding的Pod进行调度

Permit插件:该插件主要是延迟绑定,即Pod进入到Permit阶段时,用户可以自定义条件来允许Pod通过、拒绝Pod通过以及让Pod等待状态(可设置超时时间)。Permit的延迟绑定的功能,刚好可以让属于同一个PodGruop的Pod调度到这个节点时,进行等待,等待积累的Pod数目满足足够的数目时,再统一运行同一个PodGruop的所有Pod进行绑定并创建。

UnReserve插件:当某个Pod Permit 的时间超时,则会进入UnReserve阶段,则会将该Pod所在的PodGroup中所有的Pod都UnReserve,重新进入Pennding队列

Dynamic Resource Binding

NA

Custom Scheduler Plugins

编写自定义调度插件时,不需要fork k8s 原生的调度代码,只需要要引入这些代码就可以,如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
import (
scheduler "k8s.io/kubernetes/cmd/kube-scheduler/app"
)

func main() {
command := scheduler.NewSchedulerCommand(
scheduler.WithPlugin("example-plugin1", ExamplePlugin1),
scheduler.WithPlugin("example-plugin2", ExamplePlugin2))
if err := command.Execute(); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
}