🔌

[Kubernetes 1.27] Dynamic Resource Allocation のいま

2023/05/06に公開

はじめに

KubeCon Europe 2023 で KEP-3063 Dynamic Resource Allocation (DRA) についての深い話と DRA Resource Driver の実装方法の話があったので、kubernetes-sigs/dra-example-driver をベースに触りながら検証してみました。toVersus/fake-dra-driver で公開しています。

DRA ベースの Resource Driver も既にあり、DRA と共に絶賛開発中です。

GitHub リポジトリの README や KubeCon のスライドに、DRA の仕組みを使ったデモ動画が紹介されています。

  • NVIDIA GPU に Multi-instance GPU (MIG) パーティションを動的に払い出してそれぞれのコンテナに割り当てる (video)
  • NVIDIA GPU に MIG パーティションを動的に払い出して、TS (Time-slicing) と MPS (Multi-Process Service) を使って複数のコンテナで共有する (video)
  • Intel GPU に Virtual Function のパーティションを動的に払い出し、コンテナが専用のメモリ領域を使えるようにするデモで、コンテナランタイムとして cri-o を使用 (video)

他にも Pod の network namespace に複数の NIC をアタッチして、高速な通信を専用の NIC を通して行う KEP-3698 Multi-Network Requirements もネットワークデバイスの動的な管理や検出として DRA に興味を持っている (slack) ようです。

Device Plugin とは

DRA は Device Plugin v2 と呼ばれていますが、そもそも Device Plugin とは何でしょうか。

KEP-3573 Device Plugin は、Kubernetes に早い段階から入っていた機能で、GPU や DPU、FPGA、NUMA ノードなどで利用されてきました。シンプルな Device Plugin Interface を実装することで、Pod の resources.limits に CPU やメモリ以外の任意のリソースを指定できるようになり、デバイスファイルをマウントしたり、環境変数を追加したりできます。また、Pod 内のコンテナが起動する前に初期化処理などを挟むこともできます。


出典: https://intel.github.io/kubernetes-docs/device-plugins/index.html

k8stopologyawareschedwg/sample-device-plugin をベースに作成した toVersus/fake-k8s-device-plugin だと、以下のようなマニフェストを反映することで Pod にダミーのデバイスファイル (/dev/null) をマウントします。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: simple-hello
spec:
  replicas: 1
  selector:
    matchLabels:
      app: simple-hello
  template:
    metadata:
      labels:
        app: simple-hello
    spec:
      terminationGracePeriodSeconds: 30
      containers:
      - name: simple-hello
        image: cgr.dev/chainguard/wolfi-base:latest
        imagePullPolicy: IfNotPresent
        command: ["sleep", "infinity"]
        resources:
          limits:
            3-shake.com/fake: 2 # 任意のリソースを指定可能

しかし、KEP-3063 によると Device Plugin は以下の問題を抱えています。

  • ワークロードを起動する前に、FPGA を再設定 / 再プログラムしたいが、任意の設定ファイルを渡す仕組みがない
  • ワークロードが停止した後に、FPGA の設定を初期化したいが、事後処理を挟む仕組みがない
  • NVIDIA GPU の MIG (Multi Instance GPUs) など新しい機能を使って、同一デバイスを複数の Pod やコンテナ間で共有したいが、事前にパーティションに分割したリソースを割り当てることしかできない
  • デバイスのソフト要求 (割り当てるリソースがなければ割り当てない) を実現できない
  • Device Plugin が動作しているノード上のリソースしか検出できず、ファブリック接続のデバイス (e.g. InfiniBand) をサポートしていない

上記の課題を解決するために、DRA の開発が始まりました。ただ、DRA が既存の Device Plugin を完全に置き換える訳ではないようです。対象のデバイスの取り扱いで複雑な要件がないなら、Device Plugin として実装した方が楽です。同一のノード上で、Device Plugin と DRA Resource Driver を動かすことは現状できないので、移行する際はそれぞれが起動するノードを分離して、徐々に移行する必要があります。

DRA の目指すところ

KEP-3063 によると DRA は以下の実現を目指して開発されています。

  • Device Plugin よりもさらに柔軟な実装ができるように
    • リソース要求時に任意のパラメータを渡せる
    • ネットワーク接続のデバイスのサポート
    • 任意のリソース固有の初期化やお掃除を可能に
    • ソフト要求を実装できるようにリソース割り当ての処理をカスタマイズ可能に
  • リソース要求の API をユーザーが使いやすい形に
  • Kubernetes のコンポーネントやコンテナランタイムを再ビルドすることなく、リソース管理のアドオンを開発・デプロイできるように
  • 既存の Device Plugin を DRA Resource Driver に移行できるように十分な機能を揃える

