iT邦幫忙

2026 iThome 鐵人賽

DAY 8
0
Kubernetes

凌晨四點,女友帶著 GPU 來我家學習 Kubernetes:打造 K8s AI Infra 的 30 夜系列 第 8

【Day 8】Kueue:Kubernetes 上的工作准入與配額管理

  • 分享至 

  • xImage
  •  

前面幾天討論的都是「怎麼切」。DRA 讓使用者說得出要哪一張卡、要多少;HAMi 讓那個數字在容器裡被真正執行。

但這些機制都建立在同一個前提上:Pod 已經被建立出來了。

真實的叢集並不是這樣運作的。八張卡、三個團隊、十幾個訓練工作同時想跑,就會出現幾個問題:誰先跑、誰該等、等的要等多久。前面幾天介紹的機制都無法回答這些問題,因為它們處理的是「這個 Pod 要拿什麼」,而這些問題屬於「這個 Job 現在該不該開始」。

今天的主角 Kueue 處理的就是這一層。


Kueue 的定位:決定 Job 能不能開始

Kueue 負責管理配額,以及工作如何消耗這些配額。具體來說,它決定三件事:工作何時該等待、何時被准許開始,以及何時該被搶占。其中「被准許開始」指的是 Pod 可以被建立了;在那之前,Pod 根本不存在。「被搶占」則是指執行中的 Pod 會被刪除。

Kueue 不取代任何既有的 Kubernetes 元件。自動擴縮仍由 cluster-autoscaler 負責,Pod 放到哪個節點仍由 kube-scheduler 決定,Job 的生命週期仍由 kube-controller-manager 管理。Kueue 只在這些元件前面加上一道關卡。

因此整條鏈的分工如下:

元件 決定什麼
Kueue 這個 Job 現在能不能開始
kube-scheduler Pod 放在哪個節點
DRA 這個 Pod 拿哪一張
HAMi 拿到之後能用多少

由上往下,粒度一層比一層細。只有最上面這一層被擋下時,後面的事情完全不會發生。

實作方式:暫停與解除暫停

Kueue 的做法相當直接:利用 Job 的 spec.suspend 欄位

suspendbatch/v1 Job 原本就有的欄位,設為 true 的 Job 不會產生任何 Pod。Kueue 分兩個步驟使用它:

  1. Job 建立時,Kueue 透過 webhook 將它設為暫停;Kueue 的 controller 隨後為它建立一個 Workload。
  2. 額度足夠、Workload 被准入後,Kueue 解除 Job 的暫停,Job 才開始建立 Pod。

因為第一步由 webhook 自動處理,使用者不需要自己把 Job 建立成暫停狀態。本篇實驗的 yaml 仍然寫了 suspend: true,這樣寫沒有錯,但並非必要。


四個核心物件

Kueue 的 API 物件不少,本篇只用到四個:

物件 範圍 定義
ResourceFlavor 叢集 描述叢集中有哪些可用資源
ClusterQueue 叢集 管理一池資源,定義使用上限與公平共享規則
LocalQueue namespace 屬於同一個租戶、關係密切的工作負載歸為一組
Workload namespace 會執行到結束的應用程式,是 Kueue 中准入的基本單位

依關係排成一條線:

ResourceFlavor   資源的種類
      ↓
ClusterQueue     這一池有多少額度        ← 管理員建立,叢集層級
      ↓
LocalQueue       某個 namespace 的入口   ← 團隊使用的入口
      ↓
Workload         一次申請                ← Kueue 自動產生

Workload 是四者中唯一不需要自己建立的物件。 送出一個 Job 後,Kueue 會為它產生一個對應的 Workload,這才是 Kueue 實際在排隊處理的對象。

額度寫在 ClusterQueue 的 nominalQuota 欄位,代表這個 ClusterQueue 在任一時刻可以使用的該資源數量。


實驗環境

今天的實驗使用 day1 用 fake-gpu-operator 模擬假卡的叢集來測試:

kubectl get nodes -o custom-columns=NODE:.metadata.name,GPU:.status.allocatable.'nvidia\.com/gpu'
NODE                    GPU
gpu-lab-control-plane
gpu-lab-worker          8
gpu-lab-worker2

只有一台 worker 上有 8 張假卡。 另外兩個節點沒有 run.ai/simulated-gpu-node-pool 標籤,所以 fake-gpu-operator 不會管理它們。

這個 8 是整個實驗的關鍵數字。

安裝 Kueue 只需要官方的 release manifest:

kubectl apply --server-side \
  -f https://github.com/kubernetes-sigs/kueue/releases/download/v0.19.5/manifests.yaml

