🚀

真面目に理解するcontroller-runtime Cache

2024/05/02に公開

controller-runtimeには利用者がInformerの存在を意識する必要が無いよう、Cacheという仕組みが用意されています。Cacheは特に何も設定しなければ、Kubernetes ObjectをGet/Listすると裏でInformerを起動し、同じGVKのObjectをすべてキャッシュします。

この (ややナイーブな) Cacheの挙動は小さなKubernetes Clusterでは多くの場合問題になりませんが、Clusterの規模が大きくなるとControllerが予期せず大量のメモリを消費してしまうことに繋がります。このような場合に備えて、CacheにはInformerの挙動を細かく制御するためのOptionsが用意されています。

しかし、このOptionsに関するドキュメントが不足しており、細かい挙動が理解しづらいという問題があります。そのため、ここではまず最初にCacheのコードを読み解き、その上でOptionsの挙動を検討したいと思います。なお、以下では単に「Cache」と記述した場合、Cacheインターフェースおよびその実装であるinformerCachemultiNamespaceCachedelegatingByGVKCacheを総称しています。

Get/ListとCache

Cacheの詳細へ立ち入る前にCacheを用いてKubernetes ObjectをGet/Listするまでの大まかな流れを確認しておきます。通常、controller-runtimeを用いる際は初めにGetClientメソッドを用いてKubernetesのAPIとやり取りするためのClientを取得します。

GetClientメソッドの実体はcontrollerManagerのGetClientメソッドで、内部ではclusterのGetClientメソッドを呼び出していることが分かります。こちらはシンプルにclusterのclientフィールドの値を返しているだけなので、今度はこのclientフィールドに何がセットされているのか見ていきましょう。

clientフィールドにセットされているのはclientWriterです。clientWriterはoptions.NewClientファンクションを用いて初期化されています。options.NewClientにはデフォルトでclientパッケージのNewファンクションがセットされています。またここでoptions.NewCacheファンクションを用いてCacheが初期化されています。options.NewCacheにはデフォルトでcacheパッケージのNewファンクションがセットされています。初期化したCacheをoptions.NewClientの引数として渡しています[1][2]

さて、cacheパッケージのNewファンクションではnewClientファンクションを呼び出し、Clientを初期化しています。clientのcacheフィールドには先ほど初期化したCacheがセットされていることが分かります。

ここまで来れば、あとはclientのGetListを見てみましょう。それぞれshouldBypassCacheメソッドを呼び出したあと、キャッシュが許可されたKubernetes ObjectであればCacheのGet/Listメソッドを呼び出しています。

少し長かったですが、ここまででGetClientメソッドから取得したクライアントでGet/Listを呼び出した場合、実体としてはCacheのGet/Listメソッドを呼び出しているということが分かりました。

Cacheの種類

Cacheインターフェイスの実装には以下3種類があります。

  1. informerCache
  2. multiNamespaceCache
  3. delegatingByGVKCache

informerCacheは最も基礎となるCacheの実装で、Cache OptionsのDefaultNamespacesやByObjectを指定しない場合はCacheとしてinformerCacheを直接利用することになります。multiNamespaceCacheはDefaultNamespacesオプションを設定した場合に利用されるCacheで内部的には、Namespace名とCache (= informerCache) のMapを保持しており、特定のNamespaceに対するInformerの挙動を指定することができます。

delegatingByGVKCacheはByObjectオプションを設定した場合に利用されるCacheです。内部でGVKとCacheのMapを保持しており、特定のGVKに対するInformerの挙動を指定することができます。ByObjectのNamespacesオプションが指定されていた場合[3]にはGVKに対応するCacheとしてmultiNamespaceCacheを、それ以外の場合にはinformerCacheを利用します。それではそれぞれのCacheの挙動をもう少し詳しく見ていきましょう。

informerCache

