Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Kueueアーキテクチャ/Kueue Architecture

Kueueアーキテクチャ/Kueue Architecture

参考リンク一覧:
コードリーディングメモ: https://zenn.dev/bells17/scraps/16625963e51d23
動作確認用manifests: https://github.com/bells17/tmp/tree/main/kueue-example
リポジトリ: https://github.com/kubernetes-sigs/kueue/tree/v0.1.0
Design Docs(controller): https://bit.ly/kueue-controller-design
Design Docs(API): https://bit.ly/kueue-apis
Old Proposal: https://bit.ly/k8s-job-management

---

https://youtu.be/CFUfw3cMNI8?t=724
にてこのスライドを使ったKueueの解説セッションを行いましたので動画で見たい方はこちらでどうぞ

bells17

May 24, 2022
Tweet

More Decks by bells17

Other Decks in Programming

Transcript

  1. ▶ @bells17 ▶ Software Engineer ▶ 普段やってること: + Kubernetes 関連コンポーネントの開発

    + Kubernetes as a Service開発 ▶ Kubernetes SIG-Docs Japanese localization reviewer ▶ Kubernetes Internal Organizer ▶ #kubenews ▶ @bells17_
  2. 注意点 ▶ 対象バージョン: Kueue v0.1.0 + https://github.com/kubernetes-sigs/kueue/tree/v0.1.0 ▶ あくまでKueueの実装を追った結果での理解の説明になるので、 間違いが含まれている可能性があります

    ▶ 引⽤しているコードはスライドにうまく収めるため省略、⼀部書き換えを ⾏っている箇所があります ▶ コードリーディングメモ: https://zenn.dev/bells17/scraps/16625963e51d23
  3. Kueue ▶ 以下のようなポイントはKubernetes⾃⾝の仕組みを使いつつ実現している + Jobリソースのマネジメントはkube-controller-managerを利⽤ + Podのスケジューリングはkube-schedulerを利⽤ ▶ Volcano(kube-batch)のJob API、ジョブライフサイクル管理、スケジューラー

    といったコアKubernetesにすでに存在する多くの機能を 再実装している点を問題に感じたのがKueueを作成した背景の1つのよう ▶ ドキュメントなどから主なユースケースとしてMLワークロードのような ⼤規模バッチを多数同時並列で実⾏したいようなユースケースにおいて バッチの実⾏順序のコントロールやキューイングなどを⾏うため利⽤する想定? + ここらへん詳しい⼈教えてください (Design Docs読んでもなんとなくしか想像できなかった)
  4. Kueue ▶ Kubernetesコミュニティの #wg-batch により開発されているよう ▶ 2022/4/12にv0.1.0がリリースされたばかり ▶ リポジトリ: https://github.com/kubernetes-sigs/kueue

    ▶ Design Docs(controller): https://bit.ly/kueue-controller-design ▶ Design Docs(API): https://bit.ly/kueue-apis ▶ Old Proposal: https://bit.ly/k8s-job-management
  5. Kueueに必要なカスタムリソースを設定 DBU&04TFUUJOHZBNM BQJ7FSTJPOLVFVFYLTJPWBMQIB LJOE3FTPVSDF'MBWPS NFUBEBUB OBNFEFGBVMU  BQJ7FSTJPOLVFVFYLTJPWBMQIB LJOE2VFVF NFUBEBUB

    OBNFTQBDFEFGBVMU OBNFNBJO TQFD DMVTUFS2VFVFDMVTUFSUPUBM &04 DBU&04TFUUJOHZBNM  BQJ7FSTJPOLVFVFYLTJPWBMQIB LJOE$MVTUFS2VFVF NFUBEBUB OBNFDMVTUFSUPUBM TQFD OBNFTQBDF4FMFDUPS\^NBUDIBMM SFTPVSDFT OBNFDQV qBWPST OBNFEFGBVMU RVPUB NJO &04 Ϧιʔε࡞੒ LVFCDUMBQQMZa GTFUUJOHTZBNM
  6. Jobを作成 DBU&04KPCZBNM  BQJ7FSTJPOCBUDIW LJOE+PC NFUBEBUB HFOFSBUF/BNFTBNQMFKPC BOOPUBUJPOT LVFVFYLTJPRVFVFOBNFNBJORVFVF໊Λࢦఆ TQFD

    QBSBMMFMJTN DPNQMFUJPOT TVTQFOEUSVFUSVFʹ͢Δ͜ͱͰ,VFVFͷॲཧର৅ʹ UFNQMBUF TQFD DPOUBJOFST OBNFEVNNZKPC JNBHFHDSJPLTTUBHJOHQFSGUFTUTTMFFQMBUFTU BSHT<T> SFTPVSDFT SFRVFTUT DQVl SFTUBSU1PMJDZ/FWFS &04 +PC࡞੒ LVCFDUMDSFBUFGKPCZBNM
  7. 少し時間を置いてからJobを確認する LVCFDUMHFUKPCPZBNM BQJ7FSTJPOW JUFNT BQJ7FSTJPOCBUDIW LJOE+PC NFUBEBUB BOOPUBUJPOT LVFVFYLTJPRVFVFOBNFNBJO HFOFSBUF/BNFTBNQMFKPC

    MBCFMT DPOUSPMMFSVJEGDFDBBGBFGD KPCOBNFTBNQMFKPCMK OBNFTBNQMFKPCMK OBNFTQBDFEFGBVMU ʜ TQFDTVTQFOEUSVF͕ফ͑ͯΔ CBDLP⒎-JNJU DPNQMFUJPOT QBSBMMFMJTN TFMFDUPS NBUDI-BCFMT DPOUSPMMFSVJEGDFDBBGBFGD ʜ
  8. デモまとめ ▶ Kueueをインストールする ▶ ResourceFlavor/ClusterQueue/QueueリソースをセットアップするとKueueを利 ⽤する準備が完了 ▶ Jobリソースを以下のように設定して作成するとKueueがJobの スケジューリングを管理 ▶

    “kueue.x-k8s.io/queue-name”アノテーションで利⽤するQueueを指定 ▶ .spec.suspend: true に設定してJobを作成 ▶ 後はKueueがClusterQueueなどの設定に応じてJobをスケジューリング (Jobの.spec.suspendをfalseに変更する) ▶ 以下のような仕組みはKubernetesの機能をそのまま利⽤ ▶ Job→Podリソースの作成 ▶ NodeへのPodのスケジューリング
  9. Kueue Custom Resource ▶ Queue: Jobリソースから利⽤するQueue ▶ Jobリソースから指定するためのClusterQueueのエイリアスになっている ▶ ClusterQueue:

    Kueueが提供するキューの実態となるリソース ▶ ResourceFlavor: ClusterQueueのflavor定義 ▶ Workload: KueueでJobの状態管理を⾏うためのリソース ▶ Kueue内部の処理に利⽤されるのみでユーザーが触ることは基本的に無い
  10. Queue Resource type ClusterQueueReference string type QueueSpec struct { ClusterQueue

    ClusterQueueReference `json:"clusterQueue,omitempty"` } type QueueStatus struct { PendingWorkloads int32 `json:"pendingWorkloads"` } type Queue struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec QueueSpec `json:"spec,omitempty"` Status QueueStatus `json:"status,omitempty"` }
  11. ClusterQueue Resource type ClusterQueueSpec struct { Resources []Resource `json:"resources,omitempty"` Cohort

    string `json:"cohort,omitempty"` QueueingStrategy QueueingStrategy `json:"queueingStrategy,omitempty"` NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector,omitempty"` } type Resource struct { Name corev1.ResourceName `json:"name"` Flavors []Flavor `json:"flavors,omitempty"` } type Flavor struct { Name ResourceFlavorReference `json:"name"` Quota Quota `json:"quota"` } type Quota struct { Min resource.Quantity `json:"min,omitempty"` Max *resource.Quantity `json:"max,omitempty"` } type QueueingStrategy string type ResourceFlavorReference string
  12. ClusterQueue Resource type ClusterQueueStatus struct { UsedResources UsedResources `json:"usedResources"` PendingWorkloads

    int32 `json:"pendingWorkloads"` AdmittedWorkloads int32 `json:"admittedWorkloads"` } // map[<resource type>]map[<flavor name>]Usage type UsedResources map[corev1.ResourceName]map[string]Usage type Usage struct { Total *resource.Quantity `json:"total,omitempty"` Borrowed *resource.Quantity `json:"borrowing,omitempty"` } type ClusterQueue struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec ClusterQueueSpec `json:"spec,omitempty"` Status ClusterQueueStatus `json:"status,omitempty"` }
  13. ResourceFlavor Resource type ResourceFlavor struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"`

    // labels associated with this flavor. They are matched against or // converted to node affinity constraints on the workload’s pods. // For example, cloud.provider.com/accelerator: nvidia-tesla-k80. Labels map[string]string `json:"labels,omitempty"` // taints associated with this flavor that workloads must explicitly // “tolerate” to be able to use this flavor. // For example, cloud.provider.com/preemptible="true":NoSchedule Taints []corev1.Taint `json:"taints,omitempty"` }
  14. NamespaceSelector ▶ ClusterQueue.spec.namespaceSelectorを設定することでClusterQueueリソースは QueueリソースがClusterQueueリソースを利⽤可能なNamespaceを制限することが 可能 ▶ `.spec.NamespaceSelector: {}`: 全てのNamespaceがマッチする ▶

    `.spec.NamespaceSelector: nill`: どのNamespaceにもマッチしない ▶ `.spec.NamespaceSelector: { “team”: “team-a" }`: `{ “team”: “team-a" }` の ラベルが設定されているNamespaceからのみ利⽤可能
  15. NamespaceSelector apiVersion: kueue.x-k8s.io/v1alpha1 kind: ClusterQueue metadata: name: ns-selector-sample spec: namespaceSelector:

    "team": "team-a" apiVersion: v1 kind: Namespace metadata: name: team-a labels: team: team-a Match apiVersion: kueue.x-k8s.io/v1alpha1 kind: Queue metadata: namespace: team-a name: main spec: clusterQueue: ns-selector-sample
  16. ResourceとResourceFlavor apiVersion: kueue.x-k8s.io/v1alpha1 kind: ClusterQueue metadata: name: cluster-queue spec: namespaceSelector:

    {} resources: - name: cpu flavors: - name: c2-on-demand min: 1000 - name: c4-on-demand min: 1000 apiVersion: kueue.x-k8s.io/v1alpha1 kind: ResourceFlavor metadata: name: c2-on-demand labels: cloud.provider.com/vm-family: c2 apiVersion: kueue.x-k8s.io/v1alpha1 kind: ResourceFlavor metadata: name: c4-on-demand labels: cloud.provider.com/vm-family: c4 上記の例の場合、”cluster-queue" キューはCPUリソースについて 1. c2-on-demand 2. c4-on-demand の2種類のflavorが設定されており、各flavorごとに1000CPU(計2000CPU)リソースを利⽤することができる、という設定になる Jobの設定がResourceFlavorの • labels • taints 条件にマッチするflavorが利⽤される
  17. ResourceとResourceFlavor ▶ ClusterQueueのリソース容量はリソース種類 * flavorで設定される ▶ ResourceFlavorは ▶ Taints ▶

    Labels にフィールドによってJobとマッチするflavorを設定することができる
  18. Cohort apiVersion: kueue.x-k8s.io/v1alpha1 kind: ClusterQueue metadata: name: tenantA spec: cohort:

    borrowing-cohort resources: - name: nvidia.com/gpus flavors: - name: k80 quota: min: 10 max: 20 apiVersion: kueue.x-k8s.io/v1alpha1 kind: ClusterQueue metadata: name: tenantB spec: cohort: borrowing-cohort resources: - name: nvidia.com/gpus flavors: - name: k80 quota: min: 10 max: 20 下記の例の場合 `nvidia.com/gpus` リソースをClusterQueue単体だと最⼤で10までし か同時に利⽤できないが、Cohort機能で借りることで最⼤20まで同時に利⽤すること ができるようになる
  19. QueueingStrategy 現在Kueueでは以下の2種類のキューイング戦略を選択することが可能 デフォルト: BestEffortFIFO ▶ StrictFIFO: 優先度 > キューイング時刻の順にソートされる ▶

    (ClusterQueueのリソース不⾜などにより)実⾏が許可されないJobがある場合: ▶ このJobよりも後からキューイングされたJobが使⽤可能なクォータに収まっている場合 でも、後からキューイングされたJobをブロックしてJobが実⾏可能になるまで待機する ▶ 優先度は `job.Spec.Template.Spec.PriorityClassName` で指定された `PriorityClass` に 設定される値により決定される ▶ BestEffortFIFO: ソート順はStrictFIFOと同様 ▶ しかし実⾏が許可されないJobがある場合は使⽤可能なクォータに収まっている場合他の Jobの実⾏をブロックしない
  20. QueueingStrategy apiVersion: kueue.x-k8s.io/v1alpha1 kind: ClusterQueue metadata: name: queue-strict spec: namespaceSelector:

    {} queueingStrategy: "StrictFIFO" resources: - name: cpu flavors: - name: default min: 50 apiVersion: batch/v1 kind: Job metadata: generateName: sample-job- annotations: kueue.x-k8s.io/queue-name: main spec: parallelism: 1 completions: 1 suspend: true template: spec: priorityClassName: high-priority containers: - name: dummy-job image: sleep:latest args: ["15s"] resources: requests: cpu: “2" apiVersion: scheduling.k8s.io/v1 kind: PriorityClass metadata: name: high-priority value: 1000000 Queue
  21. Kueueはcontroller-runtime(v0.11.1)によって実装されてる func main() { mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), options) #

    managerを⽣成 if err != nil { setupLog.Error(err, "unable to start manager") os.Exit(1) } queues := queue.NewManager(mgr.GetClient()) // ClusterQueueを処理するQueueManagerを⽣成 cCache := cache.New(mgr.GetClient()) // 計算⽤のリソース使⽤量を管理するCacheManagerを⽣成 if failedCtrl, err := core.SetupControllers(mgr, queues, cCache); err != nil { // 各種Controllerを登録 setupLog.Error(err, "Unable to create controller", "controller", failedCtrl) } if err = job.NewReconciler(mgr.GetScheme(), // Job Controllerを登録 mgr.GetClient(), mgr.GetEventRecorderFor(constants.JobControllerName), job.WithManageJobsWithoutQueueName(config.ManageJobsWithoutQueueName), ).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Job") os.Exit(1) } ctx := ctrl.SetupSignalHandler() go func() { // Queueのクリーンアップ処理を⾏うgo routineを起動 queues.CleanUpOnContext(ctx) }() sched := scheduler.New(queues, cCache, mgr.GetClient(), // スケジューラーを⽣成〜起動 mgr.GetEventRecorderFor(constants.ManagerName)) go func() { sched.Start(ctx) }() ... } https://github.com/kubernetes-sigs/kueue/blob/v0.1.0/main.go
  22. controller-runtime ▶ controller-runtimeはKubernetes Operator を実装するためのフレームワーク ▶ ControllerやCRDの他Webhookの実装も可能 ▶ controller-runtimeについては以前 ”作って学ぶkubebuilder⼊⾨”というタイトル

    でLTをしているので興味があればそちらを参照 してください ▶ https://speakerdeck.com/bells17/ kubebuilder-introduction https://github.com/kubernetes-sigs/kubebuilder/blob/master/docs/book/src/kb_concept_diagram.svg
  23. Kueueの各CRDのController実装では通常EventFilteringを⾏う箇所を利⽤して キャッシュの更新やキューの作成処理を⾏っている func (r *ClusterQueueReconciler) Create(e event.CreateEvent) bool { cq,

    match := e.Object.(*kueue.ClusterQueue) if !match { // No need to interact with the cache for other objects. return true } log := r.log.WithValues("clusterQueue", klog.KObj(cq)) log.V(2).Info("ClusterQueue create event") ctx := ctrl.LoggerInto(context.Background(), log) if err := r.cache.AddClusterQueue(ctx, cq); err != nil { // ClusterQueueをキャッシュに追加 log.Error(err, "Failed to add clusterQueue to cache") } if err := r.qManager.AddClusterQueue(ctx, cq); err != nil { // ClusterQueueをQueueManagerに追加 log.Error(err, "Failed to add clusterQueue to queue manager") } return true } func (r *ClusterQueueReconciler) SetupWithManager(mgr ctrl.Manager) error { wHandler := cqWorkloadHandler{ qManager: r.qManager, } return ctrl.NewControllerManagedBy(mgr). For(&kueue.ClusterQueue{}). Watches(&source.Channel{Source: r.wlUpdateCh}, &wHandler). WithEventFilter(r). // 通常イベントをフィルタリングする⽤途で使⽤するEventFilterを利⽤したキャッシュとキューの更新処理 Complete(r) } https://github.com/kubernetes-sigs/kueue/blob/v0.1.0/pkg/controller/core/clusterqueue_controller.go
  24. Job作成時に起きていること 1. Jobを作成 3. WorkloadControllerが キャッシュのアップデートや QueueManager内の対象と なるQueueにジョブを追加 を⾏う 2.

    JobControllerがJob作成を トリガーにWorkloadリソースを⽣成 4. SchedulerがQueue からジョブを取り出し admit可能かを検証〜OK ならadmin処理を⾏う 5. JobControllerが workloadがadmitされた ことをトリガーにJobを unsuspendする
  25. Queueのジョブソートには”container/heap”が利⽤されている func (c *ClusterQueueImpl) pushIfNotPresent(info *workload.Info) bool { item :=

    c.heap.items[workload.Key(info.Obj)] if item != nil { return false } heap.Push(&c.heap, *info) return true } func (c *ClusterQueueImpl) PushOrUpdate(w *kueue.Workload) { item := c.heap.items[workload.Key(w)] info := *workload.NewInfo(w) if item == nil { heap.Push(&c.heap, info) return } item.obj = info heap.Fix(&c.heap, item.index) } func (c *ClusterQueueImpl) Delete(w *kueue.Workload) { item := c.heap.items[workload.Key(w)] if item != nil { heap.Remove(&c.heap, item.index) } } https://github.com/kubernetes-sigs/kueue/blob/v0.1.0/pkg/queue/cluster_queue_impl.go
  26. ”container/heap”の実装例 type IntHeap []int func (h IntHeap) Len() int {

    return len(h) } func (h IntHeap) Less(i, j int) bool { return h[i] < h[j] } func (h IntHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } func (h *IntHeap) Push(x any) { *h = append(*h, x.(int)) } func (h *IntHeap) Pop() any { old := *h n := len(old) x := old[n-1] *h = old[0 : n-1] return x } func main() { h := &IntHeap{2, 1, 5} heap.Init(h) heap.Push(h, 3) fmt.Printf("minimum: %d\n", (*h)[0]) for h.Len() > 0 { fmt.Printf("%d ", heap.Pop(h)) } } https://pkg.go.dev/container/heap Output: minimum: 1 1 2 3 5
  27. schedulerは以下のようにしてWorkload(Job)がadmin可能かを検証~ OKの場合にadmitを実⾏する func (s *Scheduler) schedule(ctx context.Context) { headWorkloads :=

    s.queues.Heads(ctx) // 1. Queueからworkloadを取得 snapshot := s.cache.Snapshot() // 2. キャッシュからsnaoshotを⽣成 entries := s.nominate(ctx, headWorkloads, snapshot) // 3. admit可能かどうかを検証 sort.Sort(entryOrdering(entries)) // 4. cohortとタイムスタンプに基づいてソート // 5. 対象となるentryに対してadmitを実⾏ for i := range entries { e := &entries[i] ... if err := s.admit(ctrl.LoggerInto(ctx, log), e); err == nil { e.status = assumed } else { e.inadmissibleReason = fmt.Sprintf("Failed to admit workload: %v", err) } ... } // 6. adminされなかったentryをrequeue for _, e := range entries { if e.status != assumed { s.requeueAndUpdate(log, ctx, e) } } } https://github.com/kubernetes-sigs/kueue/blob/v0.1.0/pkg/scheduler/scheduler.go
  28. JobControllerによるWorkload作成~Jobのsuspend/unsuspend処理の実装 func (r *JobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error)

    { ... // workloadが無くてjobが完了した訳ではないのであればworkloadを作成 jobFinishedCond, jobFinished := jobFinishedCondition(&job) if wl == nil { if jobFinished { return ctrl.Result{}, nil } err := r.handleJobWithNoWorkload(ctx, &job) return ctrl.Result{}, err } // admitされていればJobをunsuspendにする if jobSuspended(&job) { if wl.Spec.Admission != nil { err := r.startJob(ctx, wl, &job) return ctrl.Result{}, err } ... return ctrl.Result{}, nil } // admitされて無いにも関わらずJobが動作していればJobをsuspendにする if wl.Spec.Admission == nil { err := r.stopJob(ctx, wl, &job, "Not admitted by cluster queue") return ctrl.Result{}, err } ... return ctrl.Result{}, nil } https://github.com/kubernetes-sigs/kueue/blob/v0.1.0/pkg/controller/workload/job/job_controller.go
  29. [controller-runtime] contextを通してloggerオブジェクトをやり取りすることができる func (s *Scheduler) Start(ctx context.Context) { log :=

    ctrl.LoggerFrom(ctx).WithName("scheduler") ctx = ctrl.LoggerInto(ctx, log) wait.UntilWithContext(ctx, s.schedule, 0) } func (s *Scheduler) schedule(ctx context.Context) { log := ctrl.LoggerFrom(ctx) ... } https://github.com/kubernetes-sigs/kueue/blob/v0.1.0/pkg/scheduler/scheduler.go
  30. [controller-runtime] option設定をコマンドラインオプション+設定ファイルで⾏う⽅法 func main() { var configFile string flag.StringVar(&configFile, "config",

    "", "The controller will load its initial configuration from this file. "+ "Omit this flag to use the default configuration values. ") options := ctrl.Options{ Scheme: scheme, HealthProbeBindAddress: ":8081", MetricsBindAddress: ":8080", Port: 9443, LeaderElectionID: "c1f6bfd2.kueue.x-k8s.io", } var err error config := configv1alpha1.Configuration{} if configFile != "" { options, err = options.AndFrom(ctrl.ConfigFile().AtPath(configFile).OfKind(&config)) if err != nil { setupLog.Error(err, "unable to load the config file") os.Exit(1) } cfgStr, err := encodeConfig(&config) if err != nil { setupLog.Error(err, "unable to encode config file") os.Exit(1) } setupLog.Info("Successfully loaded config file", "config", cfgStr) } ... } https://github.com/kubernetes-sigs/kueue/blob/v0.1.0/main.go
  31. [controller-runtime] 設定ファイル定義 type Configuration struct { metav1.TypeMeta `json:",inline"` // ControllerManagerConfigurationSpec

    returns the configurations for controllers cfg.ControllerManagerConfigurationSpec `json:",inline"` // ManageJobsWithoutQueueName controls whether or not Kueue reconciles // batch/v1.Jobs that don't set the annotation kueue.x-k8s.io/queue-name. // If set to true, then those jobs will be suspended and never started unless // they are assigned a queue and eventually admitted. This also applies to // jobs created before starting the kueue controller. // Defaults to false; therefore, those jobs are not managed and if they are created // unsuspended, they will start immediately. ManageJobsWithoutQueueName bool `json:"manageJobsWithoutQueueName"` } func init() { SchemeBuilder.Register(&Configuration{}) } https://github.com/kubernetes-sigs/kueue/blob/v0.1.0/apis/config/v1alpha1/configuration_types.go
  32. [controller-runtime] intやboolなどをpointer値で設定するためのutility package pointer import ( "k8s.io/apimachinery/pkg/api/resource" "k8s.io/utils/pointer" ) func

    Quantity(q resource.Quantity) *resource.Quantity { return &q } var ( Int32 = pointer.Int32 Int64 = pointer.Int64 Bool = pointer.Bool ) https://github.com/kubernetes-sigs/kueue/blob/v0.1.0/pkg/util/pointer/pointer.go