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

Kubernetes Go Informer DeltaFIFO 控制器

🔓 学习重点提示  — 建议先通读全文,再重点回顾标注内容

★ 重点掌握(必须)
   • 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 源码

posted @ 2026-06-13 15:43  左扬  阅读(2)  评论(0)    收藏  举报