はじめに
横浜で行われたKubeCon・CloudNativeCon Japan2026にて、SIG Scheduling Update:
Transition from Pod to Workload Scheduling のセッションを聞いて内容に興味を持ち、Workload-Aware Schedulingについて実際に動かしながら理解を深めてみました。
検証にあたって、以下のブログも参考にしました。
検証内容について
AI/MLワークロードやバッチ処理ワークロードには、単純なPod単位のスケジューリングでは対応できない特有のスケジューリング課題が存在することを受け、v1.36で拡張されたWorkload-Aware Schedulingに関する以下の仕組みを検証しました。
-
Pod GroupとGang Scheduling
- 複数PodをPodGroupとして扱い、minCountを配置できる場合のみ、まとめてスケジューリングできる。
- 分散学習において、Podがすべて揃わないと学習を開始できない場合に、一部のPodだけがスケジュールされ、GPUやメモリなどのリソースを占有し続ける状態を防げる。
- Pod単位の配置で発生するDeadlockを、PodGroupにおける
schedulingPolicy.gangを定義して利用可能なGang Schedulingで回避できる。
- 複数PodをPodGroupとして扱い、minCountを配置できる場合のみ、まとめてスケジューリングできる。
-
Job controller連携
- Indexed JobからWorkloadとPodGroupが自動生成され、各PodにPodGroupへの参照が設定される。
-
Workload-Aware Preemption
- 高優先度のPodGroupを配置できない場合に、低優先度のPodまたはPodGroupをvictimとして退避し、PodGroup全体の配置を試みる。
- priorityでPodGroupの優先度を表し、disruptionModeでvictim側のPodを個別に退避できるか、PodGroup全体で退避するかを指定できる。
-
Topology-Aware Scheduling
- PodGroupにTopology制約を設定し、Podを同じzoneなどのTopology Domainへまとめて配置できる。
- AI/MLトレーニングやバッチ処理といった複雑な分散ワークロードの場合においてネットワーク遅延を最小化するために有効。
検証環境の準備
Feature GateとWorkload APIの有効化
まず、Kubernetesクラスタがv1.36であることを確認します。
❯ kubectl get nodes
NAME STATUS ROLES AGE VERSION
tryu1-cp Ready control-plane 122d v1.36.2
tryu1-worker Ready <none> 122d v1.36.2
tryu2-cp Ready control-plane 122d v1.36.2
tryu2-worker Ready <none> 122d v1.36.2
tryu3-cp Ready control-plane 122d v1.36.2
tryu3-worker Ready <none> 122d v1.36.2
そして、以下のFeature Gateを有効化します。私はKubesprayを使ってクラスタ構築をしているのでKubesprayを用いて反映しました。
kube_api_runtime_config:
- scheduling.k8s.io/v1alpha2=true
kube_apiserver_feature_gates:
- GenericWorkload=true
- WorkloadWithJob=true
kube_scheduler_feature_gates:
- GenericWorkload=true
- GangScheduling=true
- WorkloadAwarePreemption=true
- TopologyAwareWorkloadScheduling=true
kube_controller_feature_gates:
- WorkloadWithJob=true
PodGroupとGang Schedulingの検証
PodGroupによりPodをまとめて配置する
複数のPodを同じグループとして扱うPodGroupを作成します。PodGroupにGang policyを設定し、Gang Schedulingを行います。
ここではPodGroupを手動で作成し、minCount: 2としています。つまり、2つのPodがスケジュール可能な状態になって初めてまとめてNodeに配置します。
Podを2種類作成し、うち一つを存在しないnodeSelectorによりUnschedulableにします。
apiVersion: scheduling.k8s.io/v1alpha2
kind: PodGroup
metadata:
name: gang-demo
namespace: was-gang-test
spec:
schedulingPolicy:
gang:
minCount: 2
---
apiVersion: v1
kind: Pod
metadata:
name: gang-ok
namespace: was-gang-test
labels:
app: gang-demo
spec:
schedulingGroup:
podGroupName: gang-demo
restartPolicy: Never
containers: # 省略
---
apiVersion: v1
kind: Pod
metadata:
name: gang-ng
namespace: was-gang-test
labels:
app: gang-demo
spec:
schedulingGroup:
podGroupName: gang-demo
nodeSelector:
was.example.com/target: unavailable
restartPolicy: Never
containers: # 省略
この時、通常であればスケジュールできるgang-ok PodもPodGroupのminCountの制限によりPendingとなることがわかります。
$ kubectl -n was-gang-test get pods -o wide
NAME READY STATUS NODE
gang-ng 0/1 Pending <none>
gang-ok 0/1 Pending <none>
$ kubectl -n was-gang-test get events --sort-by=.lastTimestamp
LAST SEEN TYPE REASON OBJECT MESSAGE
3m29s Warning FailedScheduling pod/gang-ok pod group is unschedulable, waiting for minCount pods from a gang to be scheduled, one or more plugins asked to wait and no plugin rejected pod
45m Warning FailedScheduling pod/gang-ng 0/6 nodes are available: 3 node(s) didn't match Pod's node affinity/selector, 3 node(s) had untolerated taint(s). no new claims to deallocate, preemption: 0/6 nodes are available: 6 Preemption is not helpful for scheduling.
Deadlockの再現と回避
従来、Pod単位でスケジューリングの判断を行う場合、スケジューリング時にDeadlockが発生するリスクがありました。v1.36ではスケジューラがPodGroupを単一の処理単位として評価するように設計したため、この問題を解消することができます。この様子を見てみます。
Deadlockが発生する場合
以下では、nodeAffinityにより配置先のWorker Nodeは2台に限定されます。
さらに、podAntiAffinityによって各NodeへPodを1個だけ配置できるようにしています。
このような設定のPodを4つapplyします。
apiVersion: v1
kind: Pod
metadata:
name: gang-a-1
namespace: was-deadlock-test
labels:
test: deadlock-slot
gang: a
spec:
restartPolicy: Never
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values: [tryu1-worker, tryu2-worker]
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
test: deadlock-slot
topologyKey: kubernetes.io/hostname
containers: # 省略
---
apiVersion: v1
kind: Pod
metadata:
name: gang-b-1
namespace: was-deadlock-test
labels:
test: deadlock-slot
gang: b
# 同上
---
apiVersion: v1
kind: Pod
metadata:
name: gang-a-2
namespace: was-deadlock-test
labels:
test: deadlock-slot
gang: a
# 同上
---
apiVersion: v1
kind: Pod
metadata:
name: gang-b-2
namespace: was-deadlock-test
labels:
test: deadlock-slot
gang: b
# 同上
ここでは、Podは以下の順に作成されます:
- Gang A Pod 1
- Gang B Pod 1
- Gang A Pod 2
- Gang B Pod 2
配置可能なWorkerは2台だけで、podAntiAffinityにより各Nodeへ1 Podしか配置できません。
そのため、以下のようになるはずです:
tryu1-worker: Gang A Pod 1
tryu2-worker: Gang B Pod 1
Gang A Pod 2: Pending
Gang B Pod 2: Pending
実際の様子を確認します。
AとBから1 PodずつがRunningになり、残りがPendingになることで、双方が一部の配置枠を占有して必要数2を満たせない状態になっていることがわかります。
これでは両方のgangが動作せずDeadlockのような状態になってしまいます。
❯ kubectl -n was-deadlock-test get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
gang-a-1 1/1 Running 0 10s 10.233.67.229 tryu2-worker <none> <none>
gang-a-2 0/1 Pending 0 9s <none> <none> <none> <none>
gang-b-1 1/1 Running 0 10s 10.233.65.81 tryu1-worker <none> <none>
gang-b-2 0/1 Pending 0 9s <none> <none> <none> <none>
❯ kubectl -n was-deadlock-test get events --sort-by=.lastTimestamp
LAST SEEN TYPE REASON OBJECT MESSAGE
34s Normal Scheduled pod/gang-a-1 Successfully assigned was-deadlock-test/gang-a-1 to tryu2-worker
34s Warning FailedScheduling pod/gang-a-2 0/6 nodes are available: 1 node(s) didn't match Pod's node affinity/selector, 2 node(s) didn't match pod anti-affinity rules, 3 node(s) had untolerated taint(s). no new claims to deallocate, preemption: 0/6 nodes are available: 2 No preemption victims found for incoming pod, 4 Preemption is not helpful for scheduling.
この様子を図で表すと、以下のようになります:
Gang SchedulingによるDeadlockの回避
では、PodGroupを用いてDeadlockを回避する様子を見てみます。先ほどと同じように、Pod定義をA1 → B1 → A2 → B2の順にしています。
apiVersion: scheduling.k8s.io/v1alpha2
kind: PodGroup
metadata:
name: gang-a
namespace: was-deadlock-test
spec:
schedulingPolicy:
gang:
minCount: 2
---
apiVersion: scheduling.k8s.io/v1alpha2
kind: PodGroup
metadata:
name: gang-b
namespace: was-deadlock-test
spec:
schedulingPolicy:
gang:
minCount: 2
---
apiVersion: v1
kind: Pod
metadata:
name: gang-a-1
namespace: was-deadlock-test
labels:
test: deadlock-slot
gang: a
spec:
schedulingGroup:
podGroupName: gang-a
restartPolicy: Never
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values: [tryu1-worker, tryu2-worker]
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
test: deadlock-slot
topologyKey: kubernetes.io/hostname
containers: # 省略
---
apiVersion: v1
kind: Pod
metadata:
name: gang-b-1
namespace: was-deadlock-test
labels:
test: deadlock-slot
gang: b
spec:
schedulingGroup:
podGroupName: gang-b
# 同上
---
apiVersion: v1
kind: Pod
metadata:
name: gang-a-2
namespace: was-deadlock-test
labels:
test: deadlock-slot
gang: a
spec:
schedulingGroup:
podGroupName: gang-a
# 同上
---
apiVersion: v1
kind: Pod
metadata:
name: gang-b-2
namespace: was-deadlock-test
labels:
test: deadlock-slot
gang: b
spec:
schedulingGroup:
podGroupName: gang-b
# 同上
今回はgang-aがうまくいっていることがわかります。
以下の順にPodを作成しているものの、Gang B Pod 1 はPendingでGang A Pod 2 がRunningになっています。これは、gang-a PodGroupをスケジュールできるようにするためです。
- Gang A Pod 1
- Gang B Pod 1
- Gang A Pod 2
- Gang B Pod 2
gang-a PodGroupの2 PodがまとめてRunningになり、もう一方のgang-b PodGroupの2 PodがまとめてPendingになっており、複数gangが配置枠を分け合って双方とも必要数を満たせなくなる状態を回避できています。
❯ kubectl -n was-deadlock-test get podgroups,pods -o wide
NAME POLICY WORKLOAD STATUS AGE
podgroup.scheduling.k8s.io/gang-a Gang <none> Scheduled 7s
podgroup.scheduling.k8s.io/gang-b Gang <none> Unschedulable 7s
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
pod/gang-a-1 1/1 Running 0 6s 10.233.65.82 tryu1-worker <none> <none>
pod/gang-a-2 1/1 Running 0 6s 10.233.67.230 tryu2-worker <none> <none>
pod/gang-b-1 0/1 Pending 0 6s <none> <none> <none> <none>
pod/gang-b-2 0/1 Pending 0 6s <none> <none> <none> <none>
❯ kubectl -n was-deadlock-test get events --sort-by=.lastTimestamp
LAST SEEN TYPE REASON OBJECT MESSAGE
32s Normal Scheduled pod/gang-a-1 Successfully assigned was-deadlock-test/gang-a-1 to tryu1-worker
32s Normal Scheduled pod/gang-a-2 Successfully assigned was-deadlock-test/gang-a-2 to tryu2-worker
32s Warning FailedScheduling pod/gang-b-1 0/6 nodes are available: 1 node(s) didn't match Pod's node affinity/selector, 2 node(s) didn't match pod anti-affinity rules, 3 node(s) had untolerated taint(s). no new claims to deallocate, preemption: 0/6 nodes are available: 2 No preemption victims found for incoming pod, 4 Preemption is not helpful for scheduling.
Indexed Jobとの連携
WorkloadWithJobが有効であれば、次のIndexed JobからWorkloadとPodGroupが自動作成されます。
手動でPodGroupを作ったりPodからPodGroupを参照するといった記述から解放されます。
apiVersion: batch/v1
kind: Job
metadata:
name: indexed-gang
namespace: was-gang-test
spec:
completionMode: Indexed
parallelism: 2
completions: 2
backoffLimit: 0
template:
spec:
restartPolicy: Never
containers:
- name: worker
image: registry.k8s.io/e2e-test-images/agnhost:2.53
args: ["pause"]
resources:
requests:
cpu: 10m
memory: 16Mi
2 PodにschedulingGroupが設定され、Job所有のWorkloadとPodGroupが生成されることを確認できます。
$ kubectl -n was-gang-test get jobs,pods,workloads,podgroups
NAME STATUS COMPLETIONS DURATION AGE
job.batch/indexed-gang Running 0/2 90s 90s
NAME READY STATUS RESTARTS AGE
pod/indexed-gang-0-477ms 1/1 Running 0 90s
pod/indexed-gang-1-zfwdj 1/1 Running 0 89s
NAME AGE
workload.scheduling.k8s.io/indexed-gang-68f4bc9659 90s
NAME POLICY WORKLOAD STATUS AGE
podgroup.scheduling.k8s.io/indexed-gang-68f4bc9659-indexed-gang-pgt-0-6764d77859 Gang indexed-gang-68f4bc9659 Scheduled 90s
Workload単位のスケジューリング制御
Workload-Aware Preemption
優先度の高いPodGroupがスケジュールできるようにするために、優先度の低いPodGroupをPreemptする様子を確認します。
まず、優先度の低いPriorityClassでPodGroupを作成します。
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: was-preemption-low
value: 1000
globalDefault: false
description: Low priority used only by the WAS preemption test.
---
apiVersion: scheduling.k8s.io/v1alpha2
kind: PodGroup
metadata:
name: victim
namespace: was-preemption-test
spec:
schedulingPolicy:
gang:
minCount: 2
priorityClassName: was-preemption-low
disruptionMode: PodGroup
---
apiVersion: v1
kind: Pod
metadata:
name: victim-1
namespace: was-preemption-test
labels:
app: was-preemption-victim
spec:
priorityClassName: was-preemption-low
schedulingGroup:
podGroupName: victim
restartPolicy: Never
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values: [tryu1-worker, tryu2-worker]
containers:
- name: pause
image: registry.k8s.io/pause:3.10
ports:
- name: exclusive-test
containerPort: 31991
hostPort: 31991
resources:
requests:
cpu: 10m
memory: 16Mi
---
apiVersion: v1
kind: Pod
metadata:
name: victim-2
namespace: was-preemption-test
labels:
app: was-preemption-victim
spec:
priorityClassName: was-preemption-low
schedulingGroup:
podGroupName: victim
restartPolicy: Never
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values: [tryu1-worker, tryu2-worker]
containers:
- name: pause
image: registry.k8s.io/pause:3.10
ports:
- name: exclusive-test
containerPort: 31991
hostPort: 31991
resources:
requests:
cpu: 10m
memory: 16Mi
優先度の低いvictim PodGroupの様子です:
❯ kubectl -n was-preemption-test get podgroups,pods -o wide
NAME POLICY WORKLOAD STATUS AGE
podgroup.scheduling.k8s.io/victim Gang <none> Scheduled 26s
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
pod/victim-1 1/1 Running 0 27s 10.233.65.84 tryu1-worker <none> <none>
pod/victim-2 1/1 Running 0 27s 10.233.67.232 tryu2-worker <none> <none>
次に、高い優先度のPriorityClassでPodGroupを作成します。
低優先度のvictim Podは、配置対象の2台のWorker NodeでそれぞれhostPort: 31991を使用しています。高優先度のpreemptor Podも同じhostPortを要求するため、そのままでは配置できません。
そこで、Workload-Aware Preemptionにより低優先度のvictimを退避し、preemptorを配置できるか確認します。
通常のPod単位のpreemptionでは、配置に必要なvictim Podだけが退避対象になります。
しかし、victim PodGroupにはdisruptionMode: PodGroupを指定しているため、preemptorが必要とする空きが1 Pod分だけでも、victim PodGroup全体がまとめて退避されます。
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: was-preemption-high
value: 10000
globalDefault: false
description: High priority used only by the WAS preemption test.
---
apiVersion: scheduling.k8s.io/v1alpha2
kind: PodGroup
metadata:
name: preemptor
namespace: was-preemption-test
spec:
schedulingPolicy:
gang:
minCount: 1
priorityClassName: was-preemption-high
disruptionMode: Pod
---
apiVersion: v1
kind: Pod
metadata:
name: preemptor-1
namespace: was-preemption-test
labels:
app: was-preemption-preemptor
spec:
priorityClassName: was-preemption-high
schedulingGroup:
podGroupName: preemptor
restartPolicy: Never
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values: [tryu1-worker, tryu2-worker]
containers:
- name: pause
image: registry.k8s.io/pause:3.10
ports:
- name: exclusive-test
containerPort: 31991
hostPort: 31991
resources:
requests:
cpu: 10m
memory: 16Mi
preemptorを適用した後の様子です:
❯ kubectl -n was-preemption-test get podgroups,pods -o wide
NAME POLICY WORKLOAD STATUS AGE
podgroup.scheduling.k8s.io/preemptor Gang <none> Scheduled 8s
podgroup.scheduling.k8s.io/victim Gang <none> Scheduled 23s
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
pod/preemptor-1 1/1 Running 0 8s 10.233.67.237 tryu2-worker <none> <none>
高優先度のpreemptor-1を配置するために低優先度victimがpreemptionされていることがわかります。victimのdisruptionMode: PodGroupにより、高優先度Podが必要とする枠が1つだけでもvictim PodGroup全体がpreemption対象となっています。
Topology-Aware Scheduling
同じPodGroupのPodを、同じZoneやRackといったtopology domainへまとめて配置できることを確認します。
まず、Worker Nodeへ検証用zoneラベルを設定します。
kubectl label node tryu1-worker topology.kubernetes.io/zone=zone-a --overwrite
kubectl label node tryu2-worker topology.kubernetes.io/zone=zone-a --overwrite
kubectl label node tryu3-worker topology.kubernetes.io/zone=zone-b --overwrite
❯ kubectl get nodes -L topology.kubernetes.io/zone
NAME STATUS ROLES AGE VERSION ZONE
tryu1-cp Ready control-plane 121d v1.36.2
tryu1-worker Ready <none> 121d v1.36.2 zone-a
tryu2-cp Ready control-plane 121d v1.36.2
tryu2-worker Ready <none> 121d v1.36.2 zone-a
tryu3-cp Ready control-plane 121d v1.36.2
tryu3-worker Ready <none> 121d v1.36.2 zone-b
以下を適用し、PodGroup内のPodが同じZoneに配置されるかを確認します。
apiVersion: scheduling.k8s.io/v1alpha2
kind: PodGroup
metadata:
name: same-zone
namespace: was-topology-test
spec:
schedulingPolicy:
gang:
minCount: 2
schedulingConstraints:
topology:
- key: topology.kubernetes.io/zone
---
apiVersion: v1
kind: Pod
metadata:
name: same-zone-1
namespace: was-topology-test
spec:
schedulingGroup:
podGroupName: same-zone
restartPolicy: Never
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values: [tryu1-worker, tryu2-worker, tryu3-worker]
containers: # 省略
---
apiVersion: v1
kind: Pod
metadata:
name: same-zone-2
namespace: was-topology-test
spec:
schedulingGroup:
podGroupName: same-zone
restartPolicy: Never
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values: [tryu1-worker, tryu2-worker, tryu3-worker]
containers: # 省略
確認してみると、2 Podともzone-bのtryu3-workerへ配置され、PodGroupがScheduledとなっていることがわかりました。
❯ kubectl apply -f k8s/sig-scheduling/topology-aware.yaml
❯ kubectl -n was-topology-test get podgroups,pods -o wide
NAME POLICY WORKLOAD STATUS AGE
podgroup.scheduling.k8s.io/same-zone Gang <none> Scheduled 28m
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
pod/same-zone-1 1/1 Running 0 28m 10.233.69.198 tryu3-worker <none> <none>
pod/same-zone-2 1/1 Running 0 28m 10.233.69.199 tryu3-worker <none> <none>
まとめ
v1.36で拡張されたWorkload-Aware Schedulingの機能を動かしながら試してみました。セッションを聴いているだけだと分かりづらいような箇所も、実際に動かしてみると理解が深まりました。
