作者:王庆璨 张凯
前言
Kubernetes 曾经成为目前事实标准上的容器集群治理平台。它为容器化利用提供了自动化部署、运维、资源调度等全生命周期治理性能。通过 3 年多的疾速倒退,Kubernetes 在稳定性、扩展性和规模化方面都有了长足进步。尤其是 Kubernetes 管制立体的外围组件日臻成熟。而作为决定容器是否在集群中运行的调度器 Kube-scheduler,更是因为长久以来体现稳固,且已能满足大部分 Pod 调度场景,逐步不被开发人员特地关注。
随同着 Kubernetes 在私有云以及企业外部 IT 零碎中广泛应用,越来越多的开发人员尝试应用 Kubernetes 运行和治理 Web 利用和微服务以外的工作负载。典型场景包含机器学习和深度学习训练任务,高性能计算作业,基因计算工作流,甚至是传统的大数据处理工作。此外,Kubernetes 集群所治理的资源类型也更加丰盛,不仅有 GPU,TPU 和 FPGA,RDMA 高性能网络,还有针对畛域工作的各种定制加速器,比方各种 AI 芯片,NPU,视频编解码器等。开发人员心愿在 Kubernetes 集群中能像应用 CPU 和内存那样简略地申明式应用各种异构设施。
总的来说,围绕 Kubernetes 构建一个容器服务平台,对立治理各种新算力资源,弹性运行多种类型利用,最终把服务按需交付到各种运行环境(包含公共云、数据中心、边缘节点,甚至是终端设备),未然成为云原生技术的发展趋势。
阿里云容器服务团队联合多年 Kubernetes 产品化与客户反对教训, 对 Kube-scheduler 进行了大量扩大和改良,逐渐使其在多种场景下仍然能稳固、高效地调度简单工作负载类型。
《进击的 Kubernetes 调度零碎》系列文章将把咱们的教训、技术思考和实现细节全面地展示给 Kubernetes 用户和开发者,冀望帮忙大家更好地理解 Kubernetes 调度零碎的弱小能力和将来倒退方向。
晚期计划
首先,让咱们来理解一下 Kubernetes 社区都有过哪些晋升调度器扩大能力的计划。
要对立治理和调度异构资源与更多简单工作负载类型,首先面对挑战的就是 Kube-scheduler。在 Kubernetes 社区里对于晋升调度器扩大能力的探讨始终一直。sig-scheduling 给出的判断是,越多功能退出,使得调度器代码量宏大,逻辑简单,导致保护的难度越来越大,很多 bug 难以发现、解决。而对于应用了自定义调度的用户来说,跟上每一次调度器性能更新,都充斥挑战。
在阿里云,咱们的用户遇到了同样的挑战。Kubernetes 原生调度器循环解决单个 Pod 容器的固定逻辑,无奈及时、简略地反对用户在不同场景的需要。所以针对特定的场景,咱们会基于原生 Kube-scheduler 扩大本人的调度策略。
最后对于 Kube-scheduler 进行扩大的形式次要有两种,一种是调度器扩大(Scheduler Extender),另外一种是多调度器(Multiple schedulers)。接下来咱们对这两种形式别离进行介绍和比照。
Scheduler Extender
社区最后提供的计划是通过 Extender 的模式来扩大 scheduler。Extender 是内部服务,反对 Filter、Preempt、Prioritize 和 Bind 的扩大,scheduler 运行到相应阶段时,通过调用 Extender 注册的 webhook 来运行扩大的逻辑,影响调度流程中各阶段的决策后果。
以 Filter 阶段举例,执行过程会通过 2 个阶段:
- scheduler 会先执行内置的 Filter 策略,如果执行失败的话,会间接标识 Pod 调度失败。
- 如何内置的 Filter 策略执行胜利的话,scheduler 通过 Http 调用 Extender 注册的 webhook, 将调度所须要的 Pod 和 Node 的信息发送到到 Extender,依据返回 filter 后果,作为最终后果。
咱们能够发现 Extender 存在以下问题:
- 调用 Extender 的接口是 HTTP 申请,受到网络环境的影响,性能远低于本地的函数调用。同时每次调用都须要将 Pod 和 Node 的信息进行 marshaling 和 unmarshalling 的操作,会进一步升高性能。
- 用户能够扩大的点比拟无限,地位比拟固定,无奈反对灵便的扩大,例如只能在执行完默认的 Filter 策略后能力调用。
基于以上介绍,Extender 的形式在集群规模较小,调度效率要求不高的状况下,是一个灵便可用的扩大计划,然而在失常生产环境的大型集群中,Extender 无奈反对高吞吐量,性能较差。
Multiple schedulers
Scheduler 在 Kubernetes 集群中其实相似于一个非凡的 Controller,通过监听 Pod 和 Node 的信息,给 Pod 筛选最佳的节点,更新 Pod 的 spec.NodeName 的信息来将调度后果同步到节点。所以对于局部有非凡的调度需要的用户,有些开发者通过自研 Custom Scheduler 来实现以上的流程,而后通过和 default scheduler 同时部署的形式,来反对本人非凡的调度需要。
Custom Scheduler 会存在一下问题:
- 如果与 default scheduler 同时部署,因为每个调度器所看到的资源视图都是全局的,所以在调度决策中可能会在同一时刻在同一个节点资源上调度不同的 Pod,导致节点资源抵触的问题。
- 有些用户将调度器所能调度的资源通过 Label 划分不同的池子,能够防止资源抵触的景象呈现。然而这样又会导致整体集群资源利用率的降落。
- 有些用户抉择通过齐全自研的形式来替换 default scheduler,这种会带来比拟高的研发老本,以及 Kubernetes 版本升级后可能存在的兼容性问题。
Scheduler Extender 的性能较差可是保护老本较小,Custom Scheduler 的研发和保护的老本特地高然而性能较好,这种状况是开发者面临这种两难处境。这时候 Kubernetes Scheduling Framework V2 横空出世,给咱们带来鱼和熊掌能够兼得的计划。
新一代调度框架 Scheduling Framework 之解析
社区也逐步的发现开发者所面临的窘境,为了解决如上问题,使 Kube-scheduler 扩展性更好、代码更简洁,社区从 Kubernetes 1.16 版本开始, 构建了一种新的调度框架 Kubernetes Scheduling Framework 的机制。
Scheduling Framework 在原有的调度流程中, 定义了丰盛扩大点接口,开发者能够通过实现扩大点所定义的接口来实现插件,将插件注册到扩大点。Scheduling Framework 在执行调度流程时,运行到相应的扩大点时,会调用用户注册的插件,影响调度决策的后果。通过这种形式来将用户的调度逻辑集成到 Scheduling Framework 中。
Framework 的调度流程是分为两个阶段 scheduling cycle 和 binding cycle. scheduling cycle 是同步执行的,同一个工夫只有一个 scheduling cycle,是线程平安的。binding cycle 是异步执行的,同一个工夫中可能会有多个 binding cycle 在运行,是线程不平安的。
scheduling cycle
scheduling cycle 是调度的外围流程,次要的工作是进行调度决策,挑选出惟一的节点。
Queue sort
// 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
}
Scheduler 中的优先级队列是通过 heap 实现的,咱们能够在 QueueSortPlugin 中定义 heap 的比拟函数来决定的排序构造。然而须要留神的是 heap 的比拟函数在同一时刻只有一个,所以 QueueSort 插件只能 Enable 一个,如果用户 Enable 了 2 个则调度器启动时会报错退出。上面是默认的比拟函数,可供参考。
// Less is the function used by the activeQ heap algorithm to sort pods.
// It sorts pods based on their priority. When priorities are equal, it uses
// PodQueueInfo.timestamp.
func (pl *PrioritySort) Less(pInfo1, pInfo2 *framework.QueuedPodInfo) bool {p1 := pod.GetPodPriority(pInfo1.Pod)
p2 := pod.GetPodPriority(pInfo2.Pod)
return (p1 > p2) || (p1 == p2 && pInfo1.Timestamp.Before(pInfo2.Timestamp))
}
PreFilter
PreFilter 在 scheduling cycle 开始时就被调用,只有当所有的 PreFilter 插件都返回 success 时,能力进入下一个阶段,否则 Pod 将会被回绝掉,标识此次调度流程失败。PreFilter 相似于调度流程启动之前的预处理,能够对 Pod 的信息进行加工。同时 PreFilter 也能够进行一些预置条件的查看,去查看一些集群维度的条件,判断否满足 pod 的要求。
Filter
Filter 插件是 scheduler v1 版本中的 Predicate 的逻辑,用来过滤掉不满足 Pod 调度要求的节点。为了晋升效率,Filter 的执行程序能够被配置,这样用户就能够将能够过滤掉大量节点的 Filter 策略放到前边执行,从而缩小后边 Filter 策略执行的次数,例如咱们能够把 NodeSelector 的 Filter 放到第一个,从而过滤掉大量的节点。Node 节点执行 Filter 策略是并发执行的,所以在同一调度周期中屡次调用过滤器。
PostFilter
新的 PostFilter 的接口定义在 1.19 的版本会公布,次要是用于解决当 Pod 在 Filter 阶段失败后的操作,例如抢占,Autoscale 触发等行为。
PreScore
PreScore 在之前版本称为 PostFilter,当初批改为 PreScore,次要用于在 Score 之前进行一些信息生成。此处会获取到通过 Filter 阶段的节点列表,咱们也能够在此处进行一些信息预处理或者生成一些日志或者监控信息。
Scoring
Scoring 扩大点是 scheduler v1 版本中 Priority 的逻辑,目标是为了基于 Filter 过滤后的残余节点,依据 Scoring 扩大点定义的策略挑选出最优的节点。
Scoring 扩大点分为两个阶段:
- 打分:打分阶段会对 Filter 后的节点进行打分,scheduler 会调用所配置的打分策略
- 归一化: 对打分之后的构造在 0 -100 之间进行归一化解决
Reserve
Reserve 扩大点是 scheduler v1 版本的 assume 的操作,此处会对调度后果进行缓存,如果在后边的阶段产生了谬误或者失败的状况,会间接进入 Unreserve 阶段,进行数据回滚。
Permit
Permit 扩大点是 framework v2 版本引入的新性能,当 Pod 在 Reserve 阶段实现资源预留之后,Bind 操作之前,开发者能够定义本人的策略在 Permit 节点进行拦挡,依据条件对通过此阶段的 Pod 进行 allow、reject 和 wait 的 3 种操作。allow 示意 pod 容许通过 Permit 阶段。reject 示意 pod 被 Permit 阶段回绝,则 Pod 调度失败。wait 示意将 Pod 处于期待状态,开发者能够设置超时工夫。
binding cycle
binding cycle 须要调用 apiserver 的接口,耗时较长,为了进步调度的效率,须要异步执行,所以此阶段线程不平安。
Bind
Bind 扩大点是 scheduler v1 版本中的 Bind 操作,会调用 apiserver 提供的接口,将 pod 绑定到对应的节点上。
PreBind 和 PostBind
开发者能够在 PreBind 和 PostBind 别离在 Bind 操作前后执行,这两个阶段能够进行一些数据信息的获取和更新。
UnReserve
UnReserve 扩大点的性能是用于清理到 Reserve 阶段的的缓存,回滚到初始的状态。以后版本 UnReserve 与 Reserve 是离开定义的,将来会将 UnReserve 与 Reserve 对立到一起,即要求开发者在实现 Reserve 同时须要定义 UnReserve,保证数据可能无效的清理,防止留下脏数据。
实现本人的调度插件
scheduler-plugins
Kubernetes 负责 Kube-scheduler 的小组 sig-scheduling 为了更好的治理调度相干的 Plugin,新建了我的项目 scheduler-plugins 来不便用户治理不同的插件,用户能够间接基于这个我的项目来定义本人的插件。接下来咱们以其中的 Qos 的插件来为例,演示是如何开发本人的插件。
QoS 的插件次要基于 Pod 的 QoS(Quality of Service) class 来实现的,目标是为了实现调度过程中如果 Pod 的优先级雷同时,依据 Pod 的 Qos 来决定调度程序,调度程序是: 1. Guaranteed (requests == limits) 2. Burstable (requests < limits) 3. BestEffort (requests and limits not set)
插件结构
首先插件要定义插件的对象和构造函数
// 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
}
而后,依据咱们插件要对应的 extention point 来实现对应的接口,Qos 是作用于 QueueSort 的局部,所以咱们要实现 QueueSort 接口的函数。如下所示,QueueSortPlugin 接口只定义了一个函数 Less,所以咱们实现这个函数即可。
// 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
}
实现的函数如下。默认的 default QueueSort 在比拟的时候,首先比拟优先级,而后再比拟 pod 的 timestamp。咱们从新定义了 Less 函数,在优先级雷同的状况下,通过比拟 Qos 来决定优先级。
// Less is the function used by the activeQ heap algorithm to sort pods.
// It sorts pods based on their priority. When priorities are equal, it uses
// PodInfo.timestamp.
func (*Sort) Less(pInfo1, pInfo2 *framework.PodInfo) bool {p1 := pod.GetPodPriority(pInfo1.Pod)
p2 := pod.GetPodPriority(pInfo2.Pod)
return (p1 > p2) || (p1 == p2 && compQOS(pInfo1.Pod, pInfo2.Pod))
}
func compQOS(p1, p2 *v1.Pod) bool {p1QOS, p2QOS := v1qos.GetPodQOS(p1), v1qos.GetPodQOS(p2)
if p1QOS == v1.PodQOSGuaranteed {return true} else if p1QOS == v1.PodQOSBurstable {return p2QOS != v1.PodQOSGuaranteed} else {return p2QOS == v1.PodQOSBestEffort}
}
插件注册
咱们在启动的 main 函数中注册本人定义的插件和相应的构造函数
// 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)
}
}
代码编译
$ make
Scheduler 启动
kube-scheduler 启动时,配置./manifests/qos/scheduler-config.yaml 中 kubeconfig 的门路,启动时传入集群的 kubeconfig 文件以及插件的配置文件即可。
$ bin/kube-scheduler --kubeconfig=scheduler.conf --config=./manifests/qos/scheduler-config.yaml
至此,置信大家曾经通过咱们的介绍和示例理解了 Kubernetes Scheduling Framework 的架构和开发方法。
后续工作
Kubernetes Scheduling Framework 作为调度器的新架构方向,在可扩展性和定制化方面提高很大。基于此 Kubernetes 能够逐渐承载更多类型的利用负载了, 一个平台一套 IT 架构和技术堆栈的愿景向前演进。同时为了更好的反对数据计算类型的工作迁徙到 Kubernetes 平台中,咱们也在致力将数据计算类型中罕用 Coscheduling/Gang Scheduling、Capacity Scheduling、Dominant Resource Fairness 和多队列治理等个性,通过 Scheduling Framework 的插件机制来融入到原生的 Kube-scheduler 中。
接下来,本系列文章将围绕 AI、大数据处理和高规格计算资源集群等场景,介绍咱们是如何开发相应调度器插件的。敬请期待。