以本篇使用的版本來說,這份 manifest 會安裝十一個 CRD、數十個 RBAC 角色、一個 controller manager,以及兩個 webhook configuration。webhook 負責在 Job 建立時將它攔截並設為暫停。

kubectl get pods -n kueue-system
NAME                                        READY   STATUS    RESTARTS   AGE
kueue-controller-manager-5f685d7d6f-8gk4z   1/1     Running   0          23s

建立一個配額只有 4 張卡的佇列

apiVersion: v1
kind: Namespace
metadata: { name: research }

---
apiVersion: kueue.x-k8s.io/v1beta2
kind: ResourceFlavor
metadata: { name: gpu }

---
apiVersion: kueue.x-k8s.io/v1beta2
kind: ClusterQueue
metadata: { name: research-cq }
spec:
  namespaceSelector: {}                    # 允許所有 namespace 透過 LocalQueue 使用
  resourceGroups:
    - coveredResources: ["nvidia.com/gpu"]
      flavors:
        - name: gpu
          resources:
            - name: "nvidia.com/gpu"
              nominalQuota: 4              # 這個佇列只有 4 張卡的額度

---
apiVersion: kueue.x-k8s.io/v1beta2
kind: LocalQueue
metadata: { name: team-queue, namespace: research }
spec: { clusterQueue: research-cq }

這裡的 ResourceFlavor 是空的,沒有 nodeLabels,也沒有 taint。叢集中的資源同質時,可以直接使用空的 ResourceFlavor。需要區分不同型號(例如 A100 與 T4)時,才會用 nodeLabels 等欄位把 ResourceFlavor 對應到特定節點。

kubectl apply -f 01-quota.yaml
namespace/research created
resourceflavor.kueue.x-k8s.io/gpu created
clusterqueue.kueue.x-k8s.io/research-cq created
localqueue.kueue.x-k8s.io/team-queue created

目前的狀態:叢集有 8 張卡,但這個佇列只准使用 4 張。 剩下的 4 張沒有故障,也沒有被佔用,只是不在這個佇列的額度內。


送出兩個各需要 4 張卡的訓練工作

apiVersion: batch/v1
kind: Job
metadata:
  name: train-a
  namespace: research
  labels: { kueue.x-k8s.io/queue-name: team-queue }   # 交給 Kueue 管理
spec:
  parallelism: 4
  completions: 4
  suspend: true
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: worker
          image: registry.k8s.io/pause:3.10
          resources:
            limits:
              nvidia.com/gpu: 1

train-b 內容相同,只有名稱不同。)

parallelism: 4 加上每個 Pod 需要一張卡,一個 Job 就需要 4 張,剛好用完整個配額。兩個 Job 同時送出,一定有一個進不來。

決定 Kueue 是否介入的是這個 label:

labels: { kueue.x-k8s.io/queue-name: team-queue }

它指定 Job 要送進哪個 LocalQueue。本篇的 research namespace 中,沒有這個 label 的 Job 不會被 Kueue 管理,會照常直接執行。

kubectl apply -f 02-twojob.yaml
job.batch/train-a created
job.batch/train-b created

兩個 Job 都顯示 created。但被建立不代表已經開始執行。


一個執行中,一個連 Pod 都沒有

kubectl -n research get jobs \
  -o custom-columns='JOB:.metadata.name,SUSPEND:.spec.suspend,ACTIVE:.status.active'
JOB       SUSPEND   ACTIVE
train-a   true      <none>
train-b   false     4

兩個 Job 的 yaml 相同、同時送出,suspend 都寫成 true。但現在一個是 true、一個是 falseKueue 解除了 train-b 的暫停,train-a 則維持原狀。

至於為什麼是 train-b 而不是 train-a:Kueue 預設依優先權、再依 Workload 的建立時間排序。兩個 Job 優先權相同,又是同一次 apply 送出,建立時間落在同一秒,排序比不出先後,所以誰先進場並不保證。

再看 Pod:

kubectl -n research get pods
NAME            READY   STATUS    RESTARTS   AGE
train-b-56x8m   1/1     Running   0          68s
train-b-5j8bs   1/1     Running   0          68s
train-b-h46j5   1/1     Running   0          68s
train-b-vfdcl   1/1     Running   0          68s

四個 Pod 全部屬於 train-b

train-a 沒有任何 Pod。 不是 Pending,也不是 ContainerCreating,而是根本不存在

這和一般資源不足的情況完全不同。一般情況下,Pod 會停在 Pending,describe 可以看到 Insufficient nvidia.com/gpu 的事件。這裡什麼都看不到,因為 kube-scheduler 從來沒有收到過這些 Pod。


