K8s调度框架引入PreEnqueue设计
提案阶段|评审
在Kubernetes调度器框架中提供一个PreEnqueue 钩子,使插件能够在将Pod添加到调度器的内部活动队列之前运行自定义逻辑。如果该插件返回false,则调度器不会将该Pod入队。
需求说明
当前Kubernetes调度器无条件地将待调度的Pod(即spec.nodeName为空)添加到调度队列中。在Pod入队前,插件无法得知,同样也不能决定Pod是否应该入队。
PreEnqueue钩子的缺失将导致工作负载的生命周期管理的不完善,并且也会因无需调度的Pod扰动调度器的内部队列。例如,一些Pod在创建时可能还没有准备好立即被调度,控制器可能有定制的逻辑来决策Pod的Ready时机,并更新它们。因此,让 unready的Pod入队是不可取的,其浪费了宝贵的调度时间。另外,如果一个 unready的Pod被过早的调度,并在事后被抢占,这将浪费集群资源。此外,PreEnqueue钩子可以作为一种信息传输的途径,使插件能够实现更为精准的QueueSort计算,以及更高级的调度举措。
注意:这里unready Pod是指没有准备好立即被调度的Pod
使用场景
Spot instance:只有在集群有富余的可用资源或集群当前利用率较低时,Spot instance pod才会被调度。
有状态的工作负载:使用PersistentVolume的pod先不入队,直到PVC已经成功绑定到PV(在PVC即时绑定模式下)。
无效的secrets/configmaps:pod中指定的secrets/configmaps不存在或无效时不入队。目前,此类pod将被调度,可能抢占其他pod,但在容器启动时因此而失败。
目标提出一个扩展方式,以针对即将入队的Pod执行自定义逻辑。非目标管理未被插件PreEnqueue处理的已调度Pod。
用户画像
作为一个集群容量规划者,想控制Pod的入队速度。例如,具体的逻辑可能取决于一些SLO相关的指标,如调度队列的饱和度,集群的整体利用率,或特定的业务需求。
作为一个插件的开发者,想在Pod入队(进入activeQ)时得到简单的通知,这样就可以在之后的其他插件中利用自定义逻辑。
方案设计
API设计
核心逻辑是为插件开发者提供一个无状态且不可变的PreEnqueue钩子,它被注册在内部调度器activeQ中,在指定的Pod入队之前被调用。
实现方式 1. 添加一个新的插件EnqueuePlugin
在该设计中,引入了一个新的插件,叫做EnqueuePlugin。该插件中Admit()方法可以根据定制的配置文件,以判定一个pod准入/拒入activeQ。
注意:如非目标部分所述,如果Admin()返回错误,则需要由插件开发者来实现重新入队的逻辑。例如,更新Pod的spec/annotation,以便调度器的Pod处理程序会自动触发入队。
type EnqueuePlugin interface {
Plugin
// Admit is invoked every time a Pod is about to be added to activeQ.
// The given Pod won't be enqueued if a `false` value is returned.
Admit(*QueuedPodInfo) bool
}
优点:自定义入队逻辑通过新的扩展点被隔离出来,从而与其他插件实现隔离。对那些不关心此功能的调度器插件没有任何影响。
缺点:对API(KubeSchedulerConfiguration)和代码有较大改动。
实现方式2. 在EnqueueExtensions接口中添加Admit()
在这个方式中,Admit()并不会与一个新的插件产生关联。相反,它拓展了现有的EnqueueExtensions接口。
type EnqueueExtensions interface {
// EventsToRegister returns a series of possible events that may cause a Pod
// failed by this plugin schedulable.
// The events will be registered when instantiating the internal scheduling queue,
// and leveraged to build event handlers dynamically.
// Note: the returned list needs to be static (not depend on configuration parameters);
// otherwise it would lead to undefined behavior.
EventsToRegister() []ClusterEvent
// Admit is invoked every time a Pod is about to be added to activeQ.
// The given Pod won't be enqueued if a `false` value is returned.
Admit(*QueuedPodInfo) bool
}
优点:API上没有变化。(EnqueueExtensions更像是一个SDK,由插件开发者使用,而不是SRE/Devops等最终用户使用)
缺点:EnqueueExtensions是一个可选的接口,它需要成为具体插件的一部分。这意味着如果只想实现Admit()方法,仍然需要实现一个(无实际意义)的插件来与之关联。当前实现EnqueueExtension接口的插件需要被修改以实现Admit(),当然可以通过植入一个 AlwaysAdmit函数以复用。
实现
无论选择哪种方式,Admit()都可以在NewFramework()处构建SchedulerProfile后获得,最终传递给internalqueue.NewSchedulingQueue。
将引入一个新的结构体,叫做activeQ,其中包含heap.Heap和一个map,其映射关系为{profileName: admitFn}。
type activeQ struct {
heap.Heap
// Keyed with profile name, valued with AdminFunc
admitFuncMap map[string]framework.AdminFunc
}
func (aq *activeQ) Add(qInfo *framework.QueuedPodInfo) error {
if fn, ok := aq.admitFuncMap[qInfo.Pod.Spec.SchedulerName]; !ok || !fn(qInfo) {
return nil
}
return aq.Heap.Add(qInfo)
}
有了这个结构体,能够确保原先调用activeQ.XYZ()逻辑保持不变。
由于笔者时间、视野、认知有限,本文难免出现错误、疏漏等问题,期待各位读者朋友、业界专家指正交流。