作者:Abdullah Gharaibeh(谷歌),Aldo Culquicondor(谷歌)
无论是在本地还是在云中,集群都面临着资源使用、配额和成本管理等方面的实际限制。尽管有自动伸缩功能,集群的容量是有限的。因此,用户需要一种简单的方法来公平有效地共享资源。
在本文中,我们将介绍Kueue[1],这是一个开源的作业(Job)排队控制器,旨在将批处理作业作为一个单元来管理。Kueue 将 pod 级别的编排留给了 Kubernetes 现有的稳定组件。Kueue 本身支持 Kubernetes Job[2] API,并提供了用于集成其他定制的批处理作业 API 的钩子。
为什么要有 Kueue?
作业排队是在本地和云环境中大规模运行批处理工作负载的一项关键功能。作业排队的主要目的,是管理对多个租户共享的有限资源池的访问。作业排队决定哪些作业应该等待,哪些作业可以立即启动,以及它们可以使用哪些资源。
一些最理想的作业排队要求包括:
- 配额和预算来控制谁可以使用什么,以及使用到什么限度。这不仅在具有静态资源(如本地资源)的集群中需要,在云环境中也需要,以控制稀缺资源的支出或使用。
- 租户之间资源的公平共享。为了最大限度地利用可用资源,应允许分配给非活动租户的任何未使用配额在活动租户之间公平共享。
- 基于可用性跨不同资源类型灵活安排工作。这在具有异构资源的云环境中非常重要,例如不同的架构(GPU 或 CPU 型号)和不同的供应模式(spot vs. on-demand)。
- 支持可按需调配资源的自动扩展环境。
普通的 Kubernetes 不能满足上述要求。在正常情况下,一旦创建了一个作业,作业控制器立即创建 pod,kube-scheduler 不断尝试将 pod 分配给节点。在大规模环境,这种情况会让控制层工作到死。目前也没有好的方法在作业级别控制哪些作业应该首先获得哪些资源,也没有方法表示顺序或公平共享。当前的 ResourceQuota 模型不太适合这些需求,因为配额是在资源创建时强制执行的,并且没有请求排队。ResourceQuotas 的目的,是提供一种内置的可靠性机制,其中包含管理员保护集群免于故障转移所需的策略。
在 Kubernetes 生态系统中,有几种作业调度的解决方案。然而,我们发现这些替代方案存在以下一个或多个问题:
- 它们取代了 Kubernetes 现有的稳定组件,如 kube-scheduler 或 job-controller。这不仅从操作的角度来看是有问题的,而且作业 API 中的重复会导致生态系统的碎片化并降低可移植性。
- 它们不与自动缩放集成,或者
- 它们缺乏对资源灵活性的支持。
Kueue 是如何工作
通过 Kueue,我们决定在 Kubernetes 上采用一种不同的作业排队方法,这种方法基于以下几个方面:
- 不重复已建立的 Kubernetes 组件为 pod 调度、自动缩放和作业生命周期管理提供的现有功能。
- 添加现有组件中缺少的关键功能。例如,我们投资了 Job API 来覆盖更多用例,如IndexedJob[3],并修复了与 pod 跟踪相关的长期问题[4]。虽然这条道路需要更长的时间来实现功能,但我们相信这是更可持续的长期解决方案。
- 确保与计算资源灵活且异构的云环境兼容。
为了使这种方法可行,Kueue 需要设定来影响那些已建立的组件的行为,以便它可以有效地管理何时何地开始一项工作。我们以两种功能的形式将这些设定添加到 Job API 中:
- Suspend 字段[5],它允许 Kueue 向作业控制器发出信号,指示何时开始或停止作业。
- 可变调度指令[6],允许 Kueue 在启动作业之前更新作业的.spec.template.spec.nodeSelector。这样,Kueue 可以控制 pod 放置,同时仍然将实际的 Pod 到节点调度委托给 kube-scheduler。
请注意,任何自定义作业 API 都可以由 Kueue 管理,只要该 API 提供上述两种功能。
资源模型
Kueue 定义了新的 API 来满足本文开头提到的需求。三个主要的 API 是:
- ResourceFlavor:一个集群范围的 API,用于定义可供消费的资源风格,就像 GPU 模型一样。其核心是一组标签,反映了提供这些资源的节点上的标签。
- ClusterQueue:一个集群范围的 API,通过为一个或多个 ResourceFlavor 设置配额来定义资源池。
- LocalQueue:用于分组和管理单个租户作业的命名空间的 API。最简单的形式是,LocalQueue 是一个指向 ClusterQueue 的指针,租户(建模为命名空间)可以使用它来启动他们的作业。
更多细节,请看API 概念文档[7]。虽然这三个 API 看起来复杂,但 Kueue 的大部分操作都是围绕 ClusterQueue 进行的;ResourceFlavor 和 LocalQueue APIs 主要是组织包装器。
示例使用案例
想象一下在云上的 Kubernetes 集群上运行批处理工作负载的如下设置:
- 你在集群中安装了cluster-autoscaler[8]来自动调整集群的大小。
- 有两种类型的自动扩展节点组,它们的资源调配策略不同:spot 和 on-demand。每个组的节点由标签 instance-type=spot 或 instance-type=ondemand 来区分。此外,因为不是所有的作业都可以在 spot 节点上运行,所以这些节点会被污点为 spot=true:NoSchedule。
- 为了在成本和资源可用性之间取得平衡,假设你希望作业使用多达 1000 个 on-demand 节点核,然后使用多达 2000 个 spot 节点核。
作为批处理系统的管理员,你定义了两种 ResourceFlavors 来表示两种类型的节点:
代码语言:javascript复制---
apiVersion: kueue.x-k8s.io/v1alpha2
kind: ResourceFlavor
metadata:
name: ondemand
labels:
instance-type: ondemand
---
apiVersion: kueue.x-k8s.io/v1alpha2
kind: ResourceFlavor
metadata:
name: spot
labels:
instance-type: spot
taints:
- effect: NoSchedule
key: spot
value: "true"
然后,通过创建 ClusterQueue 来定义配额,如下所示:
代码语言:javascript复制apiVersion: kueue.x-k8s.io/v1alpha2
kind: ClusterQueue
metadata:
name: research-pool
spec:
namespaceSelector: {}
resources:
- name: "cpu"
flavors:
- name: ondemand
quota:
min: 1000
- name: spot
quota:
min: 2000
请注意,ClusterQueue 资源中风格的顺序很重要:Kueue 将尝试根据顺序将作业放入可用的配额中,除非作业与特定的风格有明确的关联。
对于每个命名空间,你定义一个指向上面的 ClusterQueue 的 LocalQueue:
代码语言:javascript复制apiVersion: kueue.x-k8s.io/v1alpha2
kind: LocalQueue
metadata:
name: training
namespace: team-ml
spec:
clusterQueue: research-pool
管理员创建上述设置一次。批处理用户可以通过在其命名空间中列出本地队列来找到允许他们提交的队列。该命令类似于:kubectl get -n my-namespace localqueues
要提交作业,创建一个 Job 并按如下方式设置 kueue.x-k8s.io/queue-name 注释:
代码语言:javascript复制apiVersion: batch/v1
kind: Job
metadata:
generateName: sample-job-
annotations:
kueue.x-k8s.io/queue-name: training
spec:
parallelism: 3
completions: 3
template:
spec:
tolerations:
- key: spot
operator: "Exists"
effect: "NoSchedule"
containers:
- name: example-batch-workload
image: registry.example/batch/calculate-pi:3.14
args: ["30s"]
resources:
requests:
cpu: 1
restartPolicy: Never
Job 一旦创建,Kueue 就进行干预以暂停它。一旦 Job 位于 ClusterQueue 的头部,Kueue 就会通过检查作业请求的资源是否符合可用配额来评估它是否可以启动。
在上面的例子中,任务允许使用 spot 资源。如果先前允许的作业消耗了所有现有的 on-demand 配额,但不是所有的 spot 配额,则 Kueue 会使用 spot 配额来允许作业。Kueue 通过向作业对象发出一个更新来实现这一点:
- 将.spec.suspend 标志更改为 false
- 将术语 instance-type: spot 添加到作业的.spec.template.spec.nodeSelector,以便当作业控制器创建单元时,这些单元只能调度到 spot 节点上。
最后,如果有符合节点选择器条件的可用空节点,那么 kube-scheduler 将直接调度 pod。如果没有,那么 kube-scheduler 最初会将 pod 标记为不可调度的,这将触发集群自动伸缩程序来提供新节点。
未来的工作和参与方式
上面的例子展示了 Kueue 的一些特性,包括对配额的支持、资源灵活性,以及与集群自动缩放器的集成。Kueue 还支持公平共享、作业优先级和不同的排队策略。查看Kueue 文档[9],了解更多关于这些特性,以及如何使用 Kueue 的信息。
我们计划在 Kueue 中添加一些特性,比如分级配额、预算和对动态工作大小的支持。在不久的将来,我们将致力于增加对工作抢占的支持。
Github 上有最新的Kueue 版本[10];如果你在 Kubernetes 上运行批处理工作负载(需要 1.22 或更高版本),请尝试一下。我们正处于这个项目的早期阶段,我们正在寻求所有级别的反馈,主要或次要的,所以请不要犹豫来联系。我们也向其他贡献者开放,无论是修复或报告错误,还是帮助添加新功能或编写文档。你可以通过我们的仓库[11]、邮件列表[12]或Slack[13]与我们联系。
最后但同样重要的是,感谢所有使这个项目成为可能的贡献者[14]!
参考资料
[1]
Kueue: https://github.com/kubernetes-sigs/kueue/tree/main/docs#readme
[2]
Job: https://kubernetes.io/docs/concepts/workloads/controllers/job/
[3]
IndexedJob: https://kubernetes.io/blog/2021/04/19/introducing-indexed-jobs
[4]
修复了与 pod 跟踪相关的长期问题: https://kubernetes.io/docs/concepts/workloads/controllers/job/#job-tracking-with-finalizers
[5]
Suspend 字段: https://kubernetes.io/docs/concepts/workloads/controllers/job/#suspending-a-job
[6]
可变调度指令: https://kubernetes.io/docs/concepts/workloads/controllers/job/#mutable-scheduling-directives
[7]
API 概念文档: https://sigs.k8s.io/kueue/docs/concepts
[8]
cluster-autoscaler: https://github.com/kubernetes/autoscaler/tree/master/cluster-autoscaler
[9]
Kueue 文档: https://github.com/kubernetes-sigs/kueue/tree/main/docs
[10]
Kueue 版本: https://github.com/kubernetes-sigs/kueue/releases
[11]
仓库: http://sigs.k8s.io/kueue
[12]
邮件列表: https://groups.google.com/a/kubernetes.io/g/wg-batch
[13]
Slack: https://kubernetes.slack.com/messages/wg-batch
[14]
贡献者: https://github.com/kubernetes-sigs/kueue/graphs/contributors