DRA に関わるリソース

DRA は、StorageClass / Persistent Volume (PV) / Persistent Volume Claim (PVC) を一般化したものです。一般化したことで名称に違いが出ているのがややこしいですが、「任意のリソースを要求 (claim) する」ための仕組みと認識しておくのが良さそうです。

  • Resource Class
    • Storage Class にあたる Kubernetes のコアリソース
    • 各ベンダーが実装した DRA Resource Driver の中でどれを使うか指定
    • カスタムリソースなどを参照させることで、Resource Driver に任意の共通パラメータを渡し、そのパラメータを使ってリソースを割り当て可能
  • Resource Claim
    • Persistent Volume (PV) にあたる Kubernetes のコアリソース
    • Resource Driver が割り当てるリソースの実態を表し、リソース割り当ての情報を保持
    • 手動で作成するか、テンプレートから自動生成することが可能
    • 手動で作成した場合は、リソースを複数の Pod で共有可能
  • Resource Claim Template
    • Persistent Volume Claim (PVC) にあたる Kubernetes のコアリソース
    • ResourceClaim を生成するための設計図
    • Pod 毎に ResourceClaim が生成されるので、リソースを複数の Pod で共有できない
    • リソースを同一 Pod 内の複数のコンテナで共有は可能
  • PodSchedulingContext
    • コントロールプレーンにあるスケジューラーとノード上の kubelet や Rersource Driver が協調するために使用

また、任意のリソースを取り扱うため、Resource Driver の実装者はカスタムリソースを定義する必要があります。例えば、toVersus/fake-dra-driver だと以下の CRD を使用しています。

  • NodeAllocationState
    • 割り当て可能なリソースや割り当て予定のリソース、割り当て済みのリソースを管理
    • CRD でリソースの状態をトラッキングする場合によく使われ、Intel / NVIDIA の DRA Resource Driver でも使われている
  • DeviceClassParameters
    • ResourceClass で参照可能な共通パラメータ
    • fake-dra-driver では実質使っていない
  • FakeClaimParameters
    • リソース割り当て時に使う固有のパラメータを定義 (e.g. リソースをいくつ割り当てるか)

DRA の割り当てモード

リソースの割り当てがいつ行われるかでモードが 2 種類あります。

Immediate

ResourceClaim が作成された時点ですぐにリソースの割り当てを開始するモードです。リソースの割り当てに時間がかかる場合 (e.g. FPGA のプログラミング) やリソースを複数の異なる Pod で共有する場合に有効です。既に割り当て済みのリソースを取り合うだけなので、Pod のスケジューリング実装は後述のモードよりもシンプルになります。ただ、CPU やメモリ、ディスクのような他のリソース要件をもとに柔軟に Pod をスケジューリングすることはできません。既にリソースを割り当て済みのどのノードでも他のリソース要件を満たさない場合、要件を満たすまで Pod は Pending 状態となります。

今回の記事では、即時割り当てモードについて触れません。

WaitForFirstConsumer

Pod をどのノードで起動するか決まってから、リソース割り当てを行うモードです。遅延割り当てとも呼ばれます。遅延割り当てでは、リソース割り当てもスケジューリングで考慮されます。スケジューラが適切と判断したノードに Resource Driver がリソースを割り当てます。リソース割り当てが完了するまで Pod は Pending 状態ですが、準備ができたら Pod は起動できます。CPU やメモリの要件を満たしたノードにリソースを動的に割り当てるので、即時割り当てモードとは異なり、永久に Pod が Pending 状態になるシナリオを避けることができます。

今回の記事では、遅延割り当てモードについてのみ触れます。

DRA の前提条件

Kubernetes 1.26 でアルファ機能として入り、Kubernetes 1.27 時点でもアルファ機能のままです。そのため、Dynamic Resource Allocation を試すには以下の条件を満たす必要があります。

DRA Resource Driver を実装する際のベースになる kubernetes-sigs/dra-example-driver には、Kubernetes 1.27 に containerd 1.7.0 を同梱した Kind のコンテナイメージを生成するためのスクリプト (demo/create-cluster.sh) があるので凄く便利です。

DRA の動作イメージ

DRA は様々な登場人物 (リソース) が関わっていて複雑です。仕組みを説明する前に、ユーザーが DRA をどう使うか確認しておきましょう。ResourceClass はクラスタ管理者が事前に作成している前提です。

