本文已发布超过一年。较旧的文章可能包含过时内容。请检查页面中的信息自发布以来是否已发生变化。

介绍 Kueue

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

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

为什么选择 Kueue?

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

一些最期望的作业排队需求包括:

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

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

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

  • 它们替换了 Kubernetes 现有的稳定组件,例如 kube-scheduler 或 job-controller。这不仅从操作的角度看是有问题的,而且 Job API 的重复会导致生态系统的碎片化并降低可移植性。
  • 它们不与自动扩缩容集成,或者
  • 它们缺乏对资源灵活性的支持。

Kueue 如何工作

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

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

为了使这种方法可行,Kueue 需要控制旋钮来影响这些现有组件的行为,以便有效地管理何时何地启动作业。我们将这些控制旋钮以两种特性的形式添加到了 Job API 中:

  • Suspend 字段,允许 Kueue 向 job-controller 发出信号,告知何时启动或停止作业。
  • 可变调度指令,允许 Kueue 在启动作业之前更新 Job 的 .spec.template.spec.nodeSelector。这样,Kueue 可以控制 Pod 的放置,同时仍将实际的 Pod 到节点调度委托给 kube-scheduler。

请注意,如果任何自定义作业 API 提供上述两种能力,它都可以由 Kueue 管理。

资源模型

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

  • ResourceFlavor:一个集群范围的 API,用于定义可供消费的资源特性,例如 GPU 型号。本质上,ResourceFlavor 是一组标签,映射了提供这些资源的节点上的标签。
  • ClusterQueue:一个集群范围的 API,通过为一种或多种 ResourceFlavor 设置配额来定义资源池。
  • LocalQueue:一个命名空间范围的 API,用于分组和管理单租户作业。最简单的形式是,LocalQueue 是一个指向 ClusterQueue 的指针,该 ClusterQueue 是租户(建模为命名空间)可以用于启动其作业的队列。

有关更多详细信息,请参阅 API 概念文档。虽然这三个 API 可能看起来令人不知所措,但 Kueue 的大多数操作都围绕着 ClusterQueue 进行;ResourceFlavor 和 LocalQueue API 主要作为组织性包装器。

示例用例

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

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

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

---
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

管理员只需创建上述设置一次。批处理用户可以通过列出其命名空间中的 LocalQueue 来找到他们被允许提交作业的队列。命令类似于: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 在作业创建后立即介入以挂起作业。一旦作业位于 ClusterQueue 的头部,Kueue 会评估是否可以启动,通过检查作业请求的资源是否符合可用配额。

在上述示例中,作业容忍抢占式资源。如果先前已准入的作业消耗了所有现有按需配额,但没有消耗所有抢占式配额,Kueue 将使用抢占式配额准入该作业。Kueue 通过对 Job 对象进行一次更新来完成此操作:

  • .spec.suspend 标志更改为 false
  • 将术语 instance-type: spot 添加到作业的 .spec.template.spec.nodeSelector 中,以便当 Pod 由 job controller 创建时,这些 Pod 只能调度到抢占式节点上。

最后,如果存在具有匹配节点选择器术语的可用空节点,则 kube-scheduler 将直接调度 Pod。如果不存在,则 kube-scheduler 最初会将 Pod 标记为不可调度,这将触发 cluster-autoscaler 供应新节点。

未来工作与参与方式

上面的示例提供了一瞥 Kueue 的部分功能,包括对配额、资源灵活性以及与 cluster autoscaler 集成的支持。Kueue 还支持公平共享、作业优先级和不同的排队策略。请查看 Kueue 文档,了解有关这些功能以及如何使用 Kueue 的更多信息。

我们计划为 Kueue 添加许多功能,例如分层配额、预算以及对动态大小作业的支持。在不久的将来,我们专注于添加对作业抢占的支持。

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

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