Skip to content

Commit 5ed2645

Browse files
authored
[CHAOS-76][FIX] Replace per-disruption caches with shared caches to reduce Watch connections (#1082)
perf(watchers): share caches across disruptions Each active Disruption opened two Watch connections to the API server — one per-disruption chaos pod cache and one per-disruption target pod/node cache. At scale this caused goroutine bloat, memory pressure, and excessive load on the API server. Replace per-disruption chaos pod caches with a single SharedChaosPodHandler registered on the manager's existing informer. Replace per-disruption target caches with a NamespaceCachePool so disruptions in the same namespace share one cache. Watch connections drop from O(disruptions) to O(namespaces). Also fixes stale handler accumulation on shared informers (source.Kind discards its registration), relabel-out reconcile missed by the old enqueue path, and duplicate HA events emitted by standby replicas. Jira: CHAOS-7
1 parent 89dd52e commit 5ed2645

15 files changed

Lines changed: 741 additions & 178 deletions

controllers/disruption_controller.go

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ import (
3535
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3636
"k8s.io/apimachinery/pkg/runtime"
3737
"k8s.io/apimachinery/pkg/types"
38-
kubeinformers "k8s.io/client-go/informers"
3938
"k8s.io/client-go/tools/record"
4039
"k8s.io/client-go/util/workqueue"
4140
ctrl "sigs.k8s.io/controller-runtime"
@@ -713,7 +712,7 @@ func (r *DisruptionReconciler) createChaosPods(ctx context.Context, instance *ch
713712
case chaostypes.DisruptionLevelPod:
714713
pod := corev1.Pod{}
715714

716-
if err = r.Client.Get(ctx, types.NamespacedName{Namespace: instance.Namespace, Name: target}, &pod); err != nil {
715+
if err = r.APIReader.Get(ctx, types.NamespacedName{Namespace: instance.Namespace, Name: target}, &pod); err != nil {
717716
return fmt.Errorf("error getting target to inject: %w", err)
718717
}
719718

@@ -1090,7 +1089,7 @@ func (r *DisruptionReconciler) getSelectorMatchingTargets(instance *chaosv1beta1
10901089
// select either pods or nodes depending on the disruption level
10911090
switch instance.Spec.Level {
10921091
case chaostypes.DisruptionLevelPod:
1093-
pods, totalCount, err := r.TargetSelector.GetMatchingPodsOverTotalPods(r.Client, instance)
1092+
pods, totalCount, err := r.TargetSelector.GetMatchingPodsOverTotalPods(r.APIReader, instance)
10941093
if err != nil {
10951094
return nil, 0, fmt.Errorf("can't get pods matching the given label selector: %w", err)
10961095
}
@@ -1101,7 +1100,7 @@ func (r *DisruptionReconciler) getSelectorMatchingTargets(instance *chaosv1beta1
11011100

11021101
totalAvailableTargetsCount = totalCount
11031102
case chaostypes.DisruptionLevelNode:
1104-
nodes, totalCount, err := r.TargetSelector.GetMatchingNodesOverTotalNodes(r.Client, instance)
1103+
nodes, totalCount, err := r.TargetSelector.GetMatchingNodesOverTotalNodes(r.APIReader, instance)
11051104
if err != nil {
11061105
return nil, 0, fmt.Errorf("can't get nodes matching the given label selector: %w", err)
11071106
}
@@ -1190,15 +1189,15 @@ func (r *DisruptionReconciler) recordEventOnTarget(ctx context.Context, instance
11901189
case chaostypes.DisruptionLevelPod:
11911190
p := &corev1.Pod{}
11921191

1193-
if err := r.Client.Get(ctx, types.NamespacedName{Namespace: instance.Namespace, Name: target}, p); err != nil {
1192+
if err := r.APIReader.Get(ctx, types.NamespacedName{Namespace: instance.Namespace, Name: target}, p); err != nil {
11941193
r.log.Errorw("event failed to be registered on target", tagutil.ErrorKey, err, tagutil.TargetNameKey, target)
11951194
}
11961195

11971196
o = p
11981197
case chaostypes.DisruptionLevelNode:
11991198
n := &corev1.Node{}
12001199

1201-
if err := r.Client.Get(ctx, types.NamespacedName{Name: target}, n); err != nil {
1200+
if err := r.APIReader.Get(ctx, types.NamespacedName{Name: target}, n); err != nil {
12021201
r.log.Errorw("event failed to be registered on target", tagutil.ErrorKey, err, tagutil.TargetNameKey, target)
12031202
}
12041203

@@ -1211,7 +1210,7 @@ func (r *DisruptionReconciler) recordEventOnTarget(ctx context.Context, instance
12111210
}
12121211

12131212
// SetupWithManager setups the current reconciler with the given manager
1214-
func (r *DisruptionReconciler) SetupWithManager(mgr ctrl.Manager, kubeInformerFactory kubeinformers.SharedInformerFactory) (controller.Controller, error) {
1213+
func (r *DisruptionReconciler) SetupWithManager(mgr ctrl.Manager) (controller.Controller, error) {
12151214
podToDisruption := func(ctx context.Context, d *corev1.Pod) []reconcile.Request {
12161215
// podtoDisruption is a function that maps pods to disruptions. it is meant to be used as an event handler on a pod informer
12171216
// this function should safely return an empty list of requests to reconcile if the object we receive is not actually a chaos pod
@@ -1232,12 +1231,38 @@ func (r *DisruptionReconciler) SetupWithManager(mgr ctrl.Manager, kubeInformerFa
12321231
return []reconcile.Request{{NamespacedName: types.NamespacedName{Name: name, Namespace: namespace}}}
12331232
}
12341233

1235-
return ctrl.NewControllerManagedBy(mgr).
1234+
cont, err := ctrl.NewControllerManagedBy(mgr).
12361235
For(&chaosv1beta1.Disruption{}).
12371236
WithOptions(controller.Options{RateLimiter: workqueue.NewTypedItemExponentialFailureRateLimiter[reconcile.Request](time.Second, time.Hour)}).
12381237
WatchesRawSource(source.Kind(mgr.GetCache(), &corev1.Pod{}, handler.TypedEnqueueRequestsFromMapFunc(podToDisruption))).
12391238
WithEventFilter(chaosEventsPredicate()).
12401239
Build(r)
1240+
if err != nil {
1241+
return nil, err
1242+
}
1243+
1244+
// Register a single shared chaos pod handler on the manager's pod informer.
1245+
// This replaces per-disruption ChaosPodWatcher caches: all chaos pod events are
1246+
// handled by one shared handler that dispatches by reading disruption labels from
1247+
// the pod at event time.
1248+
podInformer, err := mgr.GetCache().GetInformer(context.Background(), &corev1.Pod{})
1249+
if err != nil {
1250+
return nil, fmt.Errorf("SetupWithManager: failed to get pod informer: %w", err)
1251+
}
1252+
1253+
sharedHandler := watchers.NewSharedChaosPodHandler(
1254+
r.APIReader,
1255+
r.Recorder,
1256+
r.BaseLog,
1257+
watchers.NewWatcherMetricsAdapter(r.MetricsSink, r.BaseLog),
1258+
mgr.Elected(), // gate event/metric emission on leadership in HA deployments
1259+
)
1260+
1261+
if _, err := podInformer.AddEventHandler(sharedHandler); err != nil {
1262+
return nil, fmt.Errorf("SetupWithManager: failed to register shared chaos pod handler: %w", err)
1263+
}
1264+
1265+
return cont, nil
12411266
}
12421267

12431268
// chaosEventsPredicate determines if given event is a chaos related one or not

main.go

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import (
4949
"k8s.io/client-go/tools/cache"
5050

5151
ctrl "sigs.k8s.io/controller-runtime"
52+
ctrlcache "sigs.k8s.io/controller-runtime/pkg/cache"
5253
"sigs.k8s.io/controller-runtime/pkg/client"
5354
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
5455
"sigs.k8s.io/controller-runtime/pkg/webhook"
@@ -132,6 +133,19 @@ func main() {
132133
Port: cfg.Controller.Webhook.Port,
133134
CertDir: cfg.Controller.Webhook.CertDir,
134135
}),
136+
Cache: ctrlcache.Options{
137+
DefaultTransform: ctrlcache.TransformStripManagedFields(),
138+
// Restrict pod watch to the chaos namespace only. The controller only
139+
// needs pod events for chaos pods (all in ChaosNamespace). Target pods
140+
// in user namespaces are read via APIReader on the reconcile path.
141+
ByObject: map[client.Object]ctrlcache.ByObject{
142+
&corev1.Pod{}: {
143+
Namespaces: map[string]ctrlcache.Config{
144+
cfg.Injector.ChaosNamespace: {},
145+
},
146+
},
147+
},
148+
},
135149
})
136150
if err != nil {
137151
logger.Fatalw("unable to start manager", tags.ErrorKey, err)
@@ -204,6 +218,7 @@ func main() {
204218

205219
chaosPodService, err := services.NewChaosPodService(services.ChaosPodServiceConfig{
206220
Client: mgr.GetClient(),
221+
Reader: mgr.GetAPIReader(),
207222
ChaosNamespace: cfg.Injector.ChaosNamespace,
208223
TargetSelector: targetSelector,
209224
Injector: services.ChaosPodServiceInjectorConfig{
@@ -244,9 +259,8 @@ func main() {
244259
}
245260

246261
informerClient := kubernetes.NewForConfigOrDie(ctrl.GetConfigOrDie())
247-
kubeInformerFactory := kubeinformers.NewSharedInformerFactoryWithOptions(informerClient, time.Hour*24, kubeinformers.WithNamespace(cfg.Injector.ChaosNamespace))
248262

249-
cont, err := disruptionReconciler.SetupWithManager(mgr, kubeInformerFactory)
263+
cont, err := disruptionReconciler.SetupWithManager(mgr)
250264
if err != nil {
251265
logger.Fatalw("unable to create controller", tags.ControllerKey, chaosv1beta1.DisruptionKind, tags.ErrorKey, err)
252266
}
@@ -257,6 +271,7 @@ func main() {
257271
Reader: mgr.GetAPIReader(),
258272
Recorder: disruptionReconciler.Recorder,
259273
ChaosNamespace: cfg.Injector.ChaosNamespace,
274+
CachePool: watchers.NewNamespaceCachePool(ctrl.GetConfigOrDie()),
260275
}
261276
watcherFactory := watchers.NewWatcherFactory(watchersFactoryConfig)
262277
disruptionReconciler.DisruptionsWatchersManager = watchers.NewDisruptionsWatchersManager(cont, watcherFactory, mgr.GetAPIReader())
@@ -286,8 +301,9 @@ func main() {
286301

287302
defer cancel()
288303

289-
stopCh := make(chan struct{})
290-
kubeInformerFactory.Start(stopCh)
304+
// Buffered so the send at mgr.Start failure doesn't block when rollout informers
305+
// are disabled and nobody is reading the channel.
306+
stopCh := make(chan struct{}, 1)
291307

292308
go disruptionReconciler.ReportMetrics(ctx)
293309

services/chaospod.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ type ChaosPodServiceInjectorConfig struct {
8080
// ChaosPodServiceConfig contains configuration options for the chaosPodService.
8181
type ChaosPodServiceConfig struct {
8282
Client client.Client // Kubernetes client for interacting with the API server.
83+
Reader client.Reader // Uncached reader for target pods in user namespaces.
8384
ChaosNamespace string // Namespace where chaos-related resources are located.
8485
TargetSelector targetselector.TargetSelector // Target selector for selecting target pods.
8586
Injector ChaosPodServiceInjectorConfig // Configuration options for the injector.
@@ -110,6 +111,13 @@ func NewChaosPodService(config ChaosPodServiceConfig) (ChaosPodService, error) {
110111
return nil, fmt.Errorf("you must provide a non nil Kubernetes client")
111112
}
112113

114+
// Default Reader to Client for backwards compatibility: callers that do not set
115+
// Reader explicitly still get a working implementation (cached reads) rather than
116+
// a nil-pointer panic during TargetIsHealthy / HandleOrphanedChaosPods.
117+
if config.Reader == nil {
118+
config.Reader = config.Client
119+
}
120+
113121
return &chaosPodService{
114122
config: config,
115123
}, nil
@@ -141,7 +149,7 @@ func (m *chaosPodService) HandleChaosPodTermination(ctx context.Context, disrupt
141149
target := chaosPod.Labels[chaostypes.TargetLabel]
142150

143151
// Check if the target of the disruption is healthy (running and ready).
144-
if err := m.config.TargetSelector.TargetIsHealthy(target, m.config.Client, disruption); err != nil {
152+
if err := m.config.TargetSelector.TargetIsHealthy(target, m.config.Reader, disruption); err != nil {
145153
// return the error unless we have a specific reason to ignore it.
146154
if !apierrors.IsNotFound(err) && chaosPodAllowedErrors.isNotAllowed(strings.ToLower(err.Error())) {
147155
return false, err
@@ -412,7 +420,7 @@ func (m *chaosPodService) HandleOrphanedChaosPods(ctx context.Context, req ctrl.
412420
logger.Infow("checking if we can clean up orphaned chaos pod", tagutil.ChaosPodNameKey, pod.Name, tagutil.TargetNameKey, target)
413421

414422
// if target doesn't exist, we can try to clean up the chaos pod
415-
if err = m.config.Client.Get(ctx, types.NamespacedName{Name: target, Namespace: req.Namespace}, &p); apierrors.IsNotFound(err) {
423+
if err = m.config.Reader.Get(ctx, types.NamespacedName{Name: target, Namespace: req.Namespace}, &p); apierrors.IsNotFound(err) {
416424
logger.Warnw("orphaned chaos pod detected, will attempt to delete", tagutil.ChaosPodNameKey, pod.Name)
417425

418426
if err = m.removeFinalizerForChaosPod(ctx, &pod); err != nil {

services/chaospod_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ var _ = Describe("Chaos Pod Service", func() {
8888
if chaosPodServiceConfig.Client == nil {
8989
chaosPodServiceConfig.Client = k8sClientMock
9090
}
91+
if chaosPodServiceConfig.Reader == nil {
92+
chaosPodServiceConfig.Reader = k8sClientMock
93+
}
9194

9295
// Action
9396
chaosPodService, err = services.NewChaosPodService(chaosPodServiceConfig)

targetselector/running_target_selector.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func NewRunningTargetSelector(controllerEnableSafeguards bool, controllerNodeNam
3333
}
3434

3535
// GetMatchingPodsOverTotalPods returns a pods list containing all running pods matching the given label selector and namespace and the count of pods matching the selector
36-
func (r runningTargetSelector) GetMatchingPodsOverTotalPods(c client.Client, instance *chaosv1beta1.Disruption) (*corev1.PodList, int, error) {
36+
func (r runningTargetSelector) GetMatchingPodsOverTotalPods(c client.Reader, instance *chaosv1beta1.Disruption) (*corev1.PodList, int, error) {
3737
// get parsed selector
3838
selector, err := GetLabelSelectorFromInstance(instance)
3939
if err != nil {
@@ -115,7 +115,7 @@ podLoop:
115115
}
116116

117117
// GetMatchingNodesOverTotalNodes returns a nodes list containing all nodes matching the given label selector and the count of nodes matching the selector
118-
func (r runningTargetSelector) GetMatchingNodesOverTotalNodes(c client.Client, instance *chaosv1beta1.Disruption) (*corev1.NodeList, int, error) {
118+
func (r runningTargetSelector) GetMatchingNodesOverTotalNodes(c client.Reader, instance *chaosv1beta1.Disruption) (*corev1.NodeList, int, error) {
119119
// get parsed selector
120120
selector, err := GetLabelSelectorFromInstance(instance)
121121
if err != nil {
@@ -174,7 +174,7 @@ nodeLoop:
174174
}
175175

176176
// TargetIsHealthy returns an error if the given target is unhealthy or does not exist
177-
func (r runningTargetSelector) TargetIsHealthy(target string, c client.Client, instance *chaosv1beta1.Disruption) error {
177+
func (r runningTargetSelector) TargetIsHealthy(target string, c client.Reader, instance *chaosv1beta1.Disruption) error {
178178
switch instance.Spec.Level {
179179
case chaostypes.DisruptionLevelPod:
180180
var p corev1.Pod

targetselector/target_selector.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@ import (
1818
// TargetSelector is an interface for applying network disruptions to a Kubernetes Cluster
1919
type TargetSelector interface {
2020
// GetMatchingPodsOverTotalPods Returns list of matching ready and untargeted pods and number of total pods
21-
GetMatchingPodsOverTotalPods(c client.Client, instance *chaosv1beta1.Disruption) (*corev1.PodList, int, error)
21+
GetMatchingPodsOverTotalPods(c client.Reader, instance *chaosv1beta1.Disruption) (*corev1.PodList, int, error)
2222

2323
// GetMatchingNodesOverTotalNodes Returns list of matching ready and untargeted nodes and number of total nodes
24-
GetMatchingNodesOverTotalNodes(c client.Client, instance *chaosv1beta1.Disruption) (*corev1.NodeList, int, error)
24+
GetMatchingNodesOverTotalNodes(c client.Reader, instance *chaosv1beta1.Disruption) (*corev1.NodeList, int, error)
2525

2626
// TargetIsHealthy Returns an error if the given target is unhealthy or does not exist
27-
TargetIsHealthy(target string, c client.Client, instance *chaosv1beta1.Disruption) error
27+
TargetIsHealthy(target string, c client.Reader, instance *chaosv1beta1.Disruption) error
2828
}
2929

3030
// ValidateLabelSelector assert label selector matches valid grammar, avoids CORE-414

0 commit comments

Comments
 (0)