0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

controller-runtime: BatchKeyパターンで実現する柔軟な排他制御

Posted at

この投稿では、controller-runtimeの標準的な排他制御の限界と、それを解決するBatchKeyパターンの実装方法について解説します。特に、CRDを用いたキューシステムのような、複数のリソースを協調して処理する必要があるケースでの活用方法を、具体的なコード例とともに説明していきます。

この投稿で知れること

  • controller-runtimeの標準的な排他制御の仕組みと限界
  • キューシステムにおける排他制御の課題
  • BatchKeyパターンの基本的な考え方
  • Field IndexerとMapFuncを使った実装手順

標準的な排他制御の仕組み

controller-runtimeは通常、namespace + nameの組み合わせで排他制御を行います。これにより、同じリソースに対する複数の変更が同時に実行されることを防ぎ、データの整合性を保っています。

// 標準的なReconcile - namespace+name単位の排他制御
func (r *StandardReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    // req.NamespacedName は { Namespace: "default", Name: "my-resource" }
    // "default/my-resource" に対してのみ排他制御される
    var resource MyResource
    if err := r.Get(ctx, req.NamespacedName, &resource); err != nil {
        return ctrl.Result{}, client.IgnoreNotFound(err)
    }
    
    // この処理は "default/my-resource" に対してのみ実行される
    // 他の "default/other-resource" は並列処理される
    return ctrl.Result{}, nil
}

この方式は多くの場合で問題なく動作しますが、もっと広い範囲で排他制御したいときはどうしたらいいのでしょうか。

例えばJobキューシステム

CRDを使って簡単なJobキューシステムを実装する場合を考えてみます。各Jobはspec.queueフィールドでキューを指定し、spec.nextフィールドでキュー内の順序を表現するという設計です。

apiVersion: example.com/v1
kind: Job
metadata:
  name: job1
spec:
  queue: "queue-a"        # キューA
  next: "job2"            # 次に実行するJob
  command: ["echo", "step1"]
---
apiVersion: example.com/v1
kind: Job  
metadata:
  name: job2
spec:
  queue: "queue-a"        # 同じキューA
  next: "job3"            # 次に実行するJob
  command: ["echo", "step2"]
---
apiVersion: example.com/v1
kind: Job
metadata:
  name: job3
spec:
  queue: "queue-a"        # 同じキューA
  next: ""                # 最後のJob(nextなし)
  command: ["echo", "step3"]

なぜ標準的な制御では問題が起きるのか?

標準的な制御方式では、各Jobが個別にReconcileされるため、以下のような問題が発生します。

spec.nextによる順序制御において、同じキューのJobが並列実行され、spec.nextの順序が無視されるという問題があります。また、Job間の依存関係を個別に制御するのが難しくなります。

// 標準的制御の場合の問題例
Job1 (queue: "queue-a", next: "job2") ──→ 個別処理並列実行
Job2 (queue: "queue-a", next: "job3") ──→ 個別処理並列実行)← spec.nextが無視される
Job3 (queue: "queue-a", next: "")     ──→ 個別処理並列実行

つまり、同じキューに属するJobであっても、それぞれが独立して処理されてしまい、spec.nextで定義した実行順序を守ることができないというわけです。

BatchKeyパターンとは?

BatchKeyパターンは、標準的なnamespace/nameキーの代わりに、任意のフィールド値をキーとして使用し、同じキー値を持つリソース群をまとめて処理する実装パターンです。

キューシステムの例では、spec.queueの値をキーとしてJobをグループ化し、キュー単位で排他制御&一括処理を行います。

// BatchKeyパターンによるキューシステムの実現
spec.queue="queue-a"  BatchKey: "spec.queue:queue-a"
spec.queue="queue-b"  BatchKey: "spec.queue:queue-b"

// 各キューは独立してReconcileされる
JobController.Reconcile("spec.queue:queue-a") {
    // queue-aのすべてのJobを取得
    // spec.nextに従った順次実行制御
    // job1 → job2 → job3 の順序で処理
}

標準的制御とBatchKey制御の比較

標準的制御では、排他制御の単位が「namespace + name」で、処理対象は1つのリソース、並列度はリソース数分になります。一方、BatchKey制御では、排他制御の単位が任意のキー値で、処理対象は同じキー値のリソース群、並列度はキー値の種類数分になります。

BatchKeyパターンの処理フロー

BatchKeyパターンは、以下の3つのコンポーネントで構成されます。

  1. FieldIndexer: 逆引き用インデックスを設定します。特定のフィールド値でリソースを高速検索できるようにします。

  2. MapFunc: フィールド値をキーとしたバッチキーを生成します。標準的なnamespace/nameキーを任意のキーに変換します。

  3. Reconcile: キーに基づいてリソースを一括取得・処理します。同じキー値を持つ全リソースを一度に処理します。

視覚的に見ると、以下のような流れになります。

