Skip to content

Commit e57940c

Browse files
authored
feat(node-drainer): reduce node-drainer informer cache footprint by removing unused fields from node/pod (#1615)
Signed-off-by: Ajay Mishra <ajmishra@nvidia.com>
1 parent 4d95c0b commit e57940c

5 files changed

Lines changed: 528 additions & 17 deletions

File tree

node-drainer/pkg/evaluator/evaluator_integration_test.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,14 @@ func setupDirectTest(t *testing.T, userNamespaces []config.UserNamespace, dryRun
108108
PartialDrainEnabled: partialDrainEnabled,
109109
}
110110

111-
informersInstance, err := informers.NewInformers(client, 1*time.Minute, ptr.To(2), false, dryRun)
111+
informersInstance, err := informers.NewInformers(
112+
client,
113+
1*time.Minute,
114+
ptr.To(2),
115+
false,
116+
dryRun,
117+
tomlConfig.SystemNamespaces,
118+
)
112119
require.NoError(t, err)
113120
go func() { _ = informersInstance.Run(ctx) }()
114121
require.Eventually(t, informersInstance.HasSynced, 30*time.Second, 1*time.Second)

node-drainer/pkg/informers/informers.go

Lines changed: 173 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,15 @@ import (
3131
"k8s.io/apimachinery/pkg/api/errors"
3232
"k8s.io/apimachinery/pkg/api/resource"
3333
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
34+
"k8s.io/apimachinery/pkg/types"
3435
"k8s.io/client-go/informers"
3536
"k8s.io/client-go/kubernetes"
3637
"k8s.io/client-go/tools/cache"
3738
"k8s.io/utils/ptr"
3839

3940
"github.com/nvidia/nvsentinel/data-models/pkg/model"
4041
"github.com/nvidia/nvsentinel/data-models/pkg/protos"
42+
"github.com/nvidia/nvsentinel/fault-quarantine/pkg/common"
4143
"github.com/nvidia/nvsentinel/node-drainer/pkg/metrics"
4244
)
4345

@@ -59,15 +61,24 @@ type Informers struct {
5961
}
6062

6163
func NewInformers(clientset kubernetes.Interface, resyncPeriod time.Duration,
62-
notReadyTimeoutMinutes *int, drainGPUPods bool, dryRun bool) (*Informers, error) {
64+
notReadyTimeoutMinutes *int, drainGPUPods bool, dryRun bool, systemNamespaces string) (*Informers, error) {
6365
informerFactory := informers.NewSharedInformerFactoryWithOptions(
6466
clientset,
6567
resyncPeriod,
6668
)
6769

6870
podInformer := informerFactory.Core().V1().Pods().Informer()
6971

70-
err := podInformer.GetIndexer().AddIndexers(
72+
systemNamespacesRegex, err := compileExcludePattern(systemNamespaces)
73+
if err != nil {
74+
return nil, fmt.Errorf("failed to compile system namespaces regex: %w", err)
75+
}
76+
77+
if err := podInformer.SetTransform(excludedPodTransform(systemNamespacesRegex)); err != nil {
78+
return nil, fmt.Errorf("failed to set pod informer transform: %w", err)
79+
}
80+
81+
err = podInformer.GetIndexer().AddIndexers(
7182
cache.Indexers{
7283
NodeIndex: NodeIndexFunc,
7384
NamespaceNodeIndex: NamespaceNodeIndexFunc,
@@ -93,6 +104,9 @@ func NewInformers(clientset kubernetes.Interface, resyncPeriod time.Duration,
93104
}
94105

95106
nodeInformer := informerFactory.Core().V1().Nodes().Informer()
107+
if err := nodeInformer.SetTransform(nodeTransform); err != nil {
108+
return nil, fmt.Errorf("failed to set node informer transform: %w", err)
109+
}
96110

97111
dryRunMode := []string{}
98112
if dryRun {
@@ -111,6 +125,153 @@ func NewInformers(clientset kubernetes.Interface, resyncPeriod time.Duration,
111125
}, nil
112126
}
113127

128+
func excludedPodTransform(systemNamespacesRegex *regexp.Regexp) cache.TransformFunc {
129+
return func(obj any) (any, error) {
130+
pod, ok := obj.(*v1.Pod)
131+
if !ok {
132+
return obj, nil
133+
}
134+
135+
isSystemNamespace := systemNamespacesRegex != nil && systemNamespacesRegex.MatchString(pod.Namespace)
136+
if !isSystemNamespace && !isDaemonSetOwned(pod.OwnerReferences) {
137+
return drainEligiblePodCacheObject(pod), nil
138+
}
139+
140+
return &v1.Pod{
141+
ObjectMeta: identityObjectMeta(
142+
pod.Name,
143+
pod.Namespace,
144+
pod.UID,
145+
pod.ResourceVersion,
146+
nil,
147+
),
148+
}, nil
149+
}
150+
}
151+
152+
// drainEligiblePodCacheObject retains only fields used by pod indexes and drain decisions.
153+
// Keep this contract in sync with the cached Pod reads in this package.
154+
func drainEligiblePodCacheObject(pod *v1.Pod) *v1.Pod {
155+
var annotations map[string]string
156+
if devices, exists := pod.Annotations[model.PodDeviceAnnotationName]; exists {
157+
annotations = map[string]string{model.PodDeviceAnnotationName: devices}
158+
}
159+
160+
ownerReferences := make([]metav1.OwnerReference, len(pod.OwnerReferences))
161+
for idx, owner := range pod.OwnerReferences {
162+
ownerReferences[idx] = metav1.OwnerReference{Kind: owner.Kind}
163+
}
164+
165+
var deletionTimestamp *metav1.Time
166+
if pod.DeletionTimestamp != nil {
167+
deletionTimestamp = pod.DeletionTimestamp.DeepCopy()
168+
}
169+
170+
var terminationGracePeriodSeconds *int64
171+
if pod.Spec.TerminationGracePeriodSeconds != nil {
172+
terminationGracePeriodSeconds = ptr.To(*pod.Spec.TerminationGracePeriodSeconds)
173+
}
174+
175+
return &v1.Pod{
176+
TypeMeta: pod.TypeMeta,
177+
ObjectMeta: metav1.ObjectMeta{
178+
Name: pod.Name,
179+
Namespace: pod.Namespace,
180+
UID: pod.UID,
181+
ResourceVersion: pod.ResourceVersion,
182+
Annotations: annotations,
183+
OwnerReferences: ownerReferences,
184+
DeletionTimestamp: deletionTimestamp,
185+
},
186+
Spec: v1.PodSpec{
187+
NodeName: pod.Spec.NodeName,
188+
TerminationGracePeriodSeconds: terminationGracePeriodSeconds,
189+
Containers: trimContainers(pod.Spec.Containers),
190+
InitContainers: trimContainers(pod.Spec.InitContainers),
191+
},
192+
Status: v1.PodStatus{
193+
Phase: pod.Status.Phase,
194+
Conditions: trimPodReadyConditions(pod.Status.Conditions),
195+
},
196+
}
197+
}
198+
199+
func trimContainers(containers []v1.Container) []v1.Container {
200+
cached := make([]v1.Container, len(containers))
201+
for idx, container := range containers {
202+
limits := make(v1.ResourceList, len(container.Resources.Limits))
203+
for resourceName, quantity := range container.Resources.Limits {
204+
limits[resourceName] = quantity.DeepCopy()
205+
}
206+
207+
cached[idx].Resources.Limits = limits
208+
}
209+
210+
return cached
211+
}
212+
213+
func trimPodReadyConditions(conditions []v1.PodCondition) []v1.PodCondition {
214+
var cached []v1.PodCondition
215+
for _, condition := range conditions {
216+
if condition.Type != v1.PodReady {
217+
continue
218+
}
219+
220+
cached = append(cached, v1.PodCondition{
221+
Type: condition.Type,
222+
Status: condition.Status,
223+
LastTransitionTime: condition.LastTransitionTime,
224+
})
225+
}
226+
227+
return cached
228+
}
229+
230+
func nodeTransform(obj any) (any, error) {
231+
node, ok := obj.(*v1.Node)
232+
if !ok {
233+
return obj, nil
234+
}
235+
236+
var annotations map[string]string
237+
if quarantineHealthEvent, exists := node.Annotations[common.QuarantineHealthEventAnnotationKey]; exists {
238+
annotations = map[string]string{
239+
common.QuarantineHealthEventAnnotationKey: quarantineHealthEvent,
240+
}
241+
}
242+
243+
return &v1.Node{
244+
ObjectMeta: identityObjectMeta(
245+
node.Name,
246+
"",
247+
node.UID,
248+
node.ResourceVersion,
249+
annotations,
250+
),
251+
}, nil
252+
}
253+
254+
func identityObjectMeta(name, namespace string, uid types.UID, resourceVersion string,
255+
annotations map[string]string) metav1.ObjectMeta {
256+
return metav1.ObjectMeta{
257+
Name: name,
258+
Namespace: namespace,
259+
UID: uid,
260+
ResourceVersion: resourceVersion,
261+
Annotations: annotations,
262+
}
263+
}
264+
265+
func isDaemonSetOwned(ownerReferences []metav1.OwnerReference) bool {
266+
for _, owner := range ownerReferences {
267+
if owner.Kind == "DaemonSet" {
268+
return true
269+
}
270+
}
271+
272+
return false
273+
}
274+
114275
func (i *Informers) HasSynced() bool {
115276
return i.podInformer.HasSynced() && i.eventInformer.HasSynced() && i.nodeInformer.HasSynced()
116277
}
@@ -383,15 +544,13 @@ func (i *Informers) filterPodsWithGPURequests(pods []*v1.Pod) []*v1.Pod {
383544
}
384545

385546
func (i *Informers) isDaemonSetPod(pod *v1.Pod) bool {
386-
for _, owner := range pod.OwnerReferences {
387-
if owner.Kind == "DaemonSet" {
388-
slog.Info("Ignoring DaemonSet pod in namespace on node during eviction check",
389-
"pod", pod.Name,
390-
"namespace", pod.Namespace,
391-
"node", pod.Spec.NodeName)
547+
if isDaemonSetOwned(pod.OwnerReferences) {
548+
slog.Info("Ignoring DaemonSet pod in namespace on node during eviction check",
549+
"pod", pod.Name,
550+
"namespace", pod.Namespace,
551+
"node", pod.Spec.NodeName)
392552

393-
return true
394-
}
553+
return true
395554
}
396555

397556
return false
@@ -798,6 +957,10 @@ func (i *Informers) GetNamespacesMatchingPattern(ctx context.Context,
798957
}
799958

800959
func (i *Informers) compileExcludePattern(excludePattern string) (*regexp.Regexp, error) {
960+
return compileExcludePattern(excludePattern)
961+
}
962+
963+
func compileExcludePattern(excludePattern string) (*regexp.Regexp, error) {
801964
if excludePattern == "" {
802965
return nil, nil
803966
}

0 commit comments

Comments
 (0)