DeltaFIFO是client-go controller framework的重要环节,它的作用是保证Reflector和Indexer之间对象同步。可以说,DeltaFIFO是连接生产者(Reflector)和消费者(Indexer)的通道。阅读本文前希望读者已阅读《client-go源码分析》前面的系列文章,要求已了解Informer和Reflector。
DeltaFIFO的定义及创建:
type DeltaFIFO struct {
。。。
items map[string]Deltas
queue []string
keyFunc KeyFunc
knownObjects KeyListerGetter
。。。
}
func NewDeltaFIFO(keyFunc KeyFunc, knownObjects KeyListerGetter) *DeltaFIFO {
f := &DeltaFIFO{
items: map[string]Deltas{},
queue: []string{},
keyFunc: keyFunc,
knownObjects: knownObjects,
}
f.cond.L = &f.lock
return f
}
DeltaFIFO的核心结构包括items和queue,对应Delta和FIFO。
Delta顾名思义是变化和差异,因此items的作用是缓存某个对象的一系列变更行为(Deltas)。正因为这里保存的是Delta(当前对象和前一个对象的差异),有序地保存一系列行为才有意义,单独的某个Updated状态无法正确同步对象。
items是map结构,key是由keyfunc计算出来的对象key;value是deltas,即delta列表。delta数据结构如下:
type Delta struct {
Type DeltaType
Object interface{}
}
当前版本DeltaType的值只有Added, Updated, Deleted, Sync;Object即runtime.Object。
FIFO即先进先出队列,这里queue仅把对象键(objkey)入列。消费者通过objkey查找items的Deltas并依次同步所有行为。queue是对象同步处理队列,消费者仅处理queue中存在的objkey。
基本功能上述已经提到DeltaType只有四种:Added, Updated, Deleted, Sync。这里仅分析最难理解的Sync函数,Sync是List机制和Resync触发的全量同步;Added,Updated,Deleted是Watch触发的增量同步。
Sync是比较特殊的处理,理解了Sync也就不难理解其他行为。Sync是指Reflector全量同步Indexer,从DelaFIFO的实现看是调用Replace函数“重建”Indexer。
DelaFIFO的Replace实现了Sync行为(Resync也调用该函数)。controller第一次启动时,Reflector的List机制触发DeltaFIFO的Sync行为,《Client-Go源码分析--Reflector》会提到r.syncWith(items,resourceVersion)函数,该函数调用func (f *DeltaFIFO) Replace(list []interface{},resourceVersion string) error
开始分析Replace代码前,先看一个重要的公共函数queueActionLocked,DeltaFIFO的所有行为都会调用它。queueActionLocked的功能是:仅处理某个动作。若处理一系列动作或多个对象,需要外部调用者重复调用它。
func (f *DeltaFIFO) queueActionLocked(actionType DeltaType, obj interface{}) error {
id, err := f.KeyOf(obj)
if err != nil {
return KeyError{obj, err}
}
// If object is supposed to be deleted (last event is Deleted),
// then we should ignore Sync events, because it would result in
// recreation of this object.
// 上一次是deleted类型,且本次是sync,那么说明该对象已经删除了,直接返回nil
if actionType == Sync && f.willObjectBeDeletedLocked(id) {
return nil
}
newDeltas := append(f.items[id], Delta{actionType, obj})
// delete去重,
newDeltas = dedupDeltas(newDeltas)
// item[id]不存在,加入到queue和items中
if len(newDeltas) > 0 {
if _, exists := f.items[id]; !exists {
f.queue = append(f.queue, id)
}
f.items[id] = newDeltas
f.cond.Broadcast()
} else {
// We need to remove this from our map (extra items in the queue are
// ignored if they are not in the map).
delete(f.items, id)
}
return nil
}
// 外部同步缓存的func (f *DeltaFIFO) Replace(list []interface{}, resourceVersion string) error {
f.lock.Lock()
defer f.lock.Unlock()
keys := make(sets.String, len(list))
// 这里是k8s集群中的实际对象集合keys
for _, item := range list {
key, err := f.KeyOf(item)
if err != nil {
return KeyError{item, err}
}
keys.Insert(key)
if err := f.queueActionLocked(Sync, item); err != nil {
return fmt.Errorf("couldn't enqueue object: %v", err)
}
}
if f.knownObjects == nil {
// Do deletion detection against our own list.
for k, oldItem := range f.items {
if keys.Has(k) {
continue
}
var deletedObj interface{}
if n := oldItem.Newest(); n != nil {
deletedObj = n.Object
}
if err := f.queueActionLocked(Deleted, DeletedFinalStateUnknown{k, deletedObj}); err != nil {
return err
}
}
if !f.populated {
f.populated = true
f.initialPopulationCount = len(list)
}
return nil
}
// Detect deletions not already in the queue.
//knownKeys 是indexers中的对象集合,是缓存中的数据
knownKeys := f.knownObjects.ListKeys()
queuedDeletions := 0
for _, k := range knownKeys {
if keys.Has(k) { // k8s集群和indexers缓存中的obj都存在,不处理
continue
}
// indexers中存在obj,但是k8s集群中不存在,删除该obj
deletedObj, exists, err := f.knownObjects.GetByKey(k)
if err != nil {
deletedObj = nil
klog.Errorf("Unexpected error %v during lookup of key %v, placing DeleteFinalStateUnknown marker without object", err, k)
} else if !exists {
deletedObj = nil
klog.Infof("Key %v does not exist in known objects store, placing DeleteFinalStateUnknown marker without object", k)
}
queuedDeletions++
if err := f.queueActionLocked(Deleted, DeletedFinalStateUnknown{k, deletedObj}); err != nil { // Deleted的加入到DeltaFIFO中,删除indexers中的obj对象
return err
}
}
if !f.populated { // 如果populated还没有设置,说明是第一次 并且 还没有任何修改操作执行过
f.populated = true
f.initialPopulationCount = len(list) + queuedDeletions
}
return nil
}
上文提到的queueActionLocked特殊处理针对一种场景:obj的id是objkey1,该对象最后一次动作是Deleted,并且objkey1仍在queue中等待处理。此时周期性被动触发一次Resync(会调用Replace)。k8s集群真实的runtime.Object已删除,但Indexer的同步尚未完成,Indexer缓存里仍有这个对象。
假设queueActionLocked更新了deltas如上图(左)所示,该对象会先删除,然后添加回来!从Pop()函数里的回调函数Process可以看到这个过程。前几篇文章已经提到过,Process的入参obj是Deltas队列,该函数依次处理每个Delta行为。当Process处理到上图(左)Deleted时Index会删除对象;当它处理到最后的Sync时,Index又会添加这个对象。因此,queueActionLocked通过特殊处理规避了这个问题,现有代码的处理方式如上图(右)所示,直接返回不更新deltas。
// DeletedFinalStateUnknown isplaced into a DeltaFIFO in the case where
// an object was deleted but thewatch deletion event was missed. In this
// case we don't know the final"resting" state of the object, so there's// a chance the included Obj
isstale.
这个对象困惑了我许久,从注释上看是针对Watch Deleted事件丢失,导致k8s实际对象和Indexer缓存不一致的场景。我们用图展示两种场景:1. Deleted事件未丢失,只是queue待处理该对象;2. Deleted事件丢失。
上图(左)是Deleted消息丢失的代码处理,queueActionLocked会添加DeleteFinalStateUnknown对象到deltas,并保存到Indexer。DeleteFinalStateUnknown是对runtime.Object的封装。上图(右)是Deleted消息未丢失的代码处理。deltas对象变化过程如下:
从Process回调函数看,是直接调用clientState.Delete(d.Object)删除对象的,Indexer计算对象键是否会异常呢?抱着好奇心看了一眼keyFunc(计算对象键的函数,下一篇文章会讲到)DeletionHandlingMetaNamespaceKeyFunc:
func DeletionHandlingMetaNamespaceKeyFunc(obj interface{}) (string, error) {
if d, ok := obj.(DeletedFinalStateUnknown); ok {
return d.Key, nil
}
return MetaNamespaceKeyFunc(obj)
}
DeletionHandlingMetaNamespaceKeyFunc果然有对DeletedFinalStateUnknown的处理(obj.(DeletedFinalStateUnknown)是类型断言用法),因此不需要担心获取对象键会异常。如果是我们自己实现keyFunc,一样要考虑到DeletedFinalStateUnknown这个而特殊对象。到此为止DeltaFIFO的主要机制已分析完毕。
https://blog.csdn.net/li_101357/article/details/89763992
https://www.cnblogs.com/charlieroro/p/10330390.html