apiVersion: resource.k8s.io/v1alpha2
driverName: fake.resource.3-shake.com
kind: ResourceClass
metadata:
  # 各ベンダーが実装した Resource Driver の名前を指定
  # クラスタ内に複数の ResourceClass を作成して、ユーザー側で Resource Driver を
  # 選択することも可能
  name: fake.3-shake.com

専用のリソース割り当て

Device Plugin でも実現できていた、Pod 毎に専用のリソースを割り当てるパターンです。Fake と呼ばれるダミーのリソースを 1 つずつ割り当てます。Fake Resource Driver で FakeClaimParameters を使わない場合に、Fake を 1 つ割り当てるというデフォルトの挙動を実装しているからです。

apiVersion: resource.k8s.io/v1alpha2
kind: ResourceClaimTemplate
metadata:
  namespace: test1
  name: default-fake-template
spec:
  spec:
    # Fake Resource Driver 用の ResourceClass 名を指定
    resourceClassName: fake.3-shake.com

---
apiVersion: v1
kind: Pod
metadata:
  namespace: test1
  name: pod0
  labels:
    app: pod0
spec:
  terminationGracePeriodSeconds: 3
  containers:
  - name: ctr0
    image: cgr.dev/chainguard/wolfi-base:latest
    command: ["ash", "-c"]
    args: ["export; sleep infinity"]
    resources:
      # resources.limits や resources.requests とは別に新たに
      # resources.claims フィールドが追加され、resourceClaims から
      # コンテナ毎に割り当てるリソースを参照
      # resources.limits や resources.requests と併用も可能
      claims:
      # resourceClaims の名前と一致させること
      - name: distinct-fake
  # resourceClaims で ResourceClaim や ResourceClaimTemplate を複数参照可能
  # 参照した resourceClaims を各コンテナに割り当てることができる
  resourceClaims:
  - name: distinct-fake
    source:
      # ResourceClaimTemplate を参照するパターン
      # 参照された Pod 単位で異なるテンプレートから ResourceClaim が
      # 自動生成されて紐付く
      resourceClaimTemplateName: default-fake-template

---
apiVersion: v1
kind: Pod
metadata:
  namespace: test1
  name: pod1
  labels:
    app: pod1
spec:
  terminationGracePeriodSeconds: 3
  containers:
  - name: ctr0
    image: cgr.dev/chainguard/wolfi-base:latest
    command: ["ash", "-c"]
    args: ["export; sleep infinity"]
    resources:
      claims:
      - name: distinct-fake
  resourceClaims:
  - name: distinct-fake
    source:
      # 同一の ResourceClaimTemplate を複数の Pod で参照可能
      resourceClaimTemplateName: default-fake-template

上記のマニフェストは、明示的に割り当てるリソースを指定した以下のマニフェストと同じです。

apiVersion: fake.resource.3-shake.com/v1alpha1
kind: FakeClaimParameters
metadata:
  namespace: test1
  name: single-fake
spec:
  count: 1 # デフォルト値だが明示的に指定

---
apiVersion: resource.k8s.io/v1alpha2
kind: ResourceClaimTemplate
metadata:
  namespace: test1
  name: default-fake-template
spec:
  spec:
    resourceClassName: fake.3-shake.com
    parametersRef:
      apiGroup: fake.resource.3-shake.com
      kind: FakeClaimParameters
      name: single-fake

---
apiVersion: v1
kind: Pod
metadata:
  namespace: test1
  name: pod0
  labels:
    app: pod0
spec:
  terminationGracePeriodSeconds: 3
  containers:
  - name: ctr0
    image: cgr.dev/chainguard/wolfi-base:latest
    command: ["ash", "-c"]
    args: ["export; sleep infinity"]
    resources:
      claims:
      - name: distinct-fake
  resourceClaims:
  - name: distinct-fake
    source:
      resourceClaimTemplateName: default-fake-template

---
apiVersion: v1
kind: Pod
metadata:
  namespace: test1
  name: pod1
  labels:
    app: pod1
spec:
  terminationGracePeriodSeconds: 3
  containers:
  - name: ctr0
    image: cgr.dev/chainguard/wolfi-base:latest
    command: ["ash", "-c"]
    args: ["export; sleep infinity"]
    resources:
      claims:
      - name: distinct-fake
  resourceClaims:
  - name: distinct-fake
    source:
      resourceClaimTemplateName: default-fake-template

