You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The controller currently cannot run with more than 1 replica because it uses in-memory state for gRPC stream coordination:
listenQueues (sync.Map) — pairs Dial and Listen calls via in-memory channels. When an exporter calls Listen, it blocks on a channel. When a client calls Dial, it sends a JWT token to that channel. If Dial and Listen land on different pods, the connection pairing breaks silently.
leaseLocks (sync.Map) — ref-counted mutexes for lease operations, only valid within a single process.
Additionally, the ControllerService writes directly to the K8s API server (patches Exporters, creates Leases, rotates Secrets), and the Status streaming method patches Exporter.status.lastSeen every 10 seconds. Multiple replicas would create write conflicts.
The ControllerService does not implement NeedLeaderElection(), so it defaults to true (only runs on the leader). This means with multiple replicas, only the leader serves gRPC traffic, but:
Non-leader pods report ready (with healthz.Ping), so K8s routes ~50% of gRPC traffic to pods that aren't serving — causing connection refused errors
Only the leader reconciles; standby replicas are passive
gRPC service deployment (stateless, multiple replicas)
ControllerService and ClientService gRPC handlers
Move Dial/Listen coordination to an external store (Redis, or a K8s-native mechanism like a ConfigMap-based rendezvous, or keep a single-writer pattern)
Read-only methods (ListLeases, GetExporter, etc.) can safely run on any replica
Write methods use the K8s API server directly (optimistic concurrency handles conflicts)
These already run on all replicas, no changes needed
Alternative: keep single deployment, fix stream coordination
Instead of splitting, the Dial/Listen rendezvous could be moved to a shared data store or use the K8s API server itself (e.g., create a short-lived CR or annotation that the exporter watches). This avoids a new deployment but adds complexity.
Problem
The controller currently cannot run with more than 1 replica because it uses in-memory state for gRPC stream coordination:
listenQueues(sync.Map) — pairsDialandListencalls via in-memory channels. When an exporter callsListen, it blocks on a channel. When a client callsDial, it sends a JWT token to that channel. IfDialandListenland on different pods, the connection pairing breaks silently.leaseLocks(sync.Map) — ref-counted mutexes for lease operations, only valid within a single process.Additionally, the
ControllerServicewrites directly to the K8s API server (patches Exporters, creates Leases, rotates Secrets), and theStatusstreaming method patchesExporter.status.lastSeenevery 10 seconds. Multiple replicas would create write conflicts.The
ControllerServicedoes not implementNeedLeaderElection(), so it defaults totrue(only runs on the leader). This means with multiple replicas, only the leader serves gRPC traffic, but:healthz.Ping), so K8s routes ~50% of gRPC traffic to pods that aren't serving — causing connection refused errorsCurrent Mitigation
The operator now clamps
controller.replicasto 1 with a warning event. See the PR that introduced this.Proposed Solution
Split the controller into separate concerns:
Reconciler deployment (needs leader election, 1 active replica)
ExporterReconciler,ClientReconciler,LeaseReconcilergRPC service deployment (stateless, multiple replicas)
ControllerServiceandClientServicegRPC handlersListLeases,GetExporter, etc.) can safely run on any replicaLogin/OIDC/Dashboard (already stateless, already
NeedLeaderElection() → false)Alternative: keep single deployment, fix stream coordination
Instead of splitting, the Dial/Listen rendezvous could be moved to a shared data store or use the K8s API server itself (e.g., create a short-lived CR or annotation that the exporter watches). This avoids a new deployment but adds complexity.
References