Kubernetes 是目前最受欢迎的⾃动化容器管理平台,它提供了灵活的声明式容器编排、自动部署、资源调度等功能。Kube-Scheduler 作为 Kubernetes 的核心组件之一,主要负责整个集群资源的调度功能,根据特定的调度算法和策略,将 Pod 调度到最优的工作节点上面去,从而更加合理、充分地利用集群资源。
但是随着 Kubernetes 部署的任务类型越来越多,原生 Kube-Scheduler 已经不能应对多样的调度需求:比如机器学习、深度学习训练任务对于协同调度功能的需求;高性能计算作业,基因计算工作流对于一些动态资源 GPU、网络、存储卷的动态资源绑定需求等。因此自定义 Kubernetes 调度器的需求愈发迫切,本文讨论了扩展 Kubernetes 调度程序的各种方法,然后使用目前最佳的扩展方式 Scheduling Framework,演示如何扩展 Scheduler。
01 自定义调度器的方式
02 Scheduling Framework 解析
如下图所示,调度框架提供了丰富的扩展点,在这幅图中,Filter 相当于之前 Predicate 预选模块, Score 相当于 Priority 优选模块,每一个扩展点模块都提供了接口,我们可以实现扩展点定义的接口来实现自己的调度逻辑,并将实现的插件注册到扩展点。
Scheduling Framework 在执行调度流程时,当运行到扩展点时,会调用我们注册的插件,通过执行自定义插件的策略,满足调度需求。此外,一个插件可以在多个扩展点注册,用以执行更复杂或有状态的任务。
Scheduling Framework 每次调度一个 Pod ,都分为调度周期和绑定周期两部分来执行,调度周期为 Pod 选择一个节点,绑定周期则调用 Apiserver,用选好的 Node,更新 Pod 的 spec.nodeName 字段。调度周期和绑定周期统称为 “Scheduling Context” (调度上下文)。调度周期是串行运行的,同一时间只能有一个 Pod 被调度,是线程安全的;而绑定周期因为需要访问 Apiserver 的接口,耗时较长,为了提高调度的效率,需要异步执行,即同一时间,可执行多个 bind 操作,是非线程安全的。
如果 Pod 被确定为不可调度或远程桌面存在内部错误,那么调度周期或绑定周期将被中止。Pod 将返回队列并等待下一次重试。如果一个绑定周期被终止,它将触发 Reserve 插件中的 UnReserve 方法。
Scheduling Cycle 的扩展点
QueueSort
用于给调度队列排序,默认情况下,所有的 Pod 都会被放到一个队列中,此扩展用于对 Pod 的待调度队列进行排序,以决定先调度哪个 Pod,QueueSort 扩展本质上只需要实现一个方法 Less(Pod1, Pod2) 用于比较两个 Pod 谁更优先获得调度,同一时间点只能有一个 QueueSort 插件生效。
PreFilter
用于对 Pod 的信息进行预处理,或者检查一些集群或 Pod 必须满足的前提条件,比如 Pod 是否包含指定的 annotations 或 labels,如果 PreFilter 返回了 error ,则调度过程终止。
Filter
用于排除那些不能运行该 Pod 的节点,对于每一个节点,调度器将按顺序执行 Filter 扩展,如果任何一个 Filter 将节点标记为不可选,则余下的 Filter 扩展将不会被执行。如果对默认调度器提供的预选规则不满意,可以在配置中禁用默认调度器的预选算法,在这个扩展点只执行自己自定义的过滤逻辑。Node 节点执行 Filter 策略是并发执行的,所以在同一调度周期中多次调用过滤器。
PostFilter
实现此扩展的插件是在 Filter 阶段之后被调用,仅当没有为 Pod 找到可行的节点时才调用。如果有任何 PostFilter 插件将节点标记为可调度节点,则后面的 PostFilter 插件就不会被调用了。一个典型的 PostFilter 实现是抢占,它试图通过抢占其他 Pod 来使 Pod 可调度。
PreScore
在预选后被调用,通常用来在 Score 之前进行一些信息生成或者记录日志和监控信息
Score
实现此扩展的插件为已通过过滤阶段的所有节点进行打分,调度器将针对每一个节点调用 Score 扩展。
NormalizeScore
在 NormalizeScore 阶段,调度器将会把每个 Score 扩展对具体某个节点的评分结果和该扩展的权重合并起来,作为最终评分结果,评分结果是一个范围内的整数。如果 Score 或 NormalizeScore 返回错误,则调度周期将中止。
Reserve
此扩展点为 Pod 预留的在要运行节点上的资源,目的是避免调度器在等待 Pod 与节点绑定的过程中调度新的 Pod 到节点上时,发生实际使用资源超出可用资源的情况。(因为绑定 Pod 到节点上是异步发生的)。这是调度过程的最后一个步骤,Pod 进入 Reserved 状态以后,要么在绑定失败时,触发 Unreserve 扩展,要么在绑定成功时,由 PostBind 扩展结束绑定过程。
Permit
Permit 扩展,发生在 Pod 使用 Reserve 插件预留资源之后, Bind 扩展点 bind 之前,主要有三种策略,批准、拒绝、等待。
1)approve(批准):当所有的 Permit 扩展都批准了 Pod 与节点的绑定,调度器将继续执行绑定过程
2)deny(拒绝):如果任何一个 Permit 扩展 deny 了 Pod 与节点的绑定,Pod 将被放回到待调度队列,此时将触发 Unreserve 扩展
3)wait(等待):如果一个 Permit 扩展返回了 wait,则 Pod 将保持在 Permit 阶段,直到被其他扩展 approve,如果超时事件发生,wait 状态变成 deny,Pod 将被放回到待调度队列,此时将触发 UnReserve 扩展
Binding Cycle 的扩展点
PreBind
扩展用于在 Pod 绑定之前执行某些逻辑。这个插件引入的原因,是有一些资源,是在不在调度 Pod 时,立即确定可用的节点的资源,所以调度程序需要确保,这些资源已经成功绑定到选定的节点后,才能将 Pod 调度到此节点。例如,PreBind 扩展可以将一个基于网络的数据卷挂载到节点上,以Pod 可以使用。如果任何一个 PreBind 扩展返回错误,Pod 将被放回到待调度队列,此时将触发 Unreserve 扩展。
Bind
Bind 扩展会调用 apiserver 提供的接口,将 Pod 绑定到对应的节点上。
PostBind
PostBind 是一个信息扩展点。成功绑定 Pod 后,将调用 PostBind 插件,可用于清理关联的资源。
UnReserve
是一个通知性质的扩展,如果为 Pod 预留了资源,Pod 又在被绑定过程中被拒绝绑定,则 Unreserve 扩展将被调用。Unreserve 扩展应该释放已经为 Pod 预留的节点上的计算资源。在一个插件中,Reserve 扩展和 UnReserve 扩展应该成对出现。
03 使用 Scheduling Framework 自定义 Scheduler
自定义插件需要两个步骤:
1)实现插件的接口
2)注册插件并配置插件
3.1 实现插件的接口
这里我们实现 QueueSort 扩展点,先看看 QueueSort 扩展点定义的接口:
// 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(QueuedPodInfo, QueuedPodInfo) bool}
默认的调度器会优先调度优先级较高的 Pod , 其具体实现的方式是使用 QueueSort 这个插件,默认的实现,是对 Pod 的 Priority 值进行排序,但当优先级相同时,再比较 Pod 的 timestamp , timestamp 是 Pod 加入 queue 的时间。我们现在想先根据 Pod 的 Priority 值进行排序,当 Priority 值相同,再根据 Pod 的 QoS 类型进行排序,最后再根据 Pod 的 timestamp 排序。