Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:DeltaFIFO 核心原理与源码深度剖析
Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:DeltaFIFO 核心原理与源码深度剖析
在 Kubernetes 的 Informer 机制中,DeltaFIFO 是一个核心的数据结构,理解它对于掌握控制器开发至关重要。上一篇文章我们讲了 workqueue(工作队列),它是控制器内部的"调度中心";而 DeltaFIFO 则是 Informer 系统的"事件中心"——负责接收从 API Server 传来的 Watch 事件,并按顺序传递给下游的处理器。DeltaFIFO 全称是 "Delta First In First Out",翻译过来就是"带变更类型的先进先出队列"。它的核心能力是:把对象的所有变更历史(Deltas)累积起来,按 FIFO 顺序让消费者逐一处理,同时还能做变更去重和缺失检测。
Kubernetes 1.36.1 · Go 1.26 · client-go tools/cache
🔓 学习重点提示 — 建议先通读全文,再重点回顾标注内容
★ 重点掌握(必须)
• Delta 和 Deltas 的设计:理解为什么用变更历史而不是单一对象
• Queue 接口:理解 DeltaFIFO 实现的接口契约
• queueActionLocked() 方法:核心入队逻辑,Delta 追加和去重
• Pop() 方法:阻塞获取任务,sync 完成信号
• Replace() 方法:全量同步时的差量删除检测
☆ 次重点(了解即可)
• Resync 机制和 knownObjects 的配合
• DeletedFinalStateUnknown 的"墓碑"机制
一、为什么需要 DeltaFIFO?—— 与普通 FIFO 的区别
在讲 DeltaFIFO 之前,我们先来理解一个简单的问题:Kubernetes 为什么不用普通的 FIFO(先进先出队列),而要发明 DeltaFIFO?
普通的 FIFO 队列存的是"对象本身",每次变化都是"替换"——新对象进来,旧对象被覆盖。但 Kubernetes 的 Watch 事件不一样,它有多种类型:Added(新增)、Modified(修改)、Deleted(删除),甚至还有 Bookmark 这样的特殊事件。如果用普通 FIFO,"修改"事件只告诉你最新状态,丢失了"之前是什么"。而 DeltaFIFO 存的是"变更历史(Deltas)"——每次事件都作为一个 Delta 追加到历史里。消费者拿到的是一串变更记录,可以看到对象从创建到现在的完整变化轨迹。
普通 FIFO(存对象):
queue: [pod_v3] ← 只保留最新状态,丢失了 v1→v2→v3 的变化历史
DeltaFIFO(存变更):
items: {
"default/nginx": [
{Type: Added, Object: pod_v1}, ← 创建
{Type: Updated, Object: pod_v2}, ← 第一次修改
{Type: Updated, Object: pod_v3}, ← 第二次修改
]
}
DeltaFIFO 的另一个核心能力是"缺失检测":当 Controller 启动时需要从 API Server 全量拉取(List)一次数据,DeltaFIFO 的 Replace() 方法会对比"拉取的列表"和"已知对象",自动为那些"被删除了但你没收到删除事件"的对象生成一个 DeletedFinalStateUnknown 标记。这解决了 Watch 连接断开期间可能丢失删除事件的问题。
二、数据结构——Delta、DeltaType 和 Deltas
DeltaFIFO 的核心是三个紧密相关的类型:Delta(单次变更)、DeltaType(变更类型)和 Deltas(变更历史列表)。理解这三个类型是理解整个 DeltaFIFO 的基础。
2.1 DeltaType —— 变更类型的枚举
DeltaType 是变更类型的字符串枚举,定义了所有可能的变更类型:
// staging/src/k8s.io/client-go/tools/cache/delta_fifo.go(行 178-196)
// DeltaType is the type of a change (addition, deletion, etc)
type DeltaType string
// Change type definition
const (
Added DeltaType = "Added" // 对象被创建
Updated DeltaType = "Updated" // 对象被修改
Deleted DeltaType = "Deleted" // 对象被删除
// Sync is for an object that is present in the store but
// not reflected by the watch event. It is only for the
// initial sync (during Replace() or Resync()).
// This is used when the store has been updated since the
// last watch event was received.
Sync DeltaType = "Sync" // 同步事件(全量拉取后的初始同步)
// Replaced is emitted by Replace() if EmitDeltaTypeReplaced is set.
// It indicates the previous state of an object existed before the Replace()
// call. It is functionally equivalent to a "Deleted" event, followed by an
// "Added" event, but is emitted as a single "Replaced" event instead.
Replaced DeltaType = "Replaced" // 替换事件(新旧版本替换)
// Bookmark is used only for the LIST event.
// It indicates the caller requested the bookmark version.
Bookmark DeltaType = "Bookmark" // 书签(仅用于 LIST,请求的版本)
)
这里有六种变更类型,我们重点关注前四种:Added 是创建、Updated 是修改、Deleted 是删除、Sync 是同步(用于 Replace 或 Resync)。Replaced 是后来新增的,用于区分"真正的替换"和"同步更新"。Bookmark 比较特殊,它只出现在 List 场景中,用于携带 resourceVersion(告诉客户端当前版本)。
2.2 Delta —— 单次变更的结构体
Delta 是最小单元,代表一次变更事件:
// staging/src/k8s.io/client-go/tools/cache/delta_fifo.go(行 213-219)
// Delta has the type of change that produced the change (Added, etc), and the
// object that changed.
//
// [*] Unless the change is a deletion, and then you'll get the final
// state of the object before it was deleted.
type Delta struct {
Type DeltaType // 变更类型:Added/Updated/Deleted/Sync/Replaced
Object interface{} // 变更时的对象(interface{} 可以是任意类型)
}
Delta 的设计非常简洁:一个 Type 字段标明变更类型,一个 Object 字段存储变更时的对象。这里有个细节注释值得注意:如果是删除事件(Deleted),Object 字段存的是删除前的最后一个状态,而不是 nil。这是为了让消费者在收到删除事件时,还能看到被删对象的内容(比如获取 finalizers 做清理工作)。
2.3 Deltas —— 变更历史的切片
Deltas 是 Delta 的切片,按时间顺序存储一个对象的所有变更历史:
// staging/src/k8s.io/client-go/tools/cache/delta_fifo.go(行 221-223)
// Deltas is a list of one or more 'Delta's to an individual object. // The oldest delta is at index 0, the newest delta is the last one. type Deltas []Delta
Deltas 的注释明确说明了索引规则:索引 0 是最老的变更,最后一个元素是最新的变更。Deltas 还提供了几个辅助方法:
// staging/src/k8s.io/client-go/tools/cache/delta_fifo.go(行 760-793)
// Newest returns the newest Delta for the sample.
func (d Deltas) Newest() *Delta {
if len(d) == 0 {
return nil
}
return &d[len(d)-1]
}
// Oldest returns the oldest Delta for the sample.
func (d Deltas) Oldest() *Delta {
if len(d) == 0 {
return nil
}
return &d[0]
}
// Lists all the stored deltas as a slice, and a bool that indicates if
// the youngest (last) event is a deletion.
func (d Deltas) List() ([]Delta, bool) {
return d, d.IsDeleted()
}
// IsDeleted returns true if the newest delta is a deletion.
func (d Deltas) IsDeleted() bool {
return len(d) > 0 && d[len(d)-1].Type == Deleted
}
// copyDeltas returns a shallow copy of d; that is, it copies the slice
// but not the objects in the slice.
func copyDeltas(d Deltas) Deltas {
d2 := make(Deltas, len(d))
copy(d2, d)
return d2
}
这些辅助方法非常实用:Newest() 获取最新的变更(常用于获取当前状态)、Oldest() 获取最老的变更、IsDeleted() 判断最新状态是否是删除。copyDeltas() 返回一个浅拷贝——切片被复制了,但里面的对象引用不变,这是因为我们希望 Get/List 返回的对象不会被后续修改覆盖(对象本身不可变)。
三、Queue 接口 —— DeltaFIFO 实现的契约
DeltaFIFO 实现了 Queue 接口(来自 staging/src/k8s.io/client-go/tools/cache/thread_safe_store.go)。Queue 接口定义了生产者(Reflector)和消费者(Controller)之间的契约:
// staging/src/k8s.io/client-go/tools/cache/thread_safe_store.go
// Queue is exactly like a Store, but has Pop() and AddIfNotPresent() methods.
// It is used to coordinate producers and consumers of items in a way that
// ensures consumers don't miss any items.
type Queue interface {
Store
// Add adds an item to the queue. It will only be added if AddIfNotPresent
// would return true (i.e., the item hasn't been processed yet, or was
// deleted and re-added).
Add(obj interface{}) error
// AddIfNotPresent adds an item to the queue only if the item is
// not already present. If the item is deleted and re-added, this will
// return true.
AddIfNotPresent(obj interface{}) error
// Pop blocks until an item is ready and returns it. If there are no
// items, it blocks until either an item is added or the queue is closed.
Pop(process PopProcessFunc) (interface{}, error)
// Replace atomically deletes the contents of the queue (all existing items)
// and replaces them with the given list. After Replace, only items that
// are not present in the list will be present in the queue.
Replace(list []interface{}, resourceVersion string) error
// Resync ensures that every object in the Store has its key in the queue.
Resync() error
}
// PopProcessFunc is a callback function that is called when an item is popped
// from the queue. It returns an error if the processing failed.
// The item is removed from the queue before the function is called.
type PopProcessFunc func(item interface{}, isInInitialList bool) error
Queue 接口的核心方法是:Add() 添加变更事件、Pop() 阻塞获取待处理的 Deltas、Replace() 全量同步并检测缺失对象、Resync() 确保已知对象的 key 都在队列里。PopProcessFunc 是消费回调函数,处理从队列中弹出的 Deltas。
3.1 DeltaFIFO 结构体
现在让我们来看 DeltaFIFO 的完整结构体定义:
// staging/src/k8s.io/client-go/tools/cache/delta_fifo.go(行 108-158)
// DeltaFIFO is like FIFO, but differs in two ways. One is that the
// accumulator associated with a given object's key is not that object
// but a Deltas, which is a list of Delta objects. This allows
// multiple changes to an object to be represented at once, and allows
// multiple consumers to see different views of the change history.
// The other difference is that DeltaFIFO is keyed by a Deltas object's
// corresponding object key, at addition time. This allows Delta's to
// be placed in the queue when they occur, before the object is
// processed. This allows the accumulator to accumulate more recent
// events before the object is processed, thus allowing "up to" and
// "since" queries.
//
// A note on threading: If you call Pop() in parallel from multiple
// threads, you could end up with multiple threads processing slightly
// different versions of the same object.
type DeltaFIFO struct {
// logger is a per-instance logger
logger klog.Logger
name string
// lock/cond protects access to 'items' and 'queue'.
lock sync.RWMutex
cond sync.Cond
// `items` maps a key to a Deltas.
// Each such Deltas has at least one Delta.
items map[string]Deltas
// `queue` maintains FIFO order of keys for consumption in Pop().
// There are no duplicates in `queue`.
// A key is in `queue` if and only if it is in `items`.
queue []string
// synced is initially an open channel. It gets closed (once!) by
// checkSynced_locked as soon as the initial sync is considered complete.
synced chan struct{}
syncedClosed bool
// populated is true if the first batch of items inserted by Replace()
// has been populated or Delete/Add/Update/AddIfNotPresent was called first.
populated bool
// initialPopulationCount is the number of items inserted by the first
// call of Replace()
initialPopulationCount int
// keyFunc is used to make the key used for queued item
// insertion and retrieval, and should be deterministic.
keyFunc KeyFunc
// knownObjects list keys that are "known" --- affecting Delete(),
// Replace(), and Resync()
knownObjects KeyListerGetter
// Used to indicate a queue is closed so a control loop can exit
// when a queue is empty.
closed bool
// emitDeltaTypeReplaced is whether to emit the Replaced or Sync
// DeltaType when Replace() is called
emitDeltaTypeReplaced bool
// Called with every object if non-nil.
transformer TransformFunc
}
DeltaFIFO 的结构体设计非常精妙:items 是一个 map,key 是对象的 key(如 namespace/name),value 是 Deltas(该对象的所有变更历史)。queue 是一个 string 切片,维护了待处理 keys 的 FIFO 顺序。设计原则是:key 在 queue 中,当且仅当它在 items 中。 这样可以保证 queue 没有重复元素。 关键的同步机制是 synced channel(初始为空 channel)和 populated + initialPopulationCount 两个标志。当 Replace() 被调用时,initialPopulationCount 会被设置为要处理的 item 数量,每 Pop 出一个就减 1,减到 0 时说明初始同步完成,channel 被关闭。
四、构造函数 —— NewDeltaFIFO 和 NewDeltaFIFOWithOptions
DeltaFIFO 有两个构造函数:简单的 NewDeltaFIFO 和更灵活的 NewDeltaFIFOWithOptions。
// staging/src/k8s.io/client-go/tools/cache/delta_fifo.go(行 225-301)
// DeltaFIFOOptions is the configuration parameters for DeltaFIFO.
type DeltaFIFOOptions struct {
// Logger for the DeltaFIFO instance
Logger *klog.Logger
// Name identifies this DeltaFIFO instance for metrics/logging
Name string
// KeyFunction generates a key from an object.
// Optional, defaults to MetaNamespaceKeyFunc.
KeyFunction KeyFunc
// KnownObjects returns a list of keys known to the consumer.
// Used by Replace() to detect deletions.
// May be nil if you can tolerate missing deletions on Replace().
KnownObjects KeyListerGetter
// EmitDeltaTypeReplaced indicates that the queue consumer
// understands the Replaced DeltaType.
// When false, Replace() emits Sync instead of Replaced.
EmitDeltaTypeReplaced bool
// If set, will be called for objects before enqueueing them.
Transformer TransformFunc
}
// NewDeltaFIFO returns a Queue which can be used to process changes to items.
func NewDeltaFIFO(keyFunc KeyFunc, knownObjects KeyListerGetter) *DeltaFIFO {
return NewDeltaFIFOWithOptions(DeltaFIFOOptions{
KeyFunction: keyFunc,
KnownObjects: knownObjects,
})
}
// NewDeltaFIFOWithOptions returns a Queue which can be used to process changes to items.
func NewDeltaFIFOWithOptions(opts DeltaFIFOOptions) *DeltaFIFO {
if opts.KeyFunction == nil {
opts.KeyFunction = MetaNamespaceKeyFunc
}
f := &DeltaFIFO{
logger: klog.Background(),
name: "DeltaFIFO",
synced: make(chan struct{}),
items: map[string]Deltas{},
queue: []string{},
keyFunc: opts.KeyFunction,
knownObjects: opts.KnownObjects,
emitDeltaTypeReplaced: opts.EmitDeltaTypeReplaced,
transformer: opts.Transformer,
}
if opts.Logger != nil {
f.logger = *opts.Logger
}
if name := opts.Name; name != "" {
f.name = name
}
f.logger = klog.LoggerWithName(f.logger, f.name)
f.cond.L = &f.lock
return f
}
DeltaFIFOOptions 有几个重要配置项:KeyFunction(默认为 MetaNamespaceKeyFunc,生成 namespace/name 格式的 key)、KnownObjects(已知对象列表,用于 Replace 时的缺失检测)、EmitDeltaTypeReplaced(是否在 Replace 时发送 Replaced 事件而非 Sync)、Transformer(对象转换函数,在入队前对对象进行转换)。synced 是一个初始为 open 的空 channel,当初始同步完成时会被关闭(close(f.synced)),消费者可以通过监听这个 channel 来判断是否完成初始同步。
五、核心方法详解
5.1 Add / Update / Delete —— 变更事件的入队
Add、Update、Delete 是最常用的变更入队方法,它们最终都调用 queueActionLocked:
// staging/src/k8s.io/client-go/tools/cache/delta_fifo.go(行 384-439)
// Add inserts an item, and puts it in the queue.
func (f *DeltaFIFO) Add(obj interface{}) error {
f.lock.Lock()
defer f.lock.Unlock()
f.populated = true
f.checkSynced_locked()
return f.queueActionLocked(Added, obj)
}
// Update is just like Add, but makes an Updated Delta.
func (f *DeltaFIFO) Update(obj interface{}) error {
f.lock.Lock()
defer f.lock.Unlock()
f.populated = true
f.checkSynced_locked()
return f.queueActionLocked(Updated, obj)
}
// Delete is just like Add, but makes a Deleted Delta.
// If the object does not already exist, it will be ignored.
// In this method `f.knownObjects`, if not nil, provides
// _additional_ objects that are considered to already exist.
func (f *DeltaFIFO) Delete(obj interface{}) error {
id, err := f.KeyOf(obj)
if err != nil {
return KeyError{obj, err}
}
f.lock.Lock()
defer f.lock.Unlock()
f.populated = true
f.checkSynced_locked()
// 如果没有 knownObjects,只检查 items 中是否存在
if f.knownObjects == nil {
if _, exists := f.items[id]; !exists {
// 对象可能在 Re-list 期间被删除了,不要重复报告同一删除
return nil
}
} else {
// 有 knownObjects 时,同时检查 knownObjects 和 items
_, exists, err := f.knownObjects.GetByKey(id)
_, itemsExist := f.items[id]
if err == nil && !exists && !itemsExist {
// 对象不存在于已知对象和队列中,跳过
return nil
}
}
// 存在则加入 Deleted Delta
return f.queueActionLocked(Deleted, obj)
}
这三个方法非常简洁。关键是理解 Delete 方法的逻辑:如果一个对象既不在 items 里(说明已经出队处理过了),也已经被从 knownObjects 中删除了(说明是 Re-list 之后才知道它被删了),就跳过这个删除事件,避免重复报告。
5.2 queueActionLocked —— 核心入队逻辑
queueActionLocked 是 DeltaFIFO 的核心,所有变更事件都通过它进入队列。它的职责是:追加 Delta、去重、决定是否将 key 加入 queue。
// staging/src/k8s.io/client-go/tools/cache/delta_fifo.go(行 480-541)
// queueActionLocked appends to the delta list for the object.
// Caller must lock first.
func (f *DeltaFIFO) queueActionLocked(actionType DeltaType, obj interface{}) error {
return f.queueActionInternalLocked(actionType, actionType, obj)
}
// queueActionInternalLocked appends to the delta list for the object.
// The actionType is emitted and must honor emitDeltaTypeReplaced.
// The internalActionType is only used within this function and must
// ignore emitDeltaTypeReplaced.
// Caller must lock first.
func (f *DeltaFIFO) queueActionInternalLocked(actionType, internalActionType DeltaType, obj interface{}) error {
// 1. 获取对象的 key
id, err := f.KeyOf(obj)
if err != nil {
return KeyError{obj, err}
}
// 2. 如果有 transformer,在入队前对对象进行转换
if f.transformer != nil {
_, isTombstone := obj.(DeletedFinalStateUnknown)
if !isTombstone && internalActionType != Sync {
var err error
obj, err = f.transformer(obj)
if err != nil {
return err
}
}
}
// 3. 获取旧的 Deltas,追加新 Delta
oldDeltas := f.items[id]
newDeltas := append(oldDeltas, Delta{actionType, obj})
// 4. 去重:如果连续两个变更相同,合并为一个
newDeltas = dedupDeltas(newDeltas)
// 5. 判断是否需要加入 queue
if len(newDeltas) > 0 {
if _, exists := f.items[id]; !exists {
// key 之前不在 items 中,需要加入 queue(FIFO 顺序)
f.queue = append(f.queue, id)
}
f.items[id] = newDeltas
f.cond.Broadcast() // 通知 Pop() 中的等待者
} else {
// dedupDeltas 返回空列表时(理论上不会发生)
if oldDeltas == nil {
utilruntime.HandleErrorWithLogger(f.logger, nil, "Impossible dedupDeltas, ignoring", "id", id)
return nil
}
f.items[id] = newDeltas
return fmt.Errorf("Impossible dedupDeltas...")
}
return nil
}
queueActionLocked 的核心逻辑分五步: 第一步:获取 key。调用 KeyOf() 从对象中提取 key(通常是 namespace/name)。 第二步:Transformer(可选)。如果有 transformer 函数,在对象入队前先进行转换。这常用于过滤或修改对象。 第三步:追加 Delta。将新 Delta 追加到该 key 现有的 Deltas 列表后面。 第四步:去重。调用 dedupDeltas() 合并重复的变更。 第五步:判断是否加入 queue。这是 DeltaFIFO 和普通 FIFO 的关键区别:key 只有在"第一次出现"时才加入 queue。如果一个 key 已经在 items 中(说明已经在 queue 里了),就不再重复加入。这样保证了一个对象的多个变更在队列里只占一个位置。
5.3 dedupDeltas 和 isDup —— 变更去重
dedupDeltas 是 DeltaFIFO 的重要去重机制。Watch 事件可能因为网络抖动等原因重复发送,dedupDeltas 能合并相邻的重复变更:
// staging/src/k8s.io/client-go/tools/cache/delta_fifo.go(行 441-478)
// re-listing and watching can deliver the same update multiple times in any
// order. This will combine the most recent two deltas if they are the same.
func dedupDeltas(deltas Deltas) Deltas {
n := len(deltas)
if n < 2 {
return deltas
}
a := &deltas[n-1] // 最新的 Delta
b := &deltas[n-2] // 倒数第二新的 Delta
if out := isDup(a, b); out != nil {
deltas[n-2] = *out
return deltas[:n-1] // 合并后返回 n-1 个元素
}
return deltas
}
// If a & b represent the same event, returns the delta that ought to be kept.
func isDup(a, b *Delta) *Delta {
if out := isDeletionDup(a, b); out != nil {
return out
}
// TODO: Detect other duplicate situations? Are there any?
return nil
}
// keep the one with the most information if both are deletions.
func isDeletionDup(a, b *Delta) *Delta {
if b.Type != Deleted || a.Type != Deleted {
return nil
}
// 如果旧的 Delta 是 DeletedFinalStateUnknown(墓碑),保留旧的
// 因为旧的对象包含更多信息
if _, ok := b.Object.(DeletedFinalStateUnknown); ok {
return a
}
// 否则保留新的
return b
}
去重的逻辑非常巧妙:只检查最后两个 Delta(最新的和倒数第二新的)。为什么只检查相邻的两个?因为变更历史是有序的,如果真的重复了,它们一定是相邻的。合并规则是:保留包含更多信息的那个 Delta。 在删除去重的场景里:如果旧的 Delta 是 DeletedFinalStateUnknown(这意味着 Watch 断开期间对象被删了,我们不知道它之前是什么),保留旧的;如果新的也是普通的 Deleted,保留新的(因为 Deleted 对象里存的是删除前的最后状态,新的可能更完整)。
5.4 Pop —— 阻塞获取与初始同步
Pop 方法是消费者获取任务的核心入口。它会阻塞直到有任务或者队列被关闭。
// staging/src/k8s.io/client-go/tools/cache/delta_fifo.go(行 550-608)
// Pop blocks until the queue has some items, and then returns one.
func (f *DeltaFIFO) Pop(process PopProcessFunc) (interface{}, error) {
f.lock.Lock()
defer f.lock.Unlock()
for {
// 队列为空时阻塞等待
for len(f.queue) == 0 {
// 队列已关闭,返回错误
if f.closed {
return nil, ErrFIFOClosed
}
f.cond.Wait() // 等待 Broadcast() 唤醒
}
// 判断是否在初始同步阶段
isInInitialList := !f.hasSynced_locked()
// 取出队首的 key
id := f.queue[0]
f.queue = f.queue[1:] // 移出队列
// 记录队列深度(用于慢事件告警)
depth := len(f.queue)
// 处理 initialPopulationCount(初始同步计数)
if f.initialPopulationCount > 0 {
f.initialPopulationCount--
f.checkSynced_locked() // 减到 0 时关闭 synced channel
}
// 获取 Deltas 并从 items 中删除
item, ok := f.items[id]
if !ok {
// 理论上不应该发生
utilruntime.HandleErrorWithLogger(f.logger, nil,
"Inconceivable! Item was in f.queue but not f.items", "id", id)
continue
}
delete(f.items, id) // 重要:从 items 中删除
// 慢事件告警:队列深度超过 10 且处理耗时超过 100ms
if depth > 10 {
trace := utiltrace.New("DeltaFIFO Pop Process",
utiltrace.Field{Key: "ID", Value: id},
utiltrace.Field{Key: "Depth", Value: depth})
defer trace.LogIfLong(100 * time.Millisecond)
}
// 调用回调函数处理 Deltas
err := process(item, isInInitialList)
return item, err
}
}
Pop 方法的设计非常周密: 1. 阻塞等待:队列为空时调用 cond.Wait() 阻塞,直到有其他线程调用 Broadcast()(在 queueActionLocked 中)唤醒。 2. 初始同步标记:isInInitialList 表示这次 Pop 是否来自 Replace() 初始同步阶段的批量任务。回调函数可以用这个标记做特殊处理(比如区分"新事件"和"启动时的全量数据")。 3. initialPopulationCount:这是 DeltaFIFO 实现初始同步通知的关键。当 Replace() 被调用时,initialPopulationCount 被设置为要处理的 item 数量。每 Pop 一个,计数减 1,减到 0 时调用 checkSynced_locked() 关闭 synced channel,通知消费者"初始同步完成了"。 4. 深度追踪:队列深度超过 10 时会记录 trace(如果处理耗时超过 100ms),这对于排查"事件处理器阻塞队列"问题很有帮助。
5.5 Replace —— 全量同步与缺失检测
Replace 方法是 DeltaFIFO 最复杂的方法之一。当 Controller 启动时,Reflector 会先调用 List 获取全量数据,然后调用 Replace()。Replace 做了两件事:把新数据以 Sync 或 Replaced 类型加入队列,检测被删除但没收到删除事件的对象。
// staging/src/k8s.io/client-go/tools/cache/delta_fifo.go(行 610-699)
// Replace atomically does two things:
// (1) it adds the given objects using the Sync or Replace DeltaType
// (2) it does some deletions.
// In particular: for every pre-existing key K that is not the key of
// an object in `list` there is the effect of
// `Delete(DeletedFinalStateUnknown{K, O})` where O is the latest known
// object of K.
func (f *DeltaFIFO) Replace(list []interface{}, _ string) error {
f.lock.Lock()
defer f.lock.Unlock()
keys := make(sets.Set[string], len(list))
// 第一步:将新对象以 Sync/Replaced 类型加入队列
action := Sync
if f.emitDeltaTypeReplaced {
action = Replaced
}
for _, item := range list {
key, err := f.KeyOf(item)
if err != nil {
return KeyError{item, err}
}
keys.Insert(key)
if err := f.queueActionInternalLocked(action, Replaced, item); err != nil {
return fmt.Errorf("couldn't enqueue object: %v", err)
}
}
// 第二步:检测队列中的"幽灵删除"(Watch 断开期间被删的对象)
queuedDeletions := 0
for k, oldItem := range f.items {
if keys.Has(k) {
continue
}
// 删除队列中存在但新列表中不存在的 key
var deletedObj interface{}
if n := oldItem.Newest(); n != nil {
deletedObj = n.Object
// 如果是 DeletedFinalStateUnknown,提取其中的 Obj
if d, ok := deletedObj.(DeletedFinalStateUnknown); ok {
deletedObj = d.Obj
}
}
queuedDeletions++
if err := f.queueActionLocked(Deleted, DeletedFinalStateUnknown{k, deletedObj}); err != nil {
return err
}
}
// 第三步:检测 knownObjects 中的"幽灵删除"
if f.knownObjects != nil {
knownKeys := f.knownObjects.ListKeys()
for _, k := range knownKeys {
if keys.Has(k) {
continue
}
if len(f.items[k]) > 0 {
continue // 已经在第二步处理过了
}
deletedObj, exists, err := f.knownObjects.GetByKey(k)
if err != nil {
deletedObj = nil
} else if !exists {
deletedObj = nil
}
queuedDeletions++
if err := f.queueActionLocked(Deleted, DeletedFinalStateUnknown{k, deletedObj}); err != nil {
return err
}
}
}
// 设置初始同步计数(Pop() 会递减)
if !f.populated {
f.populated = true
f.initialPopulationCount = keys.Len() + queuedDeletions
f.checkSynced_locked()
}
return nil
}
Replace 的三步逻辑非常清晰: 第一步:加入新对象。遍历 List 返回的完整对象列表,为每个对象生成一个 Sync 或 Replaced Delta 加入队列(通过 queueActionInternalLocked)。注意这里的 internalActionType 固定为 Replaced,而 actionType 取决于 emitDeltaTypeReplaced 配置。 第二步:队列中的幽灵删除。遍历 items(当前队列中的所有对象),如果某个 key 不在新的 List 中,说明这个对象在 Watch 断开期间被删除了,但我们没收到删除事件。此时生成一个 DeletedFinalStateUnknown 标记(包含最后已知的状态)放入队列。 第三步:knownObjects 中的幽灵删除。如果有 knownObjects(通常是本地的Indexer store),也要检测它里面存在但新 List 中不存在的 key。 初始同步计数:initialPopulationCount = 新对象数 + 幽灵删除数。这样 Pop() 每处理一个任务,计数减 1,直到所有任务处理完才关闭 synced channel。
5.6 Resync —— 同步已知对象
Resync 方法用于确保 knownObjects 中的每个对象都有对应的 key 在队列里(如果还没有的话):
// staging/src/k8s.io/client-go/tools/cache/delta_fifo.go(行 701-747)
// Resync adds, with a Sync type of Delta, every object listed by
// `f.knownObjects` whose key is not already queued for processing.
// If `f.knownObjects` is `nil` then Resync does nothing.
func (f *DeltaFIFO) Resync() error {
f.lock.Lock()
defer f.lock.Unlock()
if f.knownObjects == nil {
return nil
}
keys := f.knownObjects.ListKeys()
for _, k := range keys {
if err := f.syncKeyLocked(k); err != nil {
return err
}
}
return nil
}
func (f *DeltaFIFO) syncKeyLocked(key string) error {
obj, exists, err := f.knownObjects.GetByKey(key)
if err != nil {
utilruntime.HandleErrorWithLogger(f.logger, err, "Unexpected error during lookup, unable to queue object for sync", "key", key)
return nil
} else if !exists {
f.logger.Info("Key does not exist in known objects store, unable to queue object for sync", "key", key)
return nil
}
// 如果这个 key 已经有待处理的变更,跳过 Resync
// 避免与已有变更竞争
id, err := f.KeyOf(obj)
if err != nil {
return KeyError{obj, err}
}
if len(f.items[id]) > 0 {
return nil
}
// 发送 Sync 类型的 Delta
if err := f.queueActionLocked(Sync, obj); err != nil {
return fmt.Errorf("couldn't queue object: %v", err)
}
return nil
}
Resync 的设计哲学是"保守":它只给那些还没有待处理变更的对象(len(f.items[id]) == 0)发送 Sync 事件。如果一个对象已经在队列里有待处理的变更,Resync 会跳过它,因为"已有变更"已经足够让消费者处理到最新状态了。
六、DeletedFinalStateUnknown —— "墓碑"机制
DeletedFinalStateUnknown 是 DeltaFIFO 中一个特殊的设计,称为"墓碑"(tombstone)。它用于处理 Watch 断开期间对象被删除、但我们没有收到删除事件的场景:
// staging/src/k8s.io/client-go/tools/cache/delta_fifo.go(行 793-813)
// DeletedFinalStateUnknown is placed into a DeltaFIFO in the case where an object
// was deleted but the watch deletion event was missed while disconnected from
// apiserver.
//
// The key in DeltaFIFO becomes the last known key of the object, and the
// object becomes the last known state of the object.
type DeletedFinalStateUnknown struct {
Key string
// Obj is the last known state of the object. It could be the object itself
// before it was deleted, or a tombstone object if the object was deleted
// while the informer was not running.
Obj interface{}
}
当消费者(通常是 SharedIndexInformer)收到一个 DeletedFinalStateUnknown 时,它需要特殊处理:对象的 Obj 字段可能是 nil(如果连 knownObjects 里也没有这个对象了),也可能是一个实际对象(如果是从 items 的最新 Delta 中提取出来的)。消费者需要从 Indexer(本地缓存)中尝试获取这个对象,或者直接将其作为删除事件处理。
七、DeltaFIFO 在 Informer 架构中的位置
DeltaFIFO 是 SharedIndexInformer 的核心组件,它连接了 Reflector(数据拉取)和下游处理器(事件分发)。
┌─────────────────────────────────────────────────────────────────┐
│ API Server │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ List │───▶│ Watch │───▶│ Watch │ │
│ └─────────────┘ └──────┬──────┘ └─────────────┘ │
└─────────────────────────────┼───────────────────────────────────┘
│
Reflector
│
List/Watch 事件
│
┌─────────▼─────────┐
│ DeltaFIFO │
│ - 存储变更历史 │
│ - 去重 │
│ - 缺失检测 │
└─────────┬─────────┘
│
Pop() 获取 Deltas
│
┌─────────▼─────────┐
│ SharedIndexInformer │
│ handleDeltas() │
└─────────┬─────────┘
│
processDeltas()
│
┌─────────▼─────────┐
│ Processor │
│ (多播到所有监听器) │
└───────────────────┘
Reflector 先调用 List 获取全量数据,调用 Replace() 存入 DeltaFIFO;然后持续调用 Watch,事件(Added/Updated/Deleted)通过 Add/Update/Delete() 进入 DeltaFIFO。SharedIndexInformer 的 Controller 持续调用 Pop() 从 DeltaFIFO 获取 Deltas,调用 processDeltas() 更新本地 Indexer(本地缓存),然后分发给所有注册的 ResourceEventHandler(用户的 Handler)。
八、总结
这篇文章我们从零到一剖析了 DeltaFIFO 的设计。核心要点总结如下:
- Delta(单次变更) = Type + Object。Type 有六种:Added/Updated/Deleted/Sync/Replaced/Bookmark。
- Deltas(变更历史) = Delta[],索引 0 是最老,最后一个是最新。
- DeltaFIFO 的核心能力:存储变更历史、变更去重、缺失检测。
- queueActionLocked 核心逻辑:追加 Delta → 去重 → 判断是否加入 queue(首次入 items 时才入 queue)。
- Pop 阻塞获取:isInInitialList 标记、initialPopulationCount 递减、synced channel 关闭。
- Replace 三步走:新对象加入队列 → 检测 items 中的幽灵删除 → 检测 knownObjects 中的幽灵删除。
- DeletedFinalStateUnknown:墓碑机制,处理 Watch 断开期间的缺失删除。
理解 DeltaFIFO 是掌握 Kubernetes Informer 机制的关键。它不仅仅是"先进先出队列",更是一个精心设计的变更历史管理器——通过存储 Deltas(而非单一对象)、去重、缺失检测等机制,确保控制器不会遗漏任何重要的事件,即使在网络不稳定的环境下也能可靠运行。
Kubernetes 编程 / Operator 专题【左扬精讲】—— DeltaFIFO 核心原理与源码深度剖析 · 来源:Kubernetes v1.36.1 client-go cache 源码分析
相关阅读:
• Kubernetes DeltaFIFO 源码
• Kubernetes Controller 和 processDeltas 源码
• Kubernetes SharedIndexInformer 源码

浙公网安备 33010602011771号