Job Resources                 MapFunc                 Reconcile Queue             Reconcile Logic
┌──────────────────────────┐  ┌──────────────────┐   ┌──────────────────────┐   ┌─────────────────────────┐
│ Job1 (queue:"queue-a",   │─►│ spec.queue:      │──►│ spec.queue:queue-a   │──►│ List all Jobs           │
│       next:"job2")       │  │ queue-a          │   │                      │   │ with queue="queue-a"    │
│ Job2 (queue:"queue-a",   │─►│ spec.queue:      │   │                      │   │                         │
│       next:"job3")       │  │ queue-a          │   │                      │   │                         │
│ Job3 (queue:"queue-a",   │─►│ spec.queue:      │   │                      │   │                         │
│       next:"")           │  │ queue-a          │   │                      │   │                         │
└──────────────────────────┘  └──────────────────┘   └──────────────────────┘   └─────────────────────────┘

実装手順

それでは、実際にBatchKeyパターンを実装する手順を見ていきます。

Step 1: Field Indexerの設定

まず、cmd/main.goでField Indexerを設定します。これにより、Reconcilerが特定のフィールドでの検索できるようになります。次の例では、Job.Spec.Queueを検索キーにしています。

// spec.queueの逆引き検索用のField Indexerを設定
const idxQueue = "spec.queue:queue-a" // プレフィックス付きでインデックス
if err := mgr.GetFieldIndexer().IndexField(context.Background(), &myapi.Job{}, idxQueue,
    func(rawObj client.Object) []string {
        job := rawObj.(*myapi.Job)
        // プレフィックス付きの値でインデックス
        return []string{"spec.queue:" + job.Spec.Queue}
    }); err != nil {
    setupLog.Error(err, "spec.queueのField Indexer作成に失敗")
    os.Exit(1)
}

Step 2: コントローラーの実装

次に、internal/controller/job_controller.goでコントローラーを実装します。

まず定数を定義します:

// spec.queue用のField Indexer定数
const idxQueue = "spec.queue:queue-a"

// バッチキー用のプレフィックス
const prefixQueue = "spec.queue:"

つづいて、Reconcileメソッドを次のようなかんじで実装します。

func (r *JobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    // req.Name は "spec.queue:<実際のキュー値>" (バッチキー)
    queueKey := req.Name
    l := log.FromContext(ctx).WithName(queueKey)

    l.Info(fmt.Sprintf("キュー %s のJobをReconcile中", queueKey))

    // Field Indexerを使って指定されたキュー値を持つ全Jobをリスト
    // プレフィックス付きの値で検索するため、req.Nameをそのまま使用
    var jobs myapi.JobList
    if err := r.List(ctx, &jobs,
        client.MatchingFields{idxQueue: queueKey}); err != nil {
        l.Error(err, "キュー値でのJobリストに失敗", "queue", queueKey)
        return ctrl.Result{}, err
    }

    l.Info(fmt.Sprintf("キュー %s で %d 個のJobを発見", queueKey, len(jobs.Items)))

    // 指定されたキュー値を持つ各Jobを処理
    for _, job := range jobs.Items {
        l.Info("Job処理中", "name", job.Name, "namespace", job.Namespace, "queue", job.Spec.Queue)
        // TODO: ここにビジネスロジックを実装
        // 例: spec.nextに従った順序制御など
    }

    return ctrl.Result{}, nil
}

最後にSetupWithManagerメソッドの実装です。

func (r *JobReconciler) SetupWithManager(mgr ctrl.Manager) error {
    return ctrl.NewControllerManagedBy(mgr).
        // JobリソースをWatchし、MapFuncでバッチキーを作成
        Watches(&myapi.Job{},
            handler.EnqueueRequestsFromMapFunc(func(_ context.Context, obj client.Object) []reconcile.Request {
                job := obj.(*myapi.Job)
                // プレフィックス付きのバッチキーを作成: Namespaceは空、Nameにプレフィックス付きのqueue値を含む
                key := types.NamespacedName{Name: prefixQueue + job.Spec.Queue}
                return []reconcile.Request{{NamespacedName: key}}
            })).
        WithOptions(controller.Options{
            MaxConcurrentReconciles: 8, // 異なるqueue値は並列処理可能(最大8)
        }).
        Named("job").
        Complete(r)
}

まとめ

この投稿では、controller-runtimeの標準的な排他制御の限界と、それを解決するBatchKeyパターンについて解説しました。

BatchKeyパターンは、標準的なnamespace/name単位の制御を拡張し、任意のフィールド値でリソースをグループ化して処理する強力な手法です。特にキューシステムのような、複数のリソースを協調して処理する必要があるケースで威力を発揮します。

実装のポイントは、Field Indexerによる逆引き検索の準備、MapFuncでのプレフィックス付きキー生成、そしてReconcileでの一括処理という3つのステップです。Field Indexerでもプレフィックス付きの値でインデックスすることで、実装をシンプルに保つことができるというわけです。

最後までお読みくださりありがとうございました。Twitter では、Qiita に書かない技術ネタなどもツイートしているので、よかったらフォローしてもらえると嬉しいです:relieved:Twitter@suin

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?