Kubernetes Informer 机制:从 client-go 源码理解本地缓存与事件分发
Informer 是 client-go 为 Kubernetes Controller 提供的“一次同步、持续观察、本地读取、多方分发”机制。它并不是 API Server 的事件总线:它先以 LIST 建立本地视图,再以 WATCH 追上后续变更;事件先更新本地缓存,再异步交给各个处理函数。Controller 因此可以在大多数读路径中访问内存,而不必对每次 Reconcile 都请求 API Server。^[来源:shared_informer.go(Kubernetes v1.34.0)]
本文以 Kubernetes v1.34.0 的
staging/src/k8s.io/client-go/tools/cache/为源码基线。Informer 的公开接口相对稳定,但DeltaFIFO、WatchList与 feature gate 的具体实现会随版本改变;使用其他client-go版本时,应回到对应 tag 的源码核对。
一、先建立正确的心智模型
一个 SharedIndexInformer 的源码注释直接给出了它的三段结构:
API Server
│ LIST + WATCH
▼
Reflector ──变更──> DeltaFIFO / Queue ──Pop──> HandleDeltas
│
┌────────────────────┴───────────────────┐
▼ ▼
Indexer(本地对象缓存) sharedProcessor(事件分发)
│
每个 handler 独立的 listener 队列
│
OnAdd / OnUpdate / OnDelete其中,indexer 是可按 key 或附加索引查询的本地缓存;controller 负责启动 Reflector 并从队列消费 Delta;sharedProcessor 把通知转交给所有已注册的 handler。sharedIndexInformer 明确把 DeltaFIFO 的 KnownObjects 指向自己的 indexer,从而让重列举与删除检测能依据当前本地视图进行。^[来源:shared_informer.go(Kubernetes v1.34.0)]
因此,Informer 的价值不只是“监听资源变化”,还包括:
- 用一个资源范围内共享的 List/Watch 流,避免多个消费者重复轮询 API Server;
- 提供可被 Lister 查询的本地缓存;
- 把短时间内同一对象的变化按 key 聚合,再按对象顺序处理;
- 隔离不同 handler 的消费速度,避免一个慢回调直接阻塞其他回调。
在 Controller / Operator 中,Informer 是观察集群状态的一层基础设施,而 Reconcile 或 workqueue 才是决定如何把实际状态收敛到期望状态的业务层。它也是 Kubebuilder 和 controller-runtime 通常会包装起来的底层能力。
二、入口:SharedIndexInformer.Run
sharedIndexInformer.RunWithContext 只允许运行一次。它创建队列和低层 Controller,启动 sharedProcessor,最后阻塞运行 controller.RunWithContext。在 v1.34.0 中,InOrderInformers feature gate 未启用时会创建 DeltaFIFO;启用后则改用 RealFIFO。这意味着下文围绕 DeltaFIFO 的解释描述的是该版本的一条实现路径,而不是不变的协议承诺。^[来源:shared_informer.go - RunWithContext]
fifo := cache.NewDeltaFIFOWithOptions(cache.DeltaFIFOOptions{
KnownObjects: indexer,
EmitDeltaTypeReplaced: true,
})
controller := cache.New(&cache.Config{
Queue: fifo,
ListerWatcher: lw,
Process: informer.HandleDeltas,
})上面的代码是源码结构的简化,不是应用层推荐的手工初始化方式。应用通常从 typed informer factory 获取 Informer;直接拼装时需要正确处理对象类型、索引、停止信号、错误处理和 resync。
三、Reflector:把 API 的 List/Watch 变成队列变更
低层 Controller.RunWithContext 创建一个 Reflector,并并发做两件事:一边让 Reflector 把 ListerWatcher 的对象与通知写入 Queue,另一边循环 Pop Queue 并调用 Config.Process。取消 context 时 Controller 会先关闭 Queue,使消费循环退出。^[来源:controller.go - Controller.RunWithContext]
Reflector.RunWithContext 会反复调用 ListAndWatchWithContext,失败后按退避策略重试。传统路径的顺序是:
LIST当前资源,取得 list 的resourceVersion;- 调用
Store.Replace(items, resourceVersion),把初始列表写入目标 Store; - 从该版本号开始
WATCH; - 将
ADDED、MODIFIED、DELETED等 watch 事件写到 Store,并更新最后观察到的版本号; - watch 中断、版本过期或连接异常时,重新建立同步,再继续观察。
resourceVersion 是连接 LIST 与 WATCH 的一致性游标,而不是业务版本号。重连时可能发生重新列举;因此你的 Controller 必须假设同一对象的事件会重复出现,不能把“收到一次 Update”当作只能执行一次的业务命令。^[来源:reflector.go - ListAndWatchWithContext、list、watch]
当前源码还支持由 WatchListClient feature gate 控制的 WatchList 路径:API Server 先以合成的 Added 事件流出初始快照,以 Bookmark 标记初始事件结束,随后复用同一条 watch 接收增量。失败时 Reflector 回退到传统 List/Watch。它优化的是初始化流与服务端资源使用,不改变上层“本地缓存 + 增量事件”的消费模型。^[来源:reflector.go - watchList]
四、DeltaFIFO:为何不是普通 channel
DeltaFIFO 维护两份关键状态:items map[string]Deltas 按对象 key 保存尚未处理的 Delta 序列,queue []string 则按 FIFO 顺序保存 key,且一个 key 在队列中最多出现一次。这样对象在等待消费期间连续更新时,不会让 key 重复排队;消费者拿到的是该对象积累的 Deltas,再按从旧到新的顺序应用。^[来源:delta_fifo.go - DeltaFIFO、Deltas]
Delta 类型的含义如下:
| Delta | 典型来源 | 上层效果 |
|---|---|---|
Added | 新增对象 | 缓存不存在时 OnAdd |
Updated | Watch 的修改事件 | 缓存已有对象时 OnUpdate |
Deleted | Watch 删除或重列举发现缺失 | 删除缓存并 OnDelete |
Replaced | 重新 List / 初始快照替换 | 按缓存是否存在转成 Add 或 Update |
Sync | 周期性 resync | 本地对象的合成 Update;仅分发给请求 resync 的 listener |
Replace 不只是把新列表塞入队列。它还会比较新列表与 knownObjects:原先在本地缓存里、但这次 list 中不存在的 key,会生成删除 Delta。若此前 watch 断开并错过删除事件,这一步可以最终修复缓存;此时对象可能以 DeletedFinalStateUnknown tombstone 形式出现,回调代码不能假设删除对象总是最新、完整的普通对象。^[来源:delta_fifo.go - Replace、DeletedFinalStateUnknown]
Pop 在 DeltaFIFO 的锁保护下调用处理函数。源码特别提示:处理函数不应执行昂贵 I/O,否则生产者的 Add / Update 也会被锁阻塞。sharedIndexInformer 的 HandleDeltas 只做缓存变更和通知入队;耗时的业务处理应放到 handler 之后的 workqueue / worker,而不是直接阻塞 Informer 回调。^[来源:delta_fifo.go - Pop]
五、事件先更新缓存,再通知 handler
processDeltas 是把 Delta 落到 Indexer 的核心函数。对 Sync、Replaced、Added、Updated,它先检查缓存中是否已有该对象:已有则 Update 后调用 OnUpdate(old, new),没有则 Add 后调用 OnAdd。对 Deleted,它先从缓存 Delete,再调用 OnDelete。^[来源:controller.go - processDeltas]
这给出一个很实用的顺序保证:当 handler 正在处理某个事件时,Informer 的本地缓存已经完成该事件对应的变更。但它不意味着你的业务状态、其他资源的缓存,或 API Server 中所有对象构成全局事务快照。Informer 仍是最终一致的本地视图。
对于 Update,代码不应仅凭回调次数判断“真实变更次数”。重新列举、resync 和断线恢复都可能导致额外的 OnUpdate;应比较真正关心的字段,或更常见地只把对象 key 入队,让 Reconcile 重新读取当前缓存状态并按幂等逻辑收敛。
六、Shared 的含义:每个 handler 都有独立的排队器
sharedProcessor 会向所有 listener 分发普通 Add/Update/Delete 通知。每个 processorListener 使用两个 goroutine:pop() 将 addCh 的通知写入 nextCh,在下游来不及消费时放入一个可增长的环形缓冲;run() 从 nextCh 顺序调用 OnAdd、OnUpdate 或 OnDelete。^[来源:shared_informer.go - sharedProcessor、processorListener]
这提供了两层重要语义:
- 同一 listener 内有序:该 listener 的回调由一个
run()goroutine 顺序执行。 - listener 之间隔离:慢 handler 不会直接阻塞其他 handler 的执行。
但隔离不是无限背压。源码注明,停止或卡住的 listener 会让其 pendingNotifications 无界增长,最终可能 OOM。因此 handler 应快速返回:通常只计算 key 并投递到有界、可限流的 workqueue;长时间 RPC、等待或 CPU 密集任务应由 worker 完成。
七、HasSynced 与 resync:两个最常被混淆的概念
WaitForCacheSync 轮询 Informer 的 HasSynced。在默认 DeltaFIFO 路径中,首次 Replace 写入的对象都被 Pop 和处理后,initialPopulationCount 归零,队列才报告已同步。它说明“初始本地缓存已经建好”,因此 Controller 通常应在启动 worker 前等待它成立。^[来源:shared_informer.go - WaitForCacheSync] ^[来源:delta_fifo.go - HasSynced、Pop]
resync 则不是重新向 API Server 发起 List,也不是发现远端新变化的轮询。Reflector 的定时器调用 Queue 的 Resync;DeltaFIFO 以已知本地对象生成 Sync Delta,SharedInformer 仅向请求该 resync 周期的 listener 分发相应 Update。它适合让某些处理器周期性重新检查状态,不应被误当成修复 watch 丢失事件的主机制。^[来源:reflector.go - startResync] ^[来源:shared_informer.go - shouldResync]
八、应用层的标准用法
应用通常通过 factory 获取同一类型资源的 SharedInformer 与 Lister,并用 workqueue 驱动业务逻辑:
factory := informers.NewSharedInformerFactory(clientset, 0)
podInformer := factory.Core().V1().Pods()
podInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
queue.Add(keyOf(obj))
},
UpdateFunc: func(_, newObj interface{}) {
queue.Add(keyOf(newObj))
},
DeleteFunc: func(obj interface{}) {
queue.Add(keyOf(obj)) // 处理 tombstone 的 key 提取
},
})
factory.Start(ctx.Done())
if !cache.WaitForCacheSync(ctx.Done(), podInformer.Informer().HasSynced) {
return
}
// worker 从 queue 取 key,再经 podInformer.Lister() 读取当前缓存状态并 Reconcile。这个结构的关键不是回调里“立刻处理事件”,而是把事件压缩成需要重新检查的对象 key。多次 Add/Update 可以在 workqueue 层进一步合并;worker 读取的是当前缓存,因此能自然处理旧事件到达、重复入队和事件合并。业务操作仍应幂等,因为缓存可能滞后,写 API Server 时也可能发生冲突。
从 Informer / Lister 得到的对象应视为只读。修改缓存里的对象会污染其他 reader 看到的共享状态,并可能触发 client-go 的缓存变更检测告警;需要改写时先 DeepCopy(),然后通过 Kubernetes API 提交期望的更新。
九、常见误解与排障切入点
| 误解 | 源码层面的事实 | 实践结论 |
|---|---|---|
| Informer 就是 Watch | Reflector 先建立初始快照,再持续 Watch;异常时会恢复同步 | 不要跳过 WaitForCacheSync |
| 收到事件等于业务已处理 | Handler 只是通知层,缓存与业务状态分离 | 用 workqueue + 幂等 Reconcile |
| resync 会重新拉 API Server | Resync 对已知本地对象生成 Sync | 不用短 resync 代替正确的 Watch 恢复 |
| 慢 handler 没关系 | listener 缓冲可增长,卡住时会累积通知 | 回调只入队,设置队列限流与监控 |
| Delete 一定携带完整对象 | 断线恢复下可能得到 tombstone | 删除回调要兼容 DeletedFinalStateUnknown |
| Lister 的数据绝对最新 | 缓存通过异步 List/Watch 更新 | 对强一致读取或写前校验按需直接访问 API Server |
排障时先按链路定位:是否成功完成初始同步;Reflector 是否反复 List/Watch 失败或出现资源版本过期;DeltaFIFO 是否因慢处理积压;listener / workqueue 是否增长;最后再检查 Reconcile 的重试和 API 写冲突。与其把问题笼统归为“Informer 没收到事件”,不如明确它发生在 API 流、队列、缓存、分发还是业务收敛的哪一层。
十、总结
Informer 的本质是一条受控的数据管线:Reflector 从 API Server 取得快照与增量,DeltaFIFO 按对象聚合待处理变化,Indexer 保存本地视图,sharedProcessor 将变更异步交给多个消费者。正确使用它的关键是承认它提供的是最终一致、可恢复、可共享的本地观察面,而不是精确一次的业务事件流。
当把 Informer 与 Go GMP 调度模型 一起看时,还应注意运行时并发与语义并发是两件事:goroutine 能并发执行,并不自动保证事件顺序、业务幂等或缓存一致性。Kubernetes Controller 的可靠性最终来自 key 驱动、只读缓存、限流队列和可重复收敛的 Reconcile 设计。