informerCacheのGetメソッドを例に、Informerが動的に作成される流れを確認します。Getメソッドは、内部でinformerCacheのgetInformerForKindメソッドを呼ぶことが分かります。getInformerForKindメソッドInformersのGetメソッドを呼び、InformersのGetメソッドはInformerが存在しなければInformersのaddInformerToMapメソッドによってInformerを作成します。

作成されたInformerはCacheエントリー (このCacheはcacheパッケージのCacheインターフェイスではなく、internalパッケージのCache struct) へ詰め、informersByTypeメソッドを通じてInformersのtrackerフィールド保存されます。

informerCacheのGetメソッドは、最終的にこのCacheエントリーのReaderフィールドへセットされたCacheReaderのGetメソッドを通じてObjectを取得します。CacheReaderのGetメソッドは、IndexerからObjectを取得していることが分かります[4]

multiNamespaceCache

multiNamespaceCacheに関しても、簡単に挙動を確認しておきましょう。multiNamespaceCacheはnewMultiNamespaceCacheファンクションによってNamespaceごとにinformerCacheを作成し、multiNamespaceCacheのnamespaceToCacheフィールドへ保存します。multiNamespaceCacheのGetメソッドでは、与えられたObjectのNamespaceをもとにnamespaceToCacheフィールドから対応するinformerCacheを取り出しinformerCacheのGetメソッドを呼びます。

delegatingByGVKCache

delegatingByGVKCacheはByObjectオプションを元にGVKごとにCacheを生成し、delegatingByGVKCacheのcachesフィールドへ保存します。前述の通り、ここで生成されるCacheはオプションによりinformerCacheもしくはmultiNamespaceCacheになります。delegatingByGVKCacheのGetメソッドでは与えられたObjectのGVKからCacheを取り出し、CacheのGetメソッドを呼びます。

BuilderのFor、Watches、OwnsとCache

ClientのGet/Listを呼び出す以外にも、BuilderのForWatchesOwnsメソッドを通じてInformerを登録することもできます。この場合のCacheの挙動も確認しておきましょう。BuilderのdoWatchメソッドを見ると、For、Watches、Ownsどのオプションを呼び出したとしても最終的にControllerのWatchメソッドを通じてKindのStartメソッドを呼ぶことが分かります。StartメソッドではCacheのGetInformerメソッドを用いてInformerを登録しています。

Cache Options

それでは以下、Cache Optionsの挙動を詳しく見ていきます。

ReaderFailOnMissingInformer

ReaderFailOnMissingInformerはGet/Listを通じた動的なInformerの起動を禁止するオプションです。informerCacheのgetInformerForKindメソッドで、ObjectのGVKに対応するInformerが無ければエラーを返していることが分かります。BuilderのFor、Watches、Ownsメソッドを通じてCacheのGetInformerメソッドを呼ぶか、CacheのGetInformerメソッドを直接呼ぶことで事前にInformerを起動しておくことができます。

DefaultNamespaces

DefaultNamespacesははmultiNamespaceCacheを用いてNamespaceごとにInformerの挙動を指定するためのオプションです。DefaultNamespacesで指定することのできるオプションはConfigを確認してください。DefaultNamespacesを指定した場合のエッジケースをいくつか確認しておきましょう。

Cluster-scoped ObjectのGet/List

multiNamespaceCacheのGetメソッドを確認すると、ObjectがCluster-scopeの場合はclusterCacheのGetを呼び出していることが分かります。clusterCacheの実体はNamespaceの制限を持たないinformerCacheです[5]。つまりDefaultNamespacesを指定しても、clusterCacheを用いてCluster-scope ObjectのInformerを起動し、Get/Listができることが分かります。

指定したNamespace以外のObjectのGet/List

再度multiNamespaceCacheのGetメソッドを確認します。まず初めにnamespaceToCacheを与えられたObjectのNamespaceで検索し、もしCacheがなければ再度namespaceToCachemetav1.NamespaceAll (= "")で検索しています。このことから、指定したNamespace以外のObjectのGet/Listは、空文字のNamespaceをDefaultNamespacesに指定していればinformerCacheが利用され、それ以外の場合はエラーが返ることが分かります。

