如何配置client-go informer仅缓存DaemonSet所属Pod降低内存占用
原生client-go的NewFilteredSharedInformerFactory仅支持通过TweakListOptionsFunc传递API服务端可识别的labelSelector/fieldSelector参数,不支持本地自定义谓词过滤入缓存对象,针对「仅缓存携带DaemonSet类型ownerReference的Pod」的需求,最稳妥无侵入的方案是自定义带过滤逻辑的Indexer包装层,替换Informer默认使用的缓存存储实现。
核心实现逻辑
Informer的内存缓存由cache.Indexer接口承载,所有全量同步、增量更新的对象都会先写入Indexer再触发事件回调。你可以包装原生Indexer实现,重写所有写入类方法,仅将符合谓词规则的对象存入底层存储,从根源减少缓存内存占用。
1. 定义过滤谓词
先实现Pod归属判断逻辑,兼容Informer删除事件的DeletedFinalStateUnknown兜底场景:
func isDaemonSetPod(obj interface{}) bool { pod, ok := obj.(*corev1.Pod) if !ok { tombstone, ok := obj.(cache.DeletedFinalStateUnknown) if !ok { return false } pod, ok = tombstone.Obj.(*corev1.Pod) if !ok { return false } } for _, ref := range pod.OwnerReferences { if ref.Kind == "DaemonSet" && ref.APIVersion == "apps/v1" { return true } } return false }
2. 实现带过滤能力的Indexer包装
重写Add、Update、Replace三个写入方法,不符合规则的对象直接丢弃,不进入底层存储;Delete方法直接透传,避免残留脏数据:
type FilteredIndexer struct { cache.Indexer predicate func(obj interface{}) bool } func (f *FilteredIndexer) Add(obj interface{}) error { if !f.predicate(obj) { return nil } return f.Indexer.Add(obj) } func (f *FilteredIndexer) Update(obj interface{}) error { if !f.predicate(obj) { key, err := cache.MetaNamespaceKeyFunc(obj) if err == nil { _, exists, _ := f.Indexer.GetByKey(key) if exists { return f.Indexer.Delete(obj) } } return nil } return f.Indexer.Update(obj) } func (f *FilteredIndexer) Replace(objects []interface{}, rv string) error { filteredObjs := make([]interface{}, 0, len(objects)) for _, obj := range objects { if f.predicate(obj) { filteredObjs = append(filteredObjs, obj) } } return f.Indexer.Replace(filteredObjs, rv) }
注意必须重写
Replace方法:Informer首次启动全量List、以及重连全量同步时都会调用该方法写入全量对象,不重写会导致全量非目标Pod被塞入缓存。
3. 初始化Informer时传入自定义Indexer
不要使用默认SharedInformerFactory生成的Pod Informer,手动创建SharedIndexInformer,传入包装好的FilteredIndexer作为缓存存储:
// 初始化基础Indexer baseIndexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{ cache.NamespaceIndex: cache.MetaNamespaceIndexFunc, }) // 包装为带DaemonSet Pod过滤的Indexer filteredIndexer := &FilteredIndexer{ Indexer: baseIndexer, predicate: isDaemonSetPod, } // 创建自定义Pod Informer podInformer := cache.NewSharedIndexInformer( &cache.ListWatch{ ListFunc: func(opts metav1.ListOptions) (runtime.Object, error) { return clientset.CoreV1().Pods("").List(context.TODO(), opts) }, WatchFunc: func(opts metav1.ListOptions) (watch.Interface, error) { return clientset.CoreV1().Pods("").Watch(context.TODO(), opts) }, }, &corev1.Pod{}, 0, // 按需设置resync周期 filteredIndexer, )
4. 事件回调层补充过滤
由于Informer的事件回调触发逻辑在对象写入Indexer之前,需要配合client-go原生提供的cache.FilteringResourceEventHandler包装你的业务处理逻辑,确保回调仅收到符合规则的Pod事件:
podInformer.AddEventHandler(cache.FilteringResourceEventHandler{ FilterFunc: isDaemonSetPod, Handler: cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { // 业务处理逻辑,入参一定是DaemonSet归属的Pod }, UpdateFunc: func(oldObj, newObj interface{}) { // 业务处理逻辑 }, DeleteFunc: func(obj interface{}) { // 业务处理逻辑 }, }, })
其他可选方案
- 对象裁剪方案:如果使用client-go v0.27及以上版本,可以通过
NewSharedInformerFactoryWithOptions的WithTransform选项注册转换函数,在对象入缓存前裁减掉非必要字段(比如Pod的大段Status、冗余Annotation等),可降低30%-50%内存占用,但无法完全丢弃非目标Pod,内存优化效果弱于自定义Indexer方案。 - 服务端过滤方案:如果有权限修改集群内所有DaemonSet的Pod模板,可给DaemonSet管控的Pod统一注入固定标识标签(比如
workload-owner=daemonset),后续直接通过TweakListOptionsFunc设置labelSelector=workload-owner=daemonset,让API Server直接仅返回目标Pod,可同时降低List/Watch的网络传输开销和本地内存占用,但对存量集群侵入性较高。
常见误区
仅在事件回调层加过滤逻辑、或者仅使用FilteringResourceEventHandler无法降低内存占用,所有API Server返回的Pod对象依然会全量存入Informer的Indexer缓存。
内容的提问来源于stack exchange,提问作者ahmet alp balkan

