概要: client-go 内置了一个基于 SharedInformer 的 读穿透(read-through)、写旁路(write-around) 缓存层。 GET / LIST 请求命中内存中的 Indexer,任何 写操作 则直发 API Server。这可以将控制器的读取延迟降低约 90 %,但要避免读到脏数据或掉进性能坑,你必须搞懂 同步窗口(sync window)深拷贝 以及 字段索引(field index)


为什么要用缓存? Link to heading

纯 REST(无缓存)使用 DelegatingClient
每次 Get/List → HTTPS → API Server → etcd进程内 Map 查询(0 RTT)
容易触发 QPS / watch 节流读开销近乎 0,API Server 几乎无压力
集群繁忙时尾延迟 ~100 ms典型延迟 < 1 µs

控制器以“读”为主:比对 期望实际 状态,只偶尔写入。缓存把这些读取摊平到内存。


关键组件 Link to heading

flowchart LR A[Reflector] --> B(DeltaFIFO) B --> C[Indexer] C -->|Read| D(Cache Client) D -->|Write| E(API Server)
  1. Reflector —— 从 API Server 流式拉取 ADD / UPDATE / DELETE 事件
  2. DeltaFIFO —— 缓冲事件,保证处理 Goroutine 不阻塞 watch
  3. Store / Indexer —— 线程安全 Map,以 namespace/name 为键,可加二级索引
  4. 缓存客户端(DelegatingClient
    • GetList → Store
    • CreateUpdatePatchDelete → 直发 REST

真正的调用链 —— c.Get(ctx, key, obj) Link to heading

func (c *delegatingClient) Get(ctx context.Context,
                               key client.ObjectKey,
                               obj client.Object) error {
    // 1. 先从缓存 deepCopy 数据到 obj
    if err := c.cache.Get(ctx, key, obj); err == nil {
        return nil
    }
    // 2. 缓存未同步时回退到直连 API Server
    return c.client.Get(ctx, key, obj)
}

同步窗口(Sync Window) Link to heading

  • 启动时缓存 为空
  • 首次 LIST+WATCH 完成后 HasSynced() 变为 true
  • 期间所有读都会回退到 REST
mgr.GetCache().WaitForCacheSync(ctx)

字段索引 = O(1) 二级查询 Link to heading

想找被某个 Service 选中的所有 Pod?

mgr.GetFieldIndexer().IndexField(
    &corev1.Pod{},       // 资源类型
    ".spec.nodeName",    // 字段路径
    func(obj client.Object) []string {
        pod := obj.(*corev1.Pod)
        return []string{pod.Spec.NodeName}
})

之后:

var pods corev1.PodList
_ = r.List(ctx, &pods,
           client.MatchingFields{".spec.nodeName": req.NodeName})

无需网络往返,Indexer 维护了反向索引。


写操作刻意绕过缓存 Link to heading

Create / Patch 不会 立即反映到本地缓存。 Informer 会在几毫秒后监听到自己的变更并更新 Store。 👉 把缓存视为最终一致,Reconcile 循环务必幂等。


性能评估 Link to heading

操作直连 REST缓存命中提速
单个 Get Pod6.8 ms p9535 µs p95-99.5 %
List 1000 个 Pods140 ms p951.7 ms p95-98.8 %

环境:3 节点 KIND 集群 · Go 1.22 · client-go 0.30.0


常见的坑 Link to heading

症状可能原因快速修复
读到旧数据少了 deep-copy 或比较了指针修改前先 obj.DeepCopy();切勿把指针塞进 Map
内存飙升LIST 返回过大,塞满 Store为 Informer 加 Label 选择器;缩短 resync 周期
偶发缓存未命中类型未注册到 Schemeinit()schemeBuilder.AddToScheme

总结 Link to heading

  • 所有 GET/LIST 都用缓存;不要混用自定义 REST Client
  • 启动时一定等 HasSynced()
  • 字段索引越早加越好,成本几乎为零
  • 记住缓存最终一致,Reconcile 需幂等、可重试