DefaultTransform

DefaultTransformInformへ渡すデフォルトのTransformファンクションを指定します。TransformファンクションはInformerがObjectを処理するよりも前に、Objectに対して変更を加えることができます。よくあるユースケースとしてはObjectのフィールド (例えばMetadataなど) を削除することでメモリ使用量を抑制するなどが挙げられます[6]

DefaultUnsafeDisableDeepCopy

DefaultUnsafeDisableDeepCopyはGet/Listを呼ぶ際に、InformerがキャッシュしているKubernetes ObjectをCacheReaderがDeepCopyせずに返すことでメモリ使用量を節約するためのオプションです。コメントにもある通り、このオプションを有効にするとObjectへ変更を加える場合は忘れずにDeepCopyを行う必要があり、大量のObjectを1リコンサイルでListしているなど特殊な事情がない限りは有効にすることは無いはずです。

ByObject

ByObjectはdelegatingByGVKCacheを用いてGVKごとにInformerの挙動を指定するためのオプションです。ByObjectで指定することのできるオプションはByObject structを確認してください。DefaultNamespacesとよく似たオプションですが、ByObjectで指定されていないGVKのObjectがGet/Listに渡ってきた場合の挙動が異なります。

DefaultNamespacesの場合は、一部の場合を除いて指定されていないNamespaceのGet/Listにはエラーが返っていました。一方、delegatingByGVKCacheのcacheForGVKメソッドを見ると、指定されたGVKがdbt.cachesに無い場合にはdbt.defaultCacheを返していることが分かります。

defaultCache他のオプションに応じてmultiNamespaceCacheかinformerCacheになります。つまりByObjectの場合、指定の無いGVKのObjectが渡されても基本的にはエラーが返らず、動的にInformerが起動するという違いがあります。

CacheのOptionsには上記以外にもSyncPeriodDefaultLabelSelectorDefaultFieldSelectorに加えて、DefaultNamespacesオプションのConfigやByObjectオプションのByObjectなどここでは説明しきれていないオプションがありますが、それらの挙動はここまでの内容をもとに簡単に理解できるため、ここでの説明は省略します。

脚注
  1. 初期化されたCacheはclusterのcacheフィールドへセットされることで、clusterのStartメソッドを通じて起動します。clusterそのものはRunnableとしてcontrollerManagerのrunnabblesへ追加され起動します。Cacheインターフェイスが持つStartメソッドの実体はinformerCacheの場合InformarsのStartメソッドが、multiNamespaceCachedelegatingByGVKCacheの場合はそれぞれ独自のStartメソッドが実装されています。 ↩︎

  2. CacheそのものもInformarsインターフェイスを通じてStartメソッドを実装しているため、Runnableとして直接controllerManagerへ追加することができます。このCacheを用いてClientを作成することで、controllerManagerとは全く異なるCache Optionsを採用したClientを利用することもできます。 ↩︎

  3. DefaultNamespacesオプションを指定した場合にもmultiNamespaceCacheが利用されます。コメントにもある通り、DefaultNamespacesオプションを利用しないためにはByObjectのNamespacesオプションへ空のリストを指定する必要があります。 ↩︎

  4. client-goのInformerやIndexerに関しては、client-go under the hoodなど他のドキュメントを参照してください。 ↩︎

  5. globalConfigは、DefaultNamespacesからmultiNamespaceCacheが作られる場合はoptionDefaultsToConfigファンクションから作られたConfigが、ByObjectからdelegatingByGVKCacheを経由してmultiNamespaceCacheが作られる場合はnilになります。 ↩︎

  6. メモリ使用量を抑制する方法として、PartialObjectMetadataを利用するという方法もあります。PartialObjectMetadataを利用することでObjectのMetadataのみキャッシュするためメモリ使用量を抑えることができます。 ↩︎

Discussion