很多人刚接触 Kueue 时最大的困惑,不是 YAML 怎么写,而是看着一堆 CRD:ResourceFlavor、ClusterQueue、LocalQueue、Cohort、Workload,不知道它们之间到底是什么关系。本文不会逐个照着 API 文档介绍字段,而是把这五个对象放到同一条资源准入链路中,一次讲清楚它们各自负责什么、为什么要存在,以及它们之间如何协作。
上一篇文章中分析了 Kueue 的完整工作流:
kueue-workflow.png
涉及到多个核心对象,下面逐个拆开讲。
- ResourceFlavor:资源长什么样
Kueue 要管资源,第一步得知道集群里有哪些"种类"的资源。集群里的 CPU、GPU 通常不是同构的:
价格和可用性 :竞价型 vs 按需型虚拟机
架构 :x86 vs ARM CPU
品牌和型号 :Nvidia A100 vs T4 GPU
ResourceFlavor 就是干这个的——给资源分个类,贴个标签,后面 ClusterQueue 按这个分类来管配额。
Kueue 在 Admission 阶段,会根据 ClusterQueue 中配置的 Flavor 顺序、Quota 是否满足、Flavor 是否匹配 Pod 等因素,选择一个可用的 ResourceFlavor。
空 Flavor
如果集群资源是同构的,或者不需要为不同资源规格分别管理配额,那直接创建一个不包含任何标签或污点的空 ResourceFlavor 即可:
apiVersion: kueue.x-k8s.io/v1beta2
kind: ResourceFlavor
metadata:
name: default-flavor
这类 ResourceFlavor 不承担节点筛选或污点控制的职责,仅作为统一的资源抽象存在,方便后续扩展不同资源类型。
只分类不加污点
apiVersion: kueue.x-k8s.io/v1beta2
kind: ResourceFlavor
metadata:
name: “gpu”
spec:
nodeLabels:
instance-type: gpu
新增参数:
spec.nodeLabels :通过 label 关联到对应的节点。具体实现上则是通过将 flavor 中 nodeLabels 部分自动注入到 Pod Spec 中的 nodeSelector,从而达到只将 Pod 调度到该 flavor 关联的节点上。
这类 Flavor 实现了根据 label 将节点进行分类,算是名副其实。管理员先给节点打上对应 label 即可实现分类:
root@lixd-dev-4:~# kubectl label node lixd-dev-4 instance-type=gpu
node/lixd-dev-4 labeled
但也只是简单做了分类,不是该 Flavor 中的 Job 也能手动调度到这些 GPU 节点。比较推荐的做法是给节点再打上污点,这样就不是随便能调度上去了:
kubectl taint node xxx nvidia.com/gpu=true:NoSchedule
节点打上污点后,Flavor 中的 Job 也不能调度了。Kueue 提供了两种模式来解决这个问题:
自动容忍模式 :将污点容忍信息写到 ResourceFlavor,Kueue 自动给使用该 Flavor 的 Pod 加上容忍
手动容忍模式 :将污点信息写到 ResourceFlavor,Kueue 做匹配拦截,只允许带了对应容忍的 Pod 使用该 Flavor
带污点
自动污点容忍调度
apiVersion: kueue.x-k8s.io/v1beta2
kind: ResourceFlavor
metadata:
name: “spot”
spec:
nodeLabels:
instance-type: spot
tolerations:
- key: “spot-taint” # 节点上已有污点的 key
operator: “Exists”
effect: “NoSchedule” # 支持 NoSchedule 和 NoExecute
新增参数:
spec.tolerations :声明该 flavor 关联节点上的污点容忍信息。Kueue 准入时自动注入到 Pod 的 .spec.template.spec.tolerations,确保 Pod 能容忍节点污点从而正常调度。
手动污点容忍
apiVersion: kueue.x-k8s.io/v1beta2
kind: ResourceFlavor
metadata:
name: “spot”
spec:
nodeLabels:
instance-type: spot
nodeTaints:
- effect: NoSchedule # 只支持 NoSchedule 和 NoExecute,PreferNoSchedule 会被忽略
key: spot
value: “true”
新增参数:
spec.nodeTaints :在 flavor 上定义准入门槛。Kueue 不会自动注入容忍度,Pod 必须自己带对应 toleration 才能通过准入、拿到配额。
用户提交 Job 时必须自己带上 toleration,否则 Kueue 不批配额:
spec:
template:
spec:
tolerations:
- key: spot
operator: Exists
effect: NoSchedule
注意 :spec.nodeTaints 通常应与节点上的真实污点保持一致。否则可能出现 Pod 过了 Kueue 准入、拿到配额,但 K8s 调度器发现节点有真污点而 Pod 没对应 toleration,最终调度失败——白白占了配额。本质就是把调度层面的拦截提前到 Kueue 准入层面。
两种模式怎么选
自动模式(spec.tolerations) 手动模式(spec.nodeTaints)
谁给 Pod 加 toleration Kueue 自动注入 用户自己写
用户需要关心节点污点吗 不需要,提交 Job 就行 必须自己写 toleration
用在哪 生产默认,对用户最友好 保护昂贵资源,强制用户显式声明
选型建议:优先用自动模式;只有当你需要强制用户显式声明才能使用某种昂贵资源时,才用手动模式。
一句话记住:ResourceFlavor 定义资源的属性(标签、污点、容忍等),真正决定资源配额和公平性的仍然是 ClusterQueue。
- ClusterQueue:谁是配额的真正管家
如果 ResourceFlavor 是给资源分类,那真正决定谁能用、用多少的是谁?答案就是 ClusterQueue。它才是 Kueue 配额管理的核心,定义了使用上限和公平共享规则。
基础用法
一个基础的 ClusterQueue 示例如下:
apiVersion: kueue.x-k8s.io/v1beta2
kind: ClusterQueue
metadata:
name: “cluster-queue”
spec:
namespaceSelector: {} # 匹配所有命名空间。
resourceGroups:
- coveredResources: [“cpu”, “memory”, “pods”]
flavors:- name: “default-flavor”
resources:- name: “cpu”
nominalQuota: 9 - name: “memory”
nominalQuota: 36Gi - name: “pods”
nominalQuota: 5
字段解析:
- name: “cpu”
- name: “default-flavor”
spec.namespaceSelector :指定该 ClusterQueue 可以在哪些 namespace 被使用。当前 ClusterQueue 可以被任意 Namespace 使用。
resourceGroups :资源组,定义 ClusterQueue 的资源额度,可以有多个,每个组都是独立的。
coveredResources :管哪些资源,同一个 group 里面的资源必须在同一个 flavor 里面分配。
flavors :可以有多个,分配是按顺序尝试。
name :使用名称引用前面创建的 ResourceFlavor。
resources :在这个 flavor 下,每种资源的配额上限。
nominalQuota :保底配额,表示该 ClusterQueue 的名义配额,优先保障自身使用;未使用的部分可以按照 borrowing/lending 规则被其他 Queue 借用。
borrowingLimit :你最多能从 Cohort 里借入多少,所以你最多能用 nominalQuota + borrowingLimit。
lendingLimit :你最多允许别人从你这借走多少(当你空闲时),需要时可通过预抢占收回。
以 cpu 为例,基于上面三个配额字段,你可以使用的范围是 nominalQuota ~ (nominalQuota + borrowingLimit)。
高级用法
在基础配置之上,ClusterQueue 还支持以下进阶字段,按用途分组:
调度顺序:排队策略
spec:
queueingStrategy: BestEffortFIFO
BestEffortFIFO(默认) :前面的 Job 拿不到配额,后面的能插队先跑,资源利用率高。
StrictFIFO :前面的 Job 拿不到配额,后面的必须等,适合有顺序依赖的 Pipeline。
资源共享:加入 Cohort
spec:
cohortName: research-cohort
cohortName :关联到一个 Cohort,同一个 Cohort 里的 ClusterQueue 可以互相共享资源。
抢占相关
当 ClusterQueue 或其 Cohort 中没有足够的配额时,新进入的 Workload 可以触发预抢占,挤掉低优先级的 Workload。涉及两个配置项:
预抢占策略
spec:
preemption:
reclaimWithinCohort: Any # 可抢占 Cohort 中超过名义配额的 Workload
borrowWithinCohort:
policy: LowerPriority # 借用时只抢占低优先级(不能与 Fair Sharing 一同使用)
maxPriorityThreshold: 100 # 且仅抢占优先级 ≤ 100 的 Workload
withinClusterQueue: LowerPriority # 同队列内,低优先级让路给高优先级
withinClusterQueue :同队列内,当待处理 Workload 不适合配额时,是否可抢占同队列中的活动 Workload。Never(默认)不抢占;LowerPriority 仅抢占低优先级;LowerOrNewerEqualPriority 抢占低优先级或同优先级的。
reclaimWithinCohort :是否可抢占 Cohort 中使用了超过其名义配额的 Workload。Never(默认)不抢占;LowerPriority 仅抢占低优先级;Any 可抢占任意优先级。
borrowWithinCohort :当需要借用配额时是否触发抢占。Never(默认)不触发;LowerPriority 仅抢占 Cohort 中低优先级的 Workload(需同时启用 reclaimWithinCohort)。注意:只能配置经典抢占,不能与 Fair Sharing 一同使用。
优先抢占还是借用
当 ClusterQueue 有多个 flavor 时,Kueue 按顺序尝试。当当前 flavor 配额用完时,你可以影响 Kueue 的行为:
whenCanBorrow :能从 Cohort 借的时候。MayStopSearch(默认):借了就停,不再试下一个;TryNextFlavor:不借,先试下一个 flavor。
whenCanPreempt :能抢占低优先级 Job 的时候。TryNextFlavor(默认):先试下一个 flavor;MayStopSearch:不试了,直接抢占。
spec:
flavorFungibility:
whenCanBorrow: TryNextFlavor
whenCanPreempt: MayStopSearch
运维控制:停止策略
控制队列的运行状态,ClusterQueue 和 LocalQueue 都支持。
None(默认) :正常运行,新 Job 正常准入,已运行的不受影响。
Hold :停止新的准入,已准入的不受影响,适合维护、配额调整等临时场景。
HoldAndDrain :停止新的准入 + 触发已准入工作负载的驱逐,适合紧急情况需要立刻清场。
维护完恢复为 None 或直接删掉这个字段即可。这是运维操作,不是常态配置。
spec:
stopPolicy: Hold
把上面的都放在一起
一个包含所有字段的完整 ClusterQueue:
apiVersion: kueue.x-k8s.io/v1beta2
kind: ClusterQueue
metadata:
name: ai-team-cq
spec:
1. 加入 Cohort,允许和其他 ClusterQueue 共享资源
cohortName: research-cohort
2. 排队策略
queueingStrategy: BestEffortFIFO # 默认。StrictFIFO 会阻塞
3. 命名空间选择器(谁能用这个 ClusterQueue)
namespaceSelector:
matchLabels:
team: ai
4. 抢占策略
preemption:
reclaimWithinCohort: Any # 可抢占 Cohort 中超过名义配额的 Workload
borrowWithinCohort:
policy: LowerPriority # 借用时只抢占低优先级(不能与 Fair Sharing 一同使用)
maxPriorityThreshold: 100 # 且仅抢占优先级 ≤ 100 的 Workload
withinClusterQueue: LowerPriority # 同队列内,低优先级让路给高优先级
5. 配置是优先抢占还是借用
flavorFungibility:
whenCanBorrow: TryNextFlavor
whenCanPreempt: MayStopSearch
6. 停止策略
stopPolicy: Hold
7. 资源组定义
resourceGroups:
coveredResources: [“cpu”, “memory”] # 第一组:计算资源
flavors:- name: default-flavor
resources:- name: cpu
nominalQuota: 16 # 保底配额
borrowingLimit: 8 # 最多能从 Cohort 借多少
lendingLimit: 8 # 最多允许别人借多少 - name: memory
nominalQuota: 64Gi
borrowingLimit: 32Gi
lendingLimit: 32Gi
- name: cpu
- name: default-flavor
coveredResources: [“nvidia.com/gpu”] # 第二组:GPU 资源
flavors:- name: a100
resources:- name: nvidia.com/gpu
nominalQuota: 4
borrowingLimit: 2
- name: nvidia.com/gpu
- name: t4
resources:- name: nvidia.com/gpu
nominalQuota: 8
一句话记住:ClusterQueue 才是真正管配额和公平性的地方。 第一次看会和 LocalQueue 搞混,其实记住一句就够了——ClusterQueue 管资源,LocalQueue 管用户。
- name: nvidia.com/gpu
- name: a100
- LocalQueue:用户怎么进队列
ClusterQueue 是集群级别的,但用户不能直接往里塞 Job。中间还需要一层 LocalQueue——它是一个命名空间对象,指向一个 ClusterQueue,作为用户提交 Job 的入口。
apiVersion: kueue.x-k8s.io/v1beta2
kind: LocalQueue
metadata:
namespace: team-a
name: team-a-queue
spec:
clusterQueue: cluster-queue