排隊中的工作在哪裡

train-a 目前以 Workload 物件的形式存在:

kubectl -n research get kwl
NAME                QUEUE        RESERVED IN   ADMITTED   FINISHED   AGE
job-train-a-5db4e   team-queue                                       2m35s
job-train-b-a44b6   team-queue   research-cq   True                  2m35s

kwlworkloads.kueue.x-k8s.io 的簡稱。)

兩個 Workload 都存在,名稱格式是 job-<Job 名稱>-<雜湊>由 Kueue 自動產生,yaml 裡並沒有定義它們。

差別在中間兩欄:

RESERVED IN ADMITTED
job-train-b research-cq True
job-train-a

這兩欄對應 Kueue 的兩個步驟:

  • 額度保留(Quota Reservation):Kueue 在 ClusterQueue 中為 Workload 鎖定所需的資源。這和 Pod 排程是兩回事,還沒有任何 Pod 被放到節點上。RESERVED IN 顯示額度保留在哪個 ClusterQueue。
  • 准入(Admission):允許 Workload 開始執行,也就是 Pod 可以被建立。取得額度保留是准入的前提條件之一。

train-aRESERVED IN 是空的,代表它連額度都還沒取得,自然也不會被准入。

也可以從 condition 的角度確認:

kubectl -n research get workloads.kueue.x-k8s.io \
  -o custom-columns='NAME:.metadata.name,QUEUE:.spec.queueName,ADMITTED:.status.conditions[?(@.type=="Admitted")].status'
NAME                QUEUE        ADMITTED
job-train-a-5db4e   team-queue   <none>
job-train-b-a44b6   team-queue   True

Admitted 是 Workload 上的一個 condition。train-b 的值是 Truetrain-a 則沒有 AdmittedTrue 的紀錄,也就是尚未被准入。若想看 train-a 目前完整的 condition 與未准入的原因,可以用 kubectl describe 查看該 Workload。

最後看佇列本身的狀態:

kubectl get clusterqueue research-cq \
  -o custom-columns='NAME:.metadata.name,PENDING:.status.pendingWorkloads,ADMITTED:.status.admittedWorkloads'
NAME          PENDING   ADMITTED
research-cq   1         1

一個排隊中,一個已准入。 這兩個數字可以直接從 ClusterQueue 的 status 看到。


四張空著的卡

把兩件事並列:

叢集上的卡        8 張
train-b 使用      4 張
尚未使用          4 張
train-a 需要      4 張   ← 數量剛好足夠

train-a 的狀態    排隊中

實體資源足夠,工作卻無法執行。

這不是錯誤,而是 Kueue 的設計目的。那 4 張卡之所以閒置,是因為 nominalQuota: 4 規定這個佇列只能使用 4 張。額度是人為設定的上限,與機器上實際有多少卡無關。

這種設計的用意在於保留資源給其他團隊。本實驗只有一個佇列,所以看起來像是浪費;實際環境中,research-cqinference-cqci-cq 會各自持有一份額度,而「研究團隊不能用光所有卡」正是期望的行為。

kube-scheduler 無法做到這件事。 它只判斷「節點上還有沒有資源」,不判斷「這個團隊該不該使用」。kube-scheduler 看到的是實體資源,Kueue 看到的是已分配的額度


小結

今天介紹了 Kueue,它處理的是「這個工作現在該不該開始」。它不取代 kube-scheduler,而是在 Pod 建立之前加上一道准入關卡:Job 建立時被設為暫停,等 Workload 在 ClusterQueue 中取得額度、被准入之後才解除暫停,Pod 也要到那時才會出現。

不過,Kueue 只決定工作何時開始,Pod 放到哪裡仍然交給 kube-scheduler,而 kube-scheduler 是一次排一個 Pod。對需要多個 Pod 同時到齊的分散式訓練來說,這還不夠。接下來會介紹 gang scheduling,以及在排程階段處理整組 Pod 的 Volcano 和 KAI Scheduler。


參考資料

Kueue — Overview
Kueue — Concepts
Kueue — ClusterQueue
Kueue — Resource Flavor
Kueue — Run A Kubernetes Job
Kueue — Setup default LocalQueue
Kubernetes — Jobs:Suspending a Job


上一篇
【Day 7】HAMi:這顆哈密瓜沒有哈味,但它讓你在 GPU 上無窮回味
下一篇
【Day 9】Volcano:你是否在火山救過一組永遠拿不到 GPU 的 Pod
系列文
凌晨四點,女友帶著 GPU 來我家學習 Kubernetes:打造 K8s AI Infra 的 30 夜9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言