共通のリソースをコンテナ間で共有

Device Plugin の仕組みで native にサポートされていなかった (GPU time-slicing のように Device Plugin 側の実装で頑張ることはできた)、Pod 内のコンテナでリソースを共有するパターンです。Pod の resourceClaims で参照している ResourceClaimTemplate から生成された ResourceClaims をそれぞれコンテナの resources.claims で参照しています。

apiVersion: resource.k8s.io/v1alpha2
kind: ResourceClaimTemplate
metadata:
  namespace: test2
  name: default-fake-template
spec:
  spec:
    resourceClassName: fake.3-shake.com

---
apiVersion: v1
kind: Pod
metadata:
  namespace: test2
  name: pod0
spec:
  terminationGracePeriodSeconds: 3
  containers:
  - name: ctr0
    image: cgr.dev/chainguard/wolfi-base:latest
    command: ["ash", "-c"]
    args: ["export; sleep infinity"]
    resources:
      # 同一の Pod 内で複数のコンテナが同じ ResourceClaimTemplate を
      # 参照する場合は、リソースを共有する形になる
      claims:
      - name: shared-fake
  - name: ctr1
    image: cgr.dev/chainguard/wolfi-base:latest
    command: ["ash", "-c"]
    args: ["export; sleep infinity"]
    resources:
      claims:
      - name: shared-fake
  resourceClaims:
  - name: shared-fake
    source:
      resourceClaimTemplateName: default-fake-template

動的に生成したリソースを割り当て

Device Plugin の仕組みで native にサポートされていなかった、動的に生成したリソースを割り当てるパターンです。FakeClaimParameters の countsplit を使用して、4 つの Fake リソースをそれぞれ 2 分割して、計 8 つの Fake リソースを動的に生成して Pod に割り当てます。

apiVersion: fake.resource.3-shake.com/v1alpha1
kind: FakeClaimParameters
metadata:
  namespace: test5
  name: multiple-fakes
spec:
  count: 4
  # count で作成されたデバイスを親デバイスとして split で指定した数だけ
  # ダミーなデバイスを作成する
  # 今回だと合計 8 個のダミーなデバイスが動的に生成される
  split: 2

---
apiVersion: resource.k8s.io/v1alpha2
kind: ResourceClaimTemplate
metadata:
  namespace: test5
  name: multiple-fakes
spec:
  spec:
    resourceClassName: fake.3-shake.com
    parametersRef:
      apiGroup: fake.resource.3-shake.com
      kind: FakeClaimParameters
      name: multiple-fakes

---
apiVersion: v1
kind: Pod
metadata:
  namespace: test5
  name: pod0
  labels:
    app: pod
spec:
  terminationGracePeriodSeconds: 3
  containers:
  - name: ctr0
    image: cgr.dev/chainguard/wolfi-base:latest
    command: ["ash", "-c"]
    args: ["export; sleep infinity"]
    resources:
      claims:
      - name: fakes
  resourceClaims:
  - name: fakes
    source:
      resourceClaimTemplateName: multiple-fakes

DRA の構成要素

DRA に関わる Kubernetes のコンポーネントについて軽く触れておきます。

ResourceClaim controller

kube-controller-manager に同梱されているコントローラーです。ResourceClaimTemplate から ResourceClaim を生成 / 削除したり、ResourceClaim の status から既に停止済みの Pod が使用していたリソースを開放したりします。

Dynamic Resources scheduler plugin

Dynamic Resources scheduler plugin は、scheduler framework で実装された in-tree の scheduler plugin です。Volume Binding scheduler plugin を参考に実装されています。kube-scheduler は Resource Driver と直接やり取りをしません。他のリソース要件 (e.g. CPU やメモリなど) にあったノードの候補を渡してくれるだけです。ただ、お互いが協調しないと正しく Pod をスケジューリングできません。そこで、Dynamic Resources scheduler plugin が登場し、PodSchedulingContext がスケジューラーと Resource Driver の架け橋となります。Dynamic Resources scheduler plugin は、Pod を適切なノードにスケジュールできるように、PodSchedulingContext にノードの候補などの格納します。Resource Driver は、これらのノードから現在十分なリソースを利用できないノードの情報を PodSchedulingContext に追加します。このステップを繰り返すことで、Pod をスケジュールするノードを絞っていきます。

