本文發表於一年多前。舊文章可能包含過時內容。請檢查頁面中的資訊自發布以來是否已變得不正確。

介紹 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 上與我們聯絡。

最後但同樣重要的是,感謝所有我們的貢獻者,是你們讓這個專案成為可能!