本文发表于一年多前。旧文章可能包含过时内容。请检查页面中的信息自发布以来是否已变得不正确。

介绍 Kueue

无论是本地部署还是在云端,出于资源使用、配额和成本管理的原因,集群都面临着实际的限制。无论自动伸缩能力如何,集群的容量都是有限的。因此,用户希望有一种简单的方法来公平、高效地共享资源。

在本文中,我们将介绍 Kueue,一个开源的作业排队控制器,旨在将批量作业作为一个单一单元进行管理。Kueue 将 Pod 级别的编排工作交由 Kubernetes 现有的稳定组件处理。Kueue 原生支持 Kubernetes 的 Job API,并为集成其他为批量作业定制的 API 提供了钩子。

为什么选择 Kueue?

作业排队是在本地和云环境中大规模运行批量工作负载的关键功能。作业排队的主要目标是管理对由多个租户共享的有限资源池的访问。作业排队决定了哪些作业应该等待,哪些可以立即开始,以及它们可以使用哪些资源。

一些最理想的作业排队需求包括:

  • 配额和预算,用于控制谁可以使用什么以及使用的上限。这不仅在像本地这样的静态资源集群中需要,在云环境中为了控制开销或稀缺资源的使用也同样需要。
  • 租户之间公平共享资源。为了最大限度地利用可用资源,应允许将分配给不活跃租户的任何未使用配额在活跃租户之间公平共享。
  • 根据可用性,灵活地将作业放置在不同资源类型上。这在云环境中非常重要,因为云环境拥有异构资源,例如不同的架构(GPU 或 CPU 型号)和不同的供应模式(Spot vs On-Demand)。
  • 支持可以按需供应资源的自动伸缩环境。

原生的 Kubernetes 无法解决上述需求。在正常情况下,一旦创建了一个 Job,Job 控制器就会立即创建 Pods,而 kube-scheduler 会持续尝试将 Pods 分配给节点。在大规模场景下,这种情况可能会让控制平面不堪重负。目前也没有好的方法在作业级别控制哪些作业应该优先获得哪些资源,也没有方法来表达顺序或公平共享。当前的 ResourceQuota 模型不适合这些需求,因为配额是在资源创建时强制执行的,并且没有请求排队机制。ResourceQuotas 的目的是提供一种内置的可靠性机制,并带有管理员所需的策略,以保护集群免于故障。

在 Kubernetes 生态系统中,有几种作业调度解决方案。然而,我们发现这些替代方案存在以下一个或多个问题:

  • 它们取代了 Kubernetes 现有的稳定组件,如 kube-scheduler 或 Job 控制器。这不仅在运维角度上存在问题,而且 Job API 的重复会导致生态系统的碎片化并降低可移植性。
  • 它们不与自动伸缩集成,或者
  • 它们缺乏对资源灵活性的支持。

Kueue 的工作原理

对于 Kueue,我们决定采用一种不同的方法来在 Kubernetes 上实现作业排队,该方法围绕以下几个方面:

  • 不重复已由成熟的 Kubernetes 组件提供的现有功能,如 Pod 调度、自动伸缩和作业生命周期管理。
  • 为现有组件添加缺失的关键功能。例如,我们投入精力改进 Job API 以涵盖更多用例,如 IndexedJob,并修复了与 Pod 跟踪相关的长期存在的问题。虽然这条路径需要更长的时间来推出功能,但我们相信这是一个更可持续的长期解决方案。
  • 确保与计算资源具有弹性和异构性的云环境兼容。

为了使这种方法可行,Kueue 需要一些“旋钮”来影响那些成熟组件的行为,以便它能有效地管理何时何地启动作业。我们以两种功能的形式将这些“旋钮”添加到了 Job API 中:

请注意,任何自定义的 Job API 只要提供上述两种能力,就可以由 Kueue 进行管理。

资源模型

Kueue 定义了新的 API 来满足本文开头提到的需求。三个主要的 API 是:

  • ResourceFlavor:一个集群范围的 API,用于定义可供使用的资源风味,例如一种 GPU 型号。其核心是一个 ResourceFlavor 是一组标签,这些标签与提供这些资源的节点上的标签相对应。
  • ClusterQueue:一个集群范围的 API,通过为一个或多个 ResourceFlavor 设置配额来定义资源池。
  • LocalQueue:一个命名空间范围的 API,用于分组和管理单个租户的作业。在其最简单的形式中,LocalQueue 是一个指向 ClusterQueue 的指针,租户(以命名空间为模型)可以使用它来启动他们的作业。

