Skip to content

Bound the informer cache for cluster-scoped deployments#949

Open
LipatovAlexander wants to merge 1 commit into
k8ssandra:masterfrom
LipatovAlexander:scope-informer-cache
Open

Bound the informer cache for cluster-scoped deployments#949
LipatovAlexander wants to merge 1 commit into
k8ssandra:masterfrom
LipatovAlexander:scope-informer-cache

Conversation

@LipatovAlexander

Copy link
Copy Markdown
Contributor

What this PR does:
Bounds the manager's informer cache so operator memory no longer scales with cluster size (details and production numbers in #947):

  • cache.Options.ByObject label-scopes the Pod/StatefulSet/PodDisruptionBudget/Service informers to app.kubernetes.io/managed-by=cass-operator. The operator only reads objects it created and labels; the existing managedByCassandraOperatorPredicate on Owns() already asserts the same invariant on events. Hot per-reconcile reads stay cached.
  • Secrets are read live (client.CacheOptions.DisableFor): the operator also reads user-provided secrets (SuperuserSecretName, Spec.Users, ConfigSecret, management-API TLS) that carry no operator label, so they cannot be label-scoped. Both Secret watches become metadata-only projections (builder.OnlyMetadata); their map fn / predicate only inspect annotations. configSecretMapFn drops its *corev1.Secret type assertion accordingly.
  • PVC/StorageClass/Endpoints/EndpointSlice/ConfigMap reads are cold paths (resize, decommission, additional-seeds, image config) and also go live, so no cluster-wide informers are lazily created for them.
  • All cached objects are stripped of managedFields and the kubectl last-applied-configuration annotation.

Production results (cluster-scoped): OOMKilled at startup with 512Mi → 87-91Mi steady state on clusters from 5.7k to 17.5k pods. Namespace-scoped deployments also drop ~7x.

Independent of the Events PR; both compose.

Which issue(s) this PR fixes:
Fixes #947

Checklist

  • Changes manually tested — production on five clusters, cluster-scoped, incl. chaos-mesh pod-kill runs against managed Cassandra clusters
  • Automated Tests added/updated — envtest controller suite exercises the metadata-only watches and rewritten map fn
  • Documentation added/updated
  • CHANGELOG.md updated
  • CLA Signed

In cluster-scoped mode (empty WATCH_NAMESPACE) the manager cache was
fully unscoped: it listed and cached every Pod, StatefulSet,
PodDisruptionBudget, Service and Secret across all namespaces, and
cached reads lazily instantiated cluster-wide informers for
PersistentVolumeClaims, StorageClasses, Endpoints and EndpointSlices.
On a mid-size production cluster (286 namespaces, 6.5k pods) the
operator OOM-killed at startup with a 512Mi limit; after this change it
runs at ~90Mi on clusters up to 17.5k pods (memory no longer scales
with cluster size).

The scoping model:

- Pod/StatefulSet/PodDisruptionBudget/Service informers are label-scoped
  (cache.Options.ByObject) to app.kubernetes.io/managed-by=cass-operator.
  The operator only ever reads objects it created and it labels all of
  them; the existing managedByCassandraOperatorPredicate on Owns()
  already asserted the same invariant on events. Hot per-reconcile reads
  stay cached.
- Secrets cannot be label-scoped: the operator reads user-provided
  secrets (SuperuserSecretName, Spec.Users, ConfigSecret, management
  API TLS) that carry no operator label. They are read live instead
  (Client.Cache.DisableFor), and both Secret watches use metadata-only
  projection (builder.OnlyMetadata) — their map fns and predicate only
  inspect annotations — so no full-object Secret informer ever exists.
  configSecretMapFn drops its *corev1.Secret type assertion, which
  would fail on PartialObjectMetadata.
- PVC/StorageClass/Endpoints/EndpointSlice/ConfigMap reads are cold
  paths (resize, decommission, additional-seeds, image config) and go
  live as well, so no informers are created for them at all.
- All cached objects are stripped of managedFields and the kubectl
  last-applied-configuration annotation (the latter embeds the full
  object and survives metadata-only projection).

Namespace-scoped deployments benefit too (~7x memory reduction
observed), since the same unscoped informers previously cached every
object in the watched namespaces.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Alexander Lipatov <lipatov@mindbox.cloud>
@LipatovAlexander LipatovAlexander requested a review from a team as a code owner July 6, 2026 15:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Operator OOMs in cluster-scoped mode: informer cache is unbounded and scales with cluster size

1 participant