controller-runtime キャッシュが API サーバーの過負荷を防ぐ仕組み
Kubernetes コントローラーにおける list-watch パターンとローカルキャッシュの詳細な解説。読み取り操作が低コストである一方で、整合性が結果的(eventual)になる理由を説明します。
英語の原文から自動翻訳されました。
2026年7月の Kubernetes Blog の記事で、エンジニアたちは controller-runtime キャッシュの内部メカニズムについて詳しく解説しました。この記事では、Go ベースのコントローラーが Kubernetes API サーバーとどのように連携するかを明確にし、本番環境におけるデータ整合性やメモリ使用量に関する一般的な誤解を正しています。
何が起きたか
この出版物は、Kubernetes オペレーターを開発している開発者の間で広く見られる誤解に対処しています。それは、「reconciler 内のすべての読み取り操作が、API サーバーへの直接 HTTP リクエストを引き起こす」というものです。実際には、controller-runtime は list と watch パターンを通じて作成される、ローカルのインメモリキャッシュに依存しています。このアーキテクチャ上の選択により、コントローラーは制御プレーンや etcd を圧迫することなく、毎秒数百回の reconciliations を処理できるようになります。
しかし、この効率性には特定のトレードオフが伴います。コントローラーはライブの信頼できる情報源(source of truth)ではなくローカルのコピーから読み取るため、書き込み操作の直後に古いデータ(stale data)に遭遇する可能性があります。記事では、書き込みは直接 API サーバーに行われる一方、読み取りはメモリから提供されるため、システムは強い即時一貫性よりも可用性と低レイテンシを優先していることを強調しています。このモデルを無視した開発者は、過度なメモリを消費したり、大規模なデータセットに対して非効率的な線形スキャンを実行したりするコントローラーを作成してしまうリスクがあります。
この説明は、高負荷シナリオで予期しない挙動を観察したエンジニア向けの修正ガイドとして機能します。API サーバーからローカル informer ストアまでのフローをマッピングすることで、著者たちはメモリプレッシャー、ネットワークトラフィック、reconciler ロジックのエラーに関連する問題のデバッグのための首尾一貫したメンタルモデルを提供しています。
仕組み
コアメカニズムは、client-go 内のコンポーネントである Reflector に依存しており、これは特定のリソースタイプに対する継続的な watch を維持します。起動時、Reflector は関心のあるオブジェクトの初期スナップショットを取得します。最近の実装ではストリーミングリストアプローチを使用しており、API サーバーは既存のオブジェクトに対して合成された ADDED イベントを送信してから、ライブ変更イベントに切り替えます。これにより、個別の一括 list 呼び出しが必要なくなり、接続オーバーヘッドが削減されます。
初期状態がキャプチャされると、Reflector は resourceVersion を使用してイベントの漏れがないようにしながら watch を開いたままにします。接続が切断された場合、Reflector は最後に既知のバージョンを使用して再接続します。そのバージョンが古すぎる場合、API サーバーは 410 Gone エラーを返し、Reflector は新しいスナップショットを取得してプロセスを再開せざるを得なくなります。これにより、ローカルキャッシュはクラスターの状態と最終的に一致することが保証されます。
受信した変更は delta キューを通じて処理されます。client-go の最近のバージョン(1.36+)では、イベントのグローバルな順序を保持する厳密に順序付けられたキューである RealFIFO が使用されています。オブジェクトごとにイベントを重複排除していた以前の実装とは異なり、RealFIFO はすべての通知を順番通りに通過させます。つまり、オブジェクトが短時間に3回更新された場合、コントローラーのイベントハンドラーは3つの異なる更新通知を受け取り、アプリケーションロジックに到達する前に中間状態が黙って破棄されないことが保証されます。
主要な詳細
- reconciler 内の
r.Get()およびr.List()は、API サーバーではなく、ローカルのインメモリキャッシュから読み取ります。 - 書き込みはキャッシュをバイパスし、直接 API サーバーに行われるため、後続の読み取りで古いデータが返される可能性があります。
- キャッシュは list + watch パターンを使用しており、最近の実装では初期同期のためにストリーミングリストを使用しています。
RealFIFOは最近のclient-goバージョンでDeltaFIFOを置き換え、informer レイヤーでの自動重複排除を廃止しました。- メモリ消費量は、単なるオブジェクトの数だけでなく、ローカルキャッシュのサイズとインデックス付きフィールドの数によって決定されます。
APIReaderは API サーバーへの直接アクセスを提供しますが、より高いレイテンシと負荷のため、控えめに使用すべきです。
なぜ重要なのか
オペレーターを構築するソフトウェアエンジニアにとって、このキャッシュモデルを理解することはパフォーマンスチューニングにおいて極めて重要です。読み取りはローカルで行われるため、不要なリソースタイプの監視を追加したり、インデックスを増やしたりすると、API サーバーの負荷が見える形で増加しないまま、ギガバイト単位のメモリ使用量につながる可能性があります。この隠れたコストは、しばしば本番規模でのみ顕在化するため、開発中に診断するのが困難です。List() 操作がローカルストア全体に対する線形スキャンを引き起こす可能性があることを認識することで、開発者はコントローラーの応答性を低下させる非効率的なフィルタリングロジックを書くのを避けられます。
さらに、結果的一貫性(eventual consistency)モデルは、reconciler が状態遷移をどのように扱うかに影響を与えます。Get() 呼び出しが以前の Update() を即座に反映すると仮定すると、競合条件(race conditions)や論理エラーにつながる可能性があります。エンジニアは、reconciler を冪等(idempotent)かつ古い読み取りに対して耐性があるように設計し、ローカルキャッシュを決定的な信頼できる情報源ではなくヒントとして扱う必要があります。この考え方の変化により、クラスターが高負荷状態にある場合や、ネットワークパーティションが一時的な非同期化を引き起こす場合に失敗するような、脆弱なコントローラーを防ぐことができます。
実行できること
- コントローラーの
Watchesを監査し、reconciliation ロジックに厳密に必要なリソースタイプのみをキャッシュしていることを確認してください。 - predicates を使用して informer レベルでイベントをフィルタリングし、不要な reconcile リクエストが workqueue に入るのを防いでください。
- 書き込み後の即時一貫性を仮定しないでください。ローカルキャッシュが API サーバーに遅れているケースを処理できるように reconciler を設計してください。
- 多くのインデックスを持つ大きなキャッシュは Out-of-Memory クラッシュの原因となる可能性があるため、コントローラーポッドのメモリ使用量を監視してください。
- ワークフロー完了前の最終状態の検証など、強い一貫性が必要な特定のケースでのみ
APIReaderを使用してください。 - 高い churn rate(変更頻度)下でのコントローラーの挙動をテストし、古い読み取りやキューのバックログに関連する問題を特定してください。



