Kubernetes client-go DeltaFIFO 源码分析
概述Queue 接口DeltaFIFO元素增删改 - queueActionLocked()Pop()Replace()
概述
源码版本信息
- Project: kubernetes
- Branch: master
- Last commit id: d25d741c
- Date: 2021-09-26
我们在《Kubernetes client-go 源码分析 - 开篇》里提到了自定义控制器涉及到的 client-go 组件整体工作流程,大致如下图:
DeltaFIFO 是上面的一个重要组件,今天我们来详细研究下 client-go 里 DeltaFIFO 相关代码。
Queue 接口
类似 workqueue 里的队列概念,这里也有一个队列,Queue 接口定义在 client-go/tools/cache 包中的 fifo.go 文件里,看下有哪些方法:
1type Queue interface {
2 Store
3 Pop(PopProcessFunc) (interface{}, error) // 会阻塞,知道有一个元素可以被 pop 出来,或者队列关闭
4 AddIfNotPresent(interface{}) error
5 HasSynced() bool
6 Close()
7}
这里嵌里一个 Store 接口,对应定义如下:
1type Store interface {
2 Add(obj interface{}) error
3 Update(obj interface{}) error
4 Delete(obj interface{}) error
5 List() []interface{}
6 ListKeys() []string
7 Get(obj interface{}) (item interface{}, exists bool, err error)
8 GetByKey(key string) (item interface{}, exists bool, err error)
9 Replace([]interface{}, string) error
10 Resync() error
11}
Store 接口的方法都比较直观,Store 的实现有很多,我们等下看 Queue 里用到的是哪个实现。
Queue 接口的实现是 FIFO 和 DeltaFIFO 两个类型,我们在 Informer 里用到的是 DeltaFIFO,而 DeltaFIFO 也没有依赖 FIFO,所以下面我们直接看 DeltaFIFO 是怎么实现的。
DeltaFIFO
- client-go/tools/cache/delta_fifo.go:97
1type DeltaFIFO struct {
2 lock sync.RWMutex
3 cond sync.Cond
4 items map[string]Deltas
5 queue []string // 这个 queue 里是没有重复元素的,和上面 items 的 key 保持一致
6 populated bool
7 initialPopulationCount int
8 keyFunc KeyFunc // 用于构造上面 map 用到的 key
9 knownObjects KeyListerGetter // 用来检索所有的 keys
10 closed bool
11 emitDeltaTypeReplaced bool
12}
这里有一个 Deltas 类型,看下具体的定义:
1type Deltas []Delta
2
3type Delta struct {
4 Type DeltaType
5 Object interface{}
6}
7
8type DeltaType string
9
10const (
11 Added DeltaType = "Added"
12 Updated DeltaType = "Updated"
13 Deleted DeltaType = "Deleted"
14 Replaced DeltaType = "Replaced"
15 Sync DeltaType = "Sync"
16)
可以看到 Delta 结构体保存的是 DeltaType(就是一个字符串)和发生了这种 Delta 的具体对象。
DeltaFIFO 内部主要维护的一个队列和一个 map,直观一点表示如下:
DeltaFIFO 的 New 函数是 NewDeltaFIFOWithOptions()
- client-go/tools/cache/delta_fifo.go:218
1func NewDeltaFIFOWithOptions(opts DeltaFIFOOptions) *DeltaFIFO {
2 if opts.KeyFunction == nil {
3 opts.KeyFunction = MetaNamespaceKeyFunc
4 }
5
6 f := &DeltaFIFO{
7 items: map[string]Deltas{},
8 queue: []string{},
9 keyFunc: opts.KeyFunction,
10 knownObjects: opts.KnownObjects,
11
12 emitDeltaTypeReplaced: opts.EmitDeltaTypeReplaced,
13 }
14 f.cond.L = &f.lock
15 return f
16}
元素增删改 - queueActionLocked()
可以注意到 DeltaFIFO 的 Add() 等方法等方法体都很简短,大致这样:
1func (f *DeltaFIFO) Add(obj interface{}) error {
2 f.lock.Lock()
3 defer f.lock.Unlock()
4 f.populated = true
5 return f.queueActionLocked(Added, obj)
6}
里面的逻辑就是调用 queueActionLocked()
方法传递对应的 DeltaType 进去,前面提到过 DeltaType 就是 Added、Updated、Deleted 等字符串,所以我们直接先看 queueActionLocked()
方法的实现。
- client-go/tools/cache/delta_fifo.go:409
1func (f *DeltaFIFO) queueActionLocked(actionType DeltaType, obj interface{}) error {
2 id, err := f.KeyOf(obj) // 计算这个对象的 key
3 if err != nil {
4 return KeyError{obj, err}
5 }
6 oldDeltas := f.items[id] // 从 items map 里获取当前的 Deltas
7 newDeltas := append(oldDeltas, Delta{actionType, obj}) // 构造一个 Delta,添加到 Deltas 中,也就是 []Delta 里
8 newDeltas = dedupDeltas(newDeltas) // 如果最近个 Delta 是重复的,则保留后一个;目前版本只处理的 Deleted 重复场景
9
10 if len(newDeltas) > 0 { // 理论上 newDeltas 长度一定大于0
11 if _, exists := f.items[id]; !exists {
12 f.queue = append(f.queue, id) // 如果 id 不存在,则在队列里添加
13 }
14 f.items[id] = newDeltas // 如果 id 已经存在,则只更新 items map 里对应这个 key 的 Deltas
15 f.cond.Broadcast()
16 } else { // 理论上这里执行不到
17 if oldDeltas == nil {
18 klog.Errorf("Impossible dedupDeltas for id=%q: oldDeltas=%#+v, obj=%#+v; ignoring", id, oldDeltas, obj)
19 return nil
20 }
21 klog.Errorf("Impossible dedupDeltas for id=%q: oldDeltas=%#+v, obj=%#+v; breaking invariant by storing empty Deltas", id, oldDeltas, obj)
22 f.items[id] = newDeltas
23 return fmt.Errorf("Impossible dedupDeltas for id=%q: oldDeltas=%#+v, obj=%#+v; broke DeltaFIFO invariant by storing empty Deltas", id, oldDeltas, obj)
24 }
25 return nil
26}
到这里再反过来看 Add() Delete() Update() Get() 等函数,就很清晰了,只是将对应变化类型的 obj 添加到队列中。
Pop()
Pop 按照元素的添加或更新顺序有序返回一个元素(Deltas),在队列为空时会阻塞。另外 Pop 过程会先从队列中删除一个元素然后返回,所以如果处理失败了需要通过 AddIfNotPresent()
方法将这个元素加回到队列中。
Pop 的参数是 type PopProcessFunc func(interface{}) error
类型的 process,中 Pop()
函数中直接将队列里的第一个元素出队,然后丢给 process 处理,如果处理失败会重新入队,但是这个 Deltas 和对应的错误信息会被返回。
- client-go/tools/cache/delta_fifo.go:515
1func (f *DeltaFIFO) Pop(process PopProcessFunc) (interface{}, error) {
2 f.lock.Lock()
3 defer f.lock.Unlock()
4 for { // 这个循环其实没有意义,和下面的 !ok 一起解决了一个不会发生的问题
5 for len(f.queue) == 0 { // 如果为空则进入这个循环
6 if f.closed { // 队列关闭则直接返回
7 return nil, ErrFIFOClosed
8 }
9 f.cond.Wait() // 等待
10 }
11 id := f.queue[0] // queue 里放的是 key
12 f.queue = f.queue[1:] // queue 中删除这个 key
13 depth := len(f.queue)
14 if f.initialPopulationCount > 0 { // 第一次调用 Replace() 插入到元素数量
15 f.initialPopulationCount--
16 }
17 item, ok := f.items[id] // 从 items map[string]Deltas 中获取一个 Deltas
18 if !ok { // 理论上不可能找不到,为此引入了上面的 for 嵌套,感觉不是很好
19 klog.Errorf("Inconceivable! %q was in f.queue but not f.items; ignoring.", id)
20 continue
21 }
22 delete(f.items, id) // items map 中也删除这个元素
23 // 当队列长度超过 10 并且处理一个元素时间超过 0.1 s 时打印日志;队列长度理论上不会变长因为处理一个元素时是阻塞的,这时候新的元素加不进来
24 if depth > 10 {
25 trace := utiltrace.New("DeltaFIFO Pop Process",
26 utiltrace.Field{Key: "ID", Value: id},
27 utiltrace.Field{Key: "Depth", Value: depth},
28 utiltrace.Field{Key: "Reason", Value: "slow event handlers blocking the queue"})
29 defer trace.LogIfLong(100 * time.Millisecond)
30 }
31 err := process(item) // 丢给 PopProcessFunc 处理
32 if e, ok := err.(ErrRequeue); ok { // 如果需要 requeue 则加回到队列里
33 f.addIfNotPresent(id, item)
34 err = e.Err
35 }
36 // 返回这个 Deltas 和错误信息
37 return item, err
38 }
39}
我们看一下 Pop() 的实际调用场景:
- client-go/tools/cache/controller.go:181
1func (c *controller) processLoop() { for { obj, err := c.config.Queue.Pop(PopProcessFunc(c.config.Process)) if err != nil { if err == ErrFIFOClosed { return } if c.config.RetryOnError { c.config.Queue.AddIfNotPresent(obj) // 其实 Pop 内部已经调用了 AddIfNotPresent,这里也有点多余;也许更加健壮吧 } } }}
到这还有一个疑问,就是 process 函数是怎么实现的?我们看 sharedIndexInformer 里的 process 函数逻辑(在我的另外一篇文章:《Kubernetes client-go Informer 源码分析》中会再次详细介绍这个方法):
- client-go/tools/cache/shared_informer.go:537
1func (s *sharedIndexInformer) HandleDeltas(obj interface{}) error { s.blockDeltas.Lock() defer s.blockDeltas.Unlock() // 这个遍历是从旧到新的过程 for _, d := range obj.(Deltas) { switch d.Type { case Sync, Replaced, Added, Updated: // 下面一个 case 是 Deleted s.cacheMutationDetector.AddObject(d.Object) if old, exists, err := s.indexer.Get(d.Object); err == nil && exists { // 更新 indexer if err := s.indexer.Update(d.Object); err != nil { return err } isSync := false switch { case d.Type == Sync: isSync = true case d.Type == Replaced: if accessor, err := meta.Accessor(d.Object); err == nil { if oldAccessor, err := meta.Accessor(old); err == nil { isSync = accessor.GetResourceVersion() == oldAccessor.GetResourceVersion() } } } // 更新通知 s.processor.distribute(updateNotification{oldObj: old, newObj: d.Object}, isSync) } else { // 将 obj 加到 indexer 里 if err := s.indexer.Add(d.Object); err != nil { return err } // 添加通知 s.processor.distribute(addNotification{newObj: d.Object}, false) } case Deleted: // 如果是删除,则从 indexer 中删除 obj if err := s.indexer.Delete(d.Object); err != nil { return err } // 发布一个删除消息 s.processor.distribute(deleteNotification{oldObj: d.Object}, false) } } return nil}
Replace()
Replace() 简单地做两件事:
- 给传入的对象列表添加一个 Sync/Replace DeltaType 的 Delta
- 然后执行一些删除逻辑
这里的 Replace() 过程可以简单理解成传递一个新的 []Deltas 过来,如果当前 DeltaFIFO 里已经有这些元素,则追加一个 Sync/Replace 动作,反之 DeltaFIFO 里多出来的 Deltas 则可能是与 apiserver 失联导致实际已经删除,但是删除动作没有 watch 到的那些对象,所以直接追加一个 Deleted 的 Delta;
1func (f *DeltaFIFO) Replace(list []interface{}, _ string) error { f.lock.Lock() defer f.lock.Unlock() keys := make(sets.String, len(list)) // 用来保存 list 中每个 item 的 key // 老代码兼容逻辑 action := Sync if f.emitDeltaTypeReplaced { action = Replaced } for _, item := range list { // 在每个 item 后面添加一个 Sync/Replaced 动作 key, err := f.KeyOf(item) if err != nil { return KeyError{item, err} } keys.Insert(key) if err := f.queueActionLocked(action, item); err != nil { return fmt.Errorf("couldn't enqueue object: %v", err) } } if f.knownObjects == nil { queuedDeletions := 0 for k, oldItem := range f.items { // 删除 f.items 里的老元素 if keys.Has(k) { continue } var deletedObj interface{} if n := oldItem.Newest(); n != nil { // 如果 Deltas 不为空则有返回值 deletedObj = n.Object } queuedDeletions++ // 标记删除;因为和 apiserver 失联引起的删除状态没有及时获取到,所以这里是 DeletedFinalStateUnknown 类型 if err := f.queueActionLocked(Deleted, DeletedFinalStateUnknown{k, deletedObj}); err != nil { return err } } if !f.populated { f.populated = true f.initialPopulationCount = keys.Len() + queuedDeletions } return nil } knownKeys := f.knownObjects.ListKeys() // key 就是例如 "default/pod_1" 这种字符串 queuedDeletions := 0 for _, k := range knownKeys { if keys.Has(k) { continue } // 新列表里不存在的老元素标记为将要删除 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++ // 添加一个删除动作;因为与 apiserver 失联等场景会引起删除事件没有 wathch 到,所以是 DeletedFinalStateUnknown 类型 if err := f.queueActionLocked(Deleted, DeletedFinalStateUnknown{k, deletedObj}); err != nil { return err } } if !f.populated { f.populated = true f.initialPopulationCount = keys.Len() + queuedDeletions } return nil}
这里有一个 knownObjects 属性,要完整理解 Replace() 逻辑还得看下 knownObjects 是什么逻辑。
我们去跟 knownObjects 属性的初始化,可以看到其引用的是 cache 类型实现的 Store,cache 是实现 Indexer 的那个 cache,Indexer 的源码分析可以在我的另外一篇文章《Kubernetes client-go Indexer / ThreadSafeStore 源码分析》 中看到。
- client-go/tools/cache/store.go:258
1func NewStore(keyFunc KeyFunc) Store { return &cache{ cacheStorage: NewThreadSafeStore(Indexers{}, Indices{}), keyFunc: keyFunc, }}
这里是当作一个 Store 来用,而不是 Indexer。中 NewStore() 函数调用时传递的参数是:
1clientState := NewStore(DeletionHandlingMetaNamespaceKeyFunc)
1// 处理了 DeletedFinalStateUnknown 对象获取 key 问题func DeletionHandlingMetaNamespaceKeyFunc(obj interface{}) (string, error) { if d, ok := obj.(DeletedFinalStateUnknown); ok { return d.Key, nil } return MetaNamespaceKeyFunc(obj)}
所以 knownObjects 通过 cache 类型实例,使用了和 Indexer 类似的机制,通过内部 ThreadSafeStore 来实现了检索队列所有元素的 keys 的能力。
DeltaFIFO 和 Indexer 之间还有一个桥梁 Informer,我们这里简单提到了 sharedIndexInformer 的 HandleDeltas() 方法,后面详细分析 Informer 的逻辑,最终再将整个自定义控制器和 client-go 相关组件逻辑串在一起。
(转载请保留本文原始链接 https://www.danielhu.cn)
Kubernetes client-go DeltaFIFO 源码分析的更多相关文章
- Kubernetes client-go Indexer / ThreadSafeStore 源码分析
Kubernetes client-go Indexer / ThreadSafeStore 源码分析 请阅读原文:原文地址 Contents 概述 Indexer 接口 ThreadSafe ...
- k8s client-go源码分析 informer源码分析(4)-DeltaFIFO源码分析
client-go之DeltaFIFO源码分析 1.DeltaFIFO概述 先从名字上来看,DeltaFIFO,首先它是一个FIFO,也就是一个先进先出的队列,而Delta代表变化的资源对象,其包含资 ...
- kubernetes垃圾回收器GarbageCollector源码分析(一)
kubernetes版本:1.13.2 背景 由于operator创建的redis集群,在kubernetes apiserver重启后,redis集群被异常删除(包括redis exporter s ...
- Kubernetes client-go Informer 源码分析
概述ControllerController 的初始化Controller 的启动processLoopHandleDeltas()SharedIndexInformersharedIndexerIn ...
- client-go客户端自定义开发Kubernetes及源码分析
介绍 client-go 是一种能够与 Kubernetes 集群通信的客户端,通过它可以对 Kubernetes 集群中各资源类型进行 CRUD 操作,它有三大 client 类,分别为:Clien ...
- Kubernetes client-go 源码分析 - Reflector
概述入口 - Reflector.Run()核心 - Reflector.ListAndWatch()Reflector.watchHandler()NewReflector()小结 概述 源码版本: ...
- Kubernetes Deployment 源码分析(二)
概述startDeploymentController 入口逻辑DeploymentController 对象DeploymentController 类型定义DeploymentController ...
- 【原】Spark中Client源码分析(二)
继续前一篇的内容.前一篇内容为: Spark中Client源码分析(一)http://www.cnblogs.com/yourarebest/p/5313006.html DriverClient中的 ...
- Docker源码分析(二):Docker Client创建与命令执行
1. 前言 如今,Docker作为业界领先的轻量级虚拟化容器管理引擎,给全球开发者提供了一种新颖.便捷的软件集成测试与部署之道.在团队开发软件时,Docker可以提供可复用的运行环境.灵活的资源配置. ...
随机推荐
- 基于express框架的留言板实现步骤
这个留言板是基于express框架,和ejs模板引擎,首先需要在根目录安装express框架,然后安装ejs模块和body-parser(获取用户表单提交的数据):建立项目目录 message,然后依 ...
- Uncaught TypeError: document.getElementsById is not a function
今天博主终于开始攻关javascript(俗称js)了,不过要注意了,它和java可是一丁点关系都没有,就像老婆饼和老婆一样. 下面就让我们来讨论一下博主这次犯下的低级错误吧 一.背景(解决方法在文末 ...
- FastAPI(六十八)实战开发《在线课程学习系统》接口开发--用户 个人信息接口开发
在之前的文章:FastAPI(六十七)实战开发<在线课程学习系统>接口开发--用户登陆接口开发,今天实战:用户 个人信息接口开发. 在开发个人信息接口的时候,我们要注意了,因为我们不一样的 ...
- 线程的概念及Thread模块的使用
线程 一.什么是线程? 我们可以把进程理解成一个资源空间,真正被CPU执行的就是进程里的线程. 一个进程中最少会有一条线程,同一进程下的每个线程之间资源是共享的. 二.开设线程的两种方式 开设进程需要 ...
- 洛谷 P2392 kkksc03考前临时抱佛脚, dp / 深搜
题目链接 P2392 kkksc03考前临时抱佛脚 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 题目 dp代码 #include <iostream> #includ ...
- 理解ASP.NET Core - 授权(Authorization)
注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 之前,我们已经了解了ASP.NET Core中的身份认证,现在,我们来聊一下授权. 老规矩,示 ...
- [原创][开源]C# Winform DPI自适应方案,SunnyUI三步搞定
SunnyUI.Net, 基于 C# .Net WinForm 开源控件库.工具类库.扩展类库.多页面开发框架 Blog: https://www.cnblogs.com/yhuse Gitee: h ...
- 《手把手教你》系列基础篇(九十四)-java+ selenium自动化测试-框架设计基础-POM设计模式实现-下篇(详解教程)
1.简介 上一篇宏哥用PageFactory实现了POM,宏哥再介绍一下如果不用PageFactory如何实现POM. 2.项目实战 在这里宏哥以百度首页登录的例子,如果用POM实现,在测试脚本中实际 ...
- come on! 基于LinkedHashMap实现LRU缓存
/** * @Description 基于LinkedHashMap实现一个基于'LRU最近最少使用'算法的缓存,并且最多存MAX个值 * @Author afei * @date:2021/4/25 ...
- Bugku CTF练习题---MISC---宽带信息泄露
Bugku CTF练习题---MISC---宽带信息泄露 flag:053700357621 解题步骤: 1.观察题目,下载附件 2.下载到电脑里发现是一个bin文件,二进制文件的一个种类,再看名称为 ...