有关更多详细信息,请参阅 API 概念文档。虽然这三个 API 可能看起来有些复杂,但 Kueue 的大部分操作都围绕 ClusterQueue 进行;ResourceFlavor 和 LocalQueue API 主要是组织性的包装器。

用例示例

想象一下在云上的 Kubernetes 集群上运行批量工作负载的以下设置:

  • 你在集群中安装了 cluster-autoscaler,以自动调整集群的大小。
  • 有两种类型的自动伸缩节点组,它们的供应策略不同:Spot 和 On-Demand。每个组的节点通过标签 instance-type=spotinstance-type=ondemand 来区分。此外,由于并非所有 Job 都能容忍在 Spot 节点上运行,这些节点被设置了污点 spot=true:NoSchedule
  • 为了在成本和资源可用性之间取得平衡,假设你希望 Job 最多使用 1000 核的 On-Demand 节点,然后再最多使用 2000 核的 Spot 节点。

作为批处理系统的管理员,你定义了两个代表这两种类型节点的 ResourceFlavors:

---
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 来定义配额,如下所示:

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 资源中 Flavor 的顺序很重要:除非作业对特定 Flavor 有明确的亲和性,否则 Kueue 将按照顺序尝试将作业放入可用的配额中。

对于每个命名空间,你定义一个指向上述 ClusterQueue 的 LocalQueue:

apiVersion: kueue.x-k8s.io/v1alpha2
kind: LocalQueue
metadata:
  name: training
  namespace: team-ml
spec:
  clusterQueue: research-pool

管理员一次性完成上述设置。批量用户可以通过列出其命名空间中的 LocalQueues 来找到他们被允许提交到的队列。命令类似于:kubectl get -n my-namespace localqueues

要提交工作,创建一个 Job 并设置 kueue.x-k8s.io/queue-name 注解,如下所示:

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

Kueue 在 Job 创建后立即介入将其挂起。一旦该 Job 到达 ClusterQueue 的头部,Kueue 会评估它是否可以启动,方法是检查该 Job 请求的资源是否符合可用配额。

在上面的例子中,该 Job 容忍 Spot 资源。如果先前已接纳的作业消耗了所有现有的 On-Demand 配额但并未用完 Spot 的配额,Kueue 会使用 Spot 配额接纳该 Job。Kueue 通过对 Job 对象进行一次更新来完成此操作:

  • .spec.suspend 标志更改为 false
  • instance-type: spot 这一项添加到作业的 .spec.template.spec.nodeSelector 中,这样当 Job 控制器创建 Pods 时,这些 Pods 只能调度到 Spot 节点上。

最后,如果有可用的空闲节点且其节点选择器术语匹配,那么 kube-scheduler 将直接调度这些 Pods。如果没有,kube-scheduler 最初会将这些 Pods 标记为不可调度,这将触发 cluster-autoscaler 来供应新节点。

未来工作与参与

上面的例子展示了 Kueue 的一些功能,包括对配额、资源灵活性以及与集群自动伸缩器集成的支持。Kueue 还支持公平共享、作业优先级和不同的排队策略。请查看 Kueue 文档以了解更多关于这些功能以及如何使用 Kueue 的信息。

我们计划为 Kueue 添加一些功能,例如分层配额、预算以及对动态调整大小作业的支持。在更近的将来,我们专注于增加对作业抢占的支持。

最新的 Kueue 版本已在 Github 上发布;如果你在 Kubernetes(需要 v1.22 或更新版本)上运行批量工作负载,请尝试一下。我们正处于该项目的早期阶段,我们寻求各种级别的反馈,无论是大的还是小的,所以请不要犹豫与我们联系。我们也欢迎更多的贡献者,无论是修复或报告错误,还是帮助添加新功能或编写文档。你可以通过我们的 仓库邮件列表或在 Slack 上与我们联系。

最后但同样重要的是,感谢所有我们的贡献者,是你们让这个项目成为可能!