Skip to content

Conversation

@svghadi
Copy link
Collaborator

@svghadi svghadi commented Sep 11, 2025

What type of PR is this?
/kind enhancement

What does this PR do / why we need it:

This PR reduces memory usage in the Argo CD Operator by introducing a cache transform and client wrapper for Secrets and ConfigMaps.

  • Cache transform: Strips large data fields from non-operator Secrets/ConfigMaps before storing them in the controller-runtime cache, while retaining full content for operator-tracked objects.
  • Client wrapper: Falls back to the live client when a stripped object is accessed and applies a tracking label so future updates are fully cached.
  • Integration: Wired into main.go via cache.Options.ByObject and applied across reconcilers.

See the attached proposal in this PR for design details, trade-offs, PoC results, and future scope.

Results
These metrics were collected from a test cluster containing 100 ConfigMaps and 100 Secrets, each approximately 1 MB in size. The cluster was running four Argo CD instances and no other workload operators.

With the optimization enabled, operator memory usage dropped from ~350 MB to ~100 MB.

UnOptimized Operator Manager Memory:

unoptimized-manager-memory

Optimized Operator Manager Memory:

optimized-manager-memory

However, we could not reduce the startup memory consumption, which remained at ~750 MB in both cases.

We previously attempted another approach in #1795, but it introduced significant complexity and restricted how watches could be set up. Compared to that, this solution provides a better balance between complexity, maintenance overhead, and outcome.

@svghadi svghadi marked this pull request as draft September 11, 2025 07:25
@svghadi svghadi changed the title WIP feat: Cache transform for Secrets and ConfigMaps to reduce memory Sep 22, 2025
Signed-off-by: Siddhesh Ghadi <[email protected]>
Signed-off-by: Siddhesh Ghadi <[email protected]>
Signed-off-by: Siddhesh Ghadi <[email protected]>
@svghadi svghadi marked this pull request as ready for review September 22, 2025 16:45
return in, nil
}
// Strip data for non-operator secrets
return &v1.Secret{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than creating a new Object, can we not set the Data field to nil on the same object ? When we create new Secret object. The old object with the data field also exists in memory and waits for Garbage collection to clean it up, isn't it ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @anandf I have updated it according to how you have mentioned in comment

// StripConfigMapDataTransform returns a TransformFunc that strips the data from ConfigMaps
// that are not tracked by the operator. This is useful for reducing memory usage
// when caching ConfigMaps that are not managed by the operator.
func StripConfigMapDataTransform() clientgotools.TransformFunc {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have a single generic function that can handle both Secret and ConfigMap ? Anyways the function takes in an generic interface{} struct.

Copy link
Collaborator

@anandrkskd anandrkskd Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unified both function

}

// IsTrackedByOperator checks if the given labels indicate that the resource is tracked by the operator.
func IsTrackedByOperator(labels map[string]string) bool {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can it take in a runtime.Object as input instead of labels ? That would be more meaningful as the method checks if a given resource is tracked by operator or not.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@anandrkskd - can we incorporate this suggestion aswell?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to add some tests for the wrapper?

| `REMOVE_MANAGED_BY_LABEL_ON_ARGOCD_DELETION` | false | When an Argo CD instance is deleted, namespaces managed by that instance (via the `argocd.argoproj.io/managed-by` label ) will retain the label by default. Users can change this behavior by setting the environment variable `REMOVE_MANAGED_BY_LABEL_ON_ARGOCD_DELETION` to `true` in the Subscription. |
| `ARGOCD_LABEL_SELECTOR` | none | The label selector can be set on argocd-opertor by exporting `ARGOCD_LABEL_SELECTOR` (eg: `export ARGOCD_LABEL_SELECTOR=foo=bar`). The labels can be added to the argocd instances using the command `kubectl label argocd test1 foo=bar -n test-argocd`. This will enable the operator instance to be tailored to oversee only the corresponding ArgoCD instances having the matching label selector. |
| `LOG_LEVEL` | info | This sets the logging level of the manager (operator) pod. Valid values are "debug", "info", "warn", "error", "panic" and "fatal". |
| `MEMORY_OPTIMIZATION_ENABLED` | true | Enables memory optimization by stripping data from Secrets and ConfigMaps that are not tracked by the operator, reducing memory usage. Set to `false` to disable this optimization. |
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
| `MEMORY_OPTIMIZATION_ENABLED` | true | Enables memory optimization by stripping data from Secrets and ConfigMaps that are not tracked by the operator, reducing memory usage. Set to `false` to disable this optimization. |
| `DISABLE_MEMORY_OPTIMIZATION` | false | When set to `true`, disables the memory optimization that strips data from Secrets and ConfigMaps that are not tracked by the operator. This optimization helps reduce memory usage. |

if watchedNsCache := getDefaultWatchedNamespacesCacheOptions(); watchedNsCache != nil {
// Use transformers to strip data from Secrets and ConfigMaps
// that are not tracked by the operator to reduce memory usage.
if strings.ToLower(os.Getenv("MEMORY_OPTIMIZATION_ENABLED")) != "false" {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets use DISABLE_MEMORY_OPTIMIZATION env var which users should explicitly set to true to disable optimization

Comment on lines +212 to +223
liveClient, err := crclient.New(ctrl.GetConfigOrDie(), crclient.Options{Scheme: mgr.GetScheme()})
if err != nil {
setupLog.Error(err, "unable to create live client")
os.Exit(1)
}

// Wraps the controller runtime's default client to provide:
// 1. Fallback to the live client when a Secret/ConfigMap is stripped in the cache.
// 2. Automatic labeling of fetched objects, so they are retained in full form
// in subsequent cache updates and avoid repeated live lookups.
client := cw.NewClientWrapper(mgr.GetClient(), liveClient)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic can also be placed behind the DISABLE_MEMORY_OPTIMIZATION check. Use the default client := mgr.GetClient() when memory optimization is disabled.

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.

3 participants