DRA の難しいところは、Pod をスケジュールするノードが決まってから、デバイスなどのリソースの初期化処理を走らせるところです。リソースの準備ができて、ResourceClaim の情報が更新されるまで待たなければなりません。

  • PreFilter で Pod に紐付いている ResourceClaim を確認します。
    • 即時割り当てモードでリソースの割り当て準備ができていない Pod がいる場合やそれ以外にもいくつかの条件に一致した Pod を UnschedulableAndUnresolvable にマークし、すぐに unschedulableQ に移します。
  • Filter で PodSchedulingContext のスケジュールできないノードの情報をもとに、Pod をスケジュールするノードの候補を絞り込みます。
    • PodSchedulingContext が存在しない場合は、作成してノードの候補を追加します。
  • PostFilter で割り当て済みの ResourceClaims を解除すると、スケジュール可能になる Pod があれば割り当て解除を依頼します。
  • PreScore で Pod のリソース要求を満たしたノードの一覧を渡して、ノードの候補を PodSchedulingContext の potentialNodes に追加します。
  • Reserve で ResourceClaim に紐づける Pod を選んで予約します。
  • PostBind で Pod がノードに紐付けられた後の後処理を実行します。

kubelet

kubelet はノード上のリソースを管理します。Pod がノード上にスケジュールされると、kubelet は Pod の参照している ResourceClaim を確認します。そして、Resource kubelet-plugin に命令して、ノード上のリソースを準備したり、使われていないリソースを開放したりします。

kubelet と Resource kubelet-plugin は Unix ドメインソケット経由でやり取りをします。この部分は Device Plugin と変わりません.kubelet は、Pod のリソース要求がに応じて、NodePrepareResource API を呼び出します。Resource kubelet-plugin は、NodePrepareResource API が成功すると CDI の JSON ファイルを作成して、デバイス名を返却します。kubelet はこのデバイス名を CRI 経由でコンテナランタイムを呼び出す時に使用します。ノード上で起動していた Pod を停止する時には、NodeUnprepareResource API を呼び出し、割り当てたリソースをお掃除します。

DRA Resource Driver

DRA Resource Driver は 2 つのコンポーネントで構成されています。Resource Driver の開発者が実装します。

Resource kubelet-plugin

各ノード上で DaemonSet として動作し、kubelet から命令を受けてリソースを準備したり、お掃除したりします。中央集権的なコントローラーが割り当ての判断をできるように、ノード上のリソースの状態を知らせます。準備できたデバイスの情報を Container Deivce Interface (CDI) の JSON 形式のファイルに書き出し、kubelet が読み込んで OCI の設定ファイルとマージして、コンテナランタイムに渡せるようにします。ビジネスロジックの実装が多いです。

Resource controller

中央集権的なコントローラーとして動作し、ResourceClaim に変更があるとリソースを割り当てたり、割り当てを解除したりします。ResourceClaim の状態を管理することで、kube-scheduler と協調して、ユーザーからのリソース要求に対してどのノードに割り当てるべきかを判断できるようにします。ビジネスロジックは少なく、ほとんどがボイラープレートで構成されています。

Container Device Interface (CDI)

Resource Driver の実装に進む前に Container Deivce Interface (CDI) について触れておきます。

コンテナランタイムがデバイスをどう取り扱うかこれまで決まりはありませんでした。コンテナの世界でデバイスをリソースとして扱えるように、CDI が立ち上がりました。 コンテナランタイムがサードパーティ製のデバイスをサポートするための仕様が container-orchestrated-devices/container-device-interface にまとめられています。CDI はあくまでコンテナ内からデバイスを使えるようにするための仕様で、デバイスの管理などは範囲外です。

  • コンテナランタイムの実装を変えることなくデバイスをサポートできるように、CDI の仕様に沿った JSON ファイルを OCI の仕様にマージします。
  • コンテナランタイムが検出できるように JSON 形式のファイルを特定のディレクトリ (e.g. /etc/cdi//var/run/cdi/) 内に保存します。
  • CDI 0.6.0 時点で OCI の仕様にマージできるフィールドは、env, deviceNodes, mounts, hooks です。
  • 特定のデバイスに変更をマージしたり、デバイスに関係なく変更をマージするための仕組みがあります。

また、CDI ではデバイスをリソースとして扱う際の修飾名の記法も定義しており、<vendor_id>/<device_class>=<unique_name> という形 (便宜上 "CDI 修飾名" と呼ぶ) で表します。例えば、toVersus/fake-dra-driver だと以下のように <unique_name> にダミーのデバイス UID を使っています。

k8s.fake.resource.3-shake.com/fake=36396c78-94ed-4d8d-a2b1-c7e71ec82cfe

DRA Resource Driver の実装

DRA の場合は、kubernetes/dynamic-resource-allocation にヘルパーのライブラリが用意されていますが、ボイラープレートはそれなりに書かないといけません。

KubeCon のスライドからの引用ですが、概ね以下の流れで実装していきます。

  1. ドライバ名を決めます。
  2. コントロールプレーンと協調する戦略を決めます。
    • 今回は専用の CRD を使った戦略の話しかしません。
  3. 割り当て可能なリソース、割り当て済みのリソース、利用可能なリソースの 3 段階の状態を追跡できるカスタムリソースの定義します。
  4. ResourceClass に渡すパラメータをカスタムリソースとして定義します。
    • カスタムリソースを定義せずに ConfigMap にパラメータを書く方法もあるようです。(source)
  5. ResourceClaim に渡すパラメータをカスタムリソースとして定義します。
    • カスタムリソースを定義せずに ConfigMap にパラメータを書く方法もあるようです。 (source)
  6. デフォルトの ResourceClass を少なくとも 1 つ用意します。
  7. DRA Resource controller をスケジューラーに登録するためのボイラープレートを書きます。
  8. DRA kubelet-plugin を kubelet に登録するためのボイラープレートを書きます。
  9. DRA Resource controller のビジネスロジックを書きます。
  10. DRA kubelet-plugin のビジネスロジックを書きます。

カスタムリソースの定義

自作の DRA Resource Driver を実装する前に、カスタムリソースを定義します。

  • カスタムリソースのグループ名 (=ドライバ名) を決める (e.g. fake.resource.3-shake.com)
  • 独自のリソース or デバイスの種類を決める (e.g. fake)
  • ResourceClass に渡すパラメータを定義する (e.g. DeviceClassParameter)
  • 独自のリソース要求のパラメータを定義する (e.g. FakeClaimParameters)
  • ノードのリソース割り当ての状態を管理するための NodeAllocationState を定義する

以下のコマンドを実行することで、clientset やカスタムリソースの YAML ファイルを生成できます。

make docker-generate

Resource controller の実装

Resource controller では、ヘルパーライブラリで定義されている Driver Interface を実装します。ヘルパーライブラリのおかげで、PodSchedulingContext を介したスケジューラーとのやり取りは抽象化されています。

type Driver interface {
	// GetClassParameters は、ResourceClass で参照されている
	// パラメータ (e.g. DeviceClassParameter) を取得するために使用されます。
	// パラメータの中身は可能な限り検証して下さい。
	// class.ParametersRef は nil の可能性があります。
	//
	// この関数の呼び出し元は、エラーにパラメータの参照を含めて返却します。
	//
	// 関数コメント以外の追加情報
	// - kube-apiserver から毎回 ResourceClass のパラメータを引っ張ってこなくても
	//   他の処理で使い回すことができます。
	// - 基本的にボイラープレートでパラメータ検証以外の処理はほぼ同じ
	GetClassParameters(ctx context.Context, class *resourcev1alpha2.ResourceClass) (interface{}, error)

	// GetClaimParameters は、ResourceClaim で参照されている
	// パラメータ (e.g. FakeClaimParameters) を取得するために使用します。
	// パラメータの中身は可能な限り検証して下さい。
	// claim.Spec.ParametersRef は nil の可能性があります。
	//
	// この関数の呼び出し元は、エラーにパラメータの参照を含めて返却します。
	//
	// 関数コメント以外の追加情報
	// - kube-apiserver から毎回 ResourceClaim のパラメータを引っ張ってこなくても
	//   他の処理で使い回すことができます。
	// - 基本的にボイラープレートでパラメータ検証以外の処理はほぼ同じ
	GetClaimParameters(ctx context.Context, claim *resourcev1alpha2.ResourceClaim, class *resourcev1alpha2.ResourceClass, classParameters interface{}) (interface{}, error)

	// Allocate は、ResourceClaim が割り当てられる準備ができた時に呼び出されます。
	// 即時割り当ての場合、ResourceClaim の selectedNode は空白で、
	// どこに割り当てるかは Driver が決定します。
	// 既に Allocate が進行中の場合、Driver は新しいパラメータを無視して
	// 実行中の Allocate を続けるか、進行中の Allocate を打ち切って
	// 新しいパラメータで Allocate を再実行します。実装依存です。
	//
	// ResourceClaim の selectedNode が設定されている場合、
	// Driver は指定されたノードにリソースを割り当てようとします。
	// 割り当てが無理だった場合は、必ずエラーを返して下さい。
	// コントローラーは UnsuitableNodes を呼び出して新しい情報をスケジューラーに
	// 渡します。それにより、指定されたノードが適切でない場合に異なるノードが
	// 改めて選出されます。
	//
	// オブジェクトは読み取り専用なので修正しないで下さい。
	// Allocate 内の操作はべき等でなければなりません。
	//
	// 関数コメント以外の追加情報
	// - 専用の CRD を使っている場合は、リソース割り当ての結果をカスタムリソースに
	//   保存します。
	Allocate(ctx context.Context, claim *resourcev1alpha2.ResourceClaim, claimParameters interface{}, class *resourcev1alpha2.ResourceClass, classParameters interface{}, selectedNode string) (*resourcev1alpha2.AllocationResult, error)

	// Deallocate は ResourceClaim が開放されるときに呼び出されます。
	//
	// ResourceClaim は読み取り専用なので修正しないで下さい。
	// Deallocate 内の操作はべき等でなければなりません。
	// 特に ResourceClaim が割り当てられていない場合は、エラーを返してはいけません。
	//
	// Deallocate は前の Allocate が中断されて呼び出されることがあります。
	// Deallocate は進行中の Allocate の処理を止めてからリソースを開放して下さい。
	Deallocate(ctx context.Context, claim *resourcev1alpha2.ResourceClaim) error

	// UnsuitableNodes は全ての Pending 状態の ResourceClaim (遅延割り当て) を
	// 確認します。全ての ResourceClaim が Driver によって割り当てられるように
	// 準備されます。
	//
	// Driver は、各 ResourceClaim を一つずつ評価しますが、
	// 全ての ResourceClaim が割り当てられない満たさない限り、
	// 適さないノードとしてマークを付けるようにしましょう。
	// (e.g. 2 つの GPU 要求に対して、ノードに 1 つの GPU しか残っていない)
	// 
	// 確認した結果は ClaimAllocation.UnsuitableNodes に格納されます。
	// エラーを返却すると、最初から確認し直します。
	//
	// 関数コメント以外の追加情報
	// - kube-scheduler と PodSchedulingContext に必要な情報を記載して、
	//   やり取りを繰り返すことで、どのノードに Pod を割り当てるか決定します。
	// - potentialNodes を見ていって、それらのノードで利用可能なリソースを確認し、
	//   リソースが利用できないノードを返却します。
	UnsuitableNodes(ctx context.Context, pod *v1.Pod, claims []*ClaimAllocation, potentialNodes []string) error
}

toVersus/fake-dra-driver に日本語のコメントとログで処理を追っているので、詳細が気になる方は確認してみて下さい。

Resource kubelet-plugin の実装

Resource kubelet-plugin では、NodeServer Interface を実装します。こちらも kubeletplugin のヘルパーライブラリが用意されていて、kubelet に登録するための処理や gRPC サーバーの起動などが抽象化されています。

type NodeServer interface {
	// NodePrepareResource は、NodePrepareResourceRequest の情報をもとに
	// ノード上にリソースを準備し、ResourceClaim に対応した CDI の設定ファイルを
	// 作成します。最終的に CDI 修飾名の一覧を返却します。
	// リソースの準備が完了したら、NodeAllocationState の preparedDevices の
	// 情報を追加します。
	NodePrepareResource(context.Context, *NodePrepareResourceRequest) (*NodePrepareResourceResponse, error)
	
	// NodeUnprepareResource は、NodeUnprepareResourceRequest の情報をもとに
	// ノード上のリソースを開放し、ResourceClaim に対応した CDI の設定ファイルを
	// 削除します。
	// リソースが開放できたら、NodeAllocationState の preparedDevices の情報を
	// 削除します。
	NodeUnprepareResource(context.Context, *NodeUnprepareResourceRequest) (*NodeUnprepareResourceResponse, error)
}

toVersus/fake-dra-driver に日本語のコメントとログで処理を追っているので、詳細が気になる方は確認してみて下さい。

リソース割り当ての流れ

ユーザーが ResourceClaim を参照した Pod を作成してからスケジュール先のノードが決まるまでの流れは以下の通りです。

  1. スケジューラーが Pending 状態の Pod をキューから取り出して、組み込みリソースと ResourceClass にノードセレクタの情報があればそれも使って、割り当て可能なノードの候補を絞り込みます。
    • 初回は Resource Driver が提供する情報 (UnsuitableNodes) を持っていないので使えないが、スケジュールのサイクルを繰り返していく内に割り当てられないノードの一覧の情報も使っていきます。
  2. Dynamic Resource スケジューラーが PodSchedulingContext を作成し、PotentialNodes をその時点でのノードの候補の一覧で更新します。
  3. Resource controller が PodSchedulingContext の変更を検知して、UnsuitableNodes API を呼び出し、ノードの候補の一覧からリソース割り当てのできないノードの一覧を生成します。
  4. Resource controller が PodSchedulingContext の status の UnsuitableNodes をその時点で割り当て不可能なノードの一覧で更新します。

上記の処理を繰り返すことで、割り当て可能なノードを絞り込みます。

スケジュール先のノードが決まってから、ResourceClaim の情報を更新するまでの流れは以下の通りです。

  1. スケジューラーが Pod をスケジュールするノードを選択します。
  2. PodSchedulingContext の SelectedNode にスケジュールするノードを指定して更新します。
  3. Resource controller が PodSchedulingContext の変更を検知して、Allocate API を を呼び出します。
  4. Resource controller が Pod にリソースを割り当てて、ResourceClaim の status の Allocation (スケジュールされるノードを絞り込むための条件) と ReservedFor (どの Pod に紐付けたか) をそれぞれ更新します。
    apiVersion: resource.k8s.io/v1alpha2
    kind: ResourceClaim
    metadata:
      (...)
      name: pod0-distinct-fake
      namespace: test1
      (...)
    status:
      allocation:
        availableOnNodes:
          nodeSelectorTerms:
          - matchFields:
            - key: metadata.name
              operator: In
              values:
              - fake-dra-driver-cluster-worker
        shareable: true
      driverName: fake.resource.3-shake.com
      reservedFor:
      - name: pod0
        resource: pods
        uid: 2e24c7fd-49c1-41d1-9d70-9ffdd1e06a77
    
  5. Resource controller が NodeAllocationState の AllocatedClaims に割り当て済みのリソースを追加して更新します。

スケジューラーが Pod をノードにスケジュールしてから、Pod が起動するまでの流れは以下の通りです。

  1. スケジューラーが Pending 状態の Pod を再度キューから取り出します。
  2. スケジューラーが Pod の nodeName を書き換えます。
  3. kubelet が Pod を起動するときに、Resource kubelet-plugin の NodePrepareResource API を呼び出し、ResourceClaim の情報を渡します。
  4. Resource kubelet-plugin がリソースを割り当てるのに必要なデバイスの初期化処理などを行って、CDI の設定ファイルを作成します。
  5. Resource kubelet-plugin が NodeAllocationState の PreparedDevices に使用する準備のできたデバイスの UID を書き込みます。
  6. コンテナランタイムがコンテナを起動するときに、OCI の設定ファイルに CDI の設定ファイルをマージして渡して、必要なデバイスや環境変数などをマウントできるようにします。
  7. コンテナが起動します。

Pod が削除されるまでの流れは以下の通りです。

  1. Pod を削除します。
  2. kubelet が Pod を停止した後に、Resource kubelet-plugin の NodeUnprepareResource API を呼び出し、ResourceClaim の情報を渡します。
  3. Resource kubelet-plugin がデバイスなどをお掃除して、CDI の設定ファイルを削除します。
  4. コンテナランタイムがコンテナを削除します。

DRA の複雑なところ

DRA を理解した気になる上で、個人的に以下の点が複雑に感じました。

  • リソース要求の遅延評価 (遅延割り当て) の仕組みがとっつき辛かったです。
    • PodSchedulingContext を使ったスケジューラーと Resource controller の協調の仕組み
  • 利用可能なリソース、割り当て済みのリソース、準備のできたリソースの状態管理が重要になってきますが、割り当て済みのリソースの意味を理解するのに時間が掛かりました。
  • DRA Resource Driver で実装されている処理は冪等性を担保する必要があります。コードが冗長化していて迷子になることがありました。

まとめ

  • DRA は、永続化ディスクを一般化して任意のリソースをサポートするための仕組みです。
  • kubernetes-sigs/dra-example-driver をベースに kubernetes/dynamic-resource-allocation のライブラリを使いつつ実装すると、ボイラープレートを減らしてビジネスロジックの実装に集中することができます。
  • DRA は Kubernetes 1.29 でベータ機能に昇格する予定で、DRA を実装した Intel / Nvidia の GPU Resource Driver の開発も順調に進んでいます。

参考

Discussion