概述入口 - Reflector.Run()核心 - Reflector.ListAndWatch()Reflector.watchHandler()NewReflector()小结

概述

源码版本:kubernetes master 分支 commit-fe62fc(2021年10月14日)

回顾一下 Reflector 在整个自定义控制器工作流中的位置:

《Kubernetes client-go 源码分析 - 开篇》中我们提到过 Reflector 的任务就是向 apiserver watch 特定类型的资源,拿到变更通知后将其丢到 DeltaFIFO 队列中。另外前面已经在 《Kubernetes client-go 源码分析 - ListWatcher》中分析过 ListWatcher 是如何从 apiserver 中 list-watch 资源的,今天我们继续来看 Reflector 的实现。

入口 - Reflector.Run()

Reflector 的启动入口是 Run() 方法:

  • client-go/tools/cache/reflector.go:218
1func (r *Reflector) Run(stopCh <-chan struct{}) {
2   klog.V(3).Infof("Starting reflector %s (%s) from %s", r.expectedTypeName, r.resyncPeriod, r.name)
3   wait.BackoffUntil(func() {
4      if err := r.ListAndWatch(stopCh); err != nil {
5         r.watchErrorHandler(r, err)
6      }
7   }, r.backoffManager, true, stopCh)
8   klog.V(3).Infof("Stopping reflector %s (%s) from %s", r.expectedTypeName, r.resyncPeriod, r.name)
9}

这里有一些健壮性机制,用于处理 apiserver 短暂失联的场景。我们直接来看主要逻辑先,也就是 Reflector.ListAndWatch() 方法的内容。

核心 - Reflector.ListAndWatch()

Reflector.ListAndWatch() 方法有将近 200 行,是 Reflector 的核心逻辑之一。ListAndWatch() 方法做的事情是先 list 特定资源的所有对象,然后获取其资源版本,接着使用这个资源版本来开始 watch 流程。watch 到新版本资源然后将其加入 DeltaFIFO 的动作是在 watchHandler() 方法中具体实现的,后面一节会单独分析。在此之前 list 到的最新 items 会通过 syncWith() 方法添加一个 Sync 类型的 DeltaType 到 DeltaFIFO 中,所以 list 操作本身也会触发后面的调谐逻辑运行。具体来看:

  • client-go/tools/cache/reflector.go:254
  1func (r *Reflector) ListAndWatch(stopCh <-chan struct{}) error {
2   klog.V(3).Infof("Listing and watching %v from %s", r.expectedTypeName, r.name)
3   var resourceVersion string
4
5   // 当 r.lastSyncResourceVersion 为 "" 时这里为 "0",当使用 r.lastSyncResourceVersion 失败时这里为 ""
6   // 区别是 "" 会直接请求到 etcd,获取一个最新的版本,而 "0" 访问的是 cache
7   options := metav1.ListOptions{ResourceVersion: r.relistResourceVersion()}
8
9   if err := func() error {
10      // trace 是用于记录操作耗时的,这里的逻辑是超过 10s 的步骤打印出来
11      initTrace := trace.New("Reflector ListAndWatch", trace.Field{"name", r.name})
12      defer initTrace.LogIfLong(10 * time.Second)
13      var list runtime.Object
14      var paginatedResult bool
15      var err error
16      listCh := make(chan struct{}, 1)
17      panicCh := make(chan interface{}, 1)
18      go func() { // 内嵌一个函数,这里会直接调用
19         defer func() {
20            if r := recover(); r != nil { // 收集这个 goroutine panic 的时候将奔溃信息
21               panicCh <- r
22            }
23         }()
24         // 开始尝试收集 list 的 chunks,我们在 《Kubernetes List-Watch 机制原理与实现 - chunked》中介绍过相关逻辑
25         pager := pager.New(pager.SimplePageFunc(func(opts metav1.ListOptions) (runtime.Object, error) {
26            return r.listerWatcher.List(opts)
27         }))
28         switch {
29         case r.WatchListPageSize != 0:
30            pager.PageSize = r.WatchListPageSize
31         case r.paginatedResult:
32         case options.ResourceVersion != "" && options.ResourceVersion != "0":
33            pager.PageSize = 0
34         }
35
36         list, paginatedResult, err = pager.List(context.Background(), options)
37         if isExpiredError(err) || isTooLargeResourceVersionError(err) {
38            // 设置这个属性后,下一次 list 会从 etcd 里取
39            r.setIsLastSyncResourceVersionUnavailable(true)
40            list, paginatedResult, err = pager.List(context.Background(), metav1.ListOptions{ResourceVersion: r.relistResourceVersion()})
41         }
42         close(listCh)
43      }()
44      select {
45      case <-stopCh:
46         return nil
47      case r := <-panicCh:
48         panic(r)
49      case <-listCh:
50      }
51      if err != nil {
52         return fmt.Errorf("failed to list %v: %v", r.expectedTypeName, err)
53      }
54
55      if options.ResourceVersion == "0" && paginatedResult {
56         r.paginatedResult = true
57      }
58
59      // list 成功
60      r.setIsLastSyncResourceVersionUnavailable(false)
61      initTrace.Step("Objects listed")
62      listMetaInterface, err := meta.ListAccessor(list)
63      if err != nil {
64         return fmt.Errorf("unable to understand list result %#v: %v", list, err)
65      }
66      resourceVersion = listMetaInterface.GetResourceVersion()
67      initTrace.Step("Resource version extracted")
68      items, err := meta.ExtractList(list)
69      if err != nil {
70         return fmt.Errorf("unable to understand list result %#v (%v)", list, err)
71      }
72      initTrace.Step("Objects extracted")
73      // 将 list 到的 items 添加到 store 里,这里是 store 也就是 DeltaFIFO,也就是添加一个 Sync DeltaType 这里的 resourveVersion 并没有用到
74      if err := r.syncWith(items, resourceVersion); err != nil {
75         return fmt.Errorf("unable to sync list result: %v", err)
76      }
77      initTrace.Step("SyncWith done")
78      r.setLastSyncResourceVersion(resourceVersion)
79      initTrace.Step("Resource version updated")
80      return nil
81   }(); err != nil {
82      return err
83   }
84
85   resyncerrc := make(chan error, 1)
86   cancelCh := make(chan struct{})
87   defer close(cancelCh)
88   go func() {
89      resyncCh, cleanup := r.resyncChan()
90      defer func() {
91         cleanup()
92      }()
93      for {
94         select {
95         case <-resyncCh:
96         case <-stopCh:
97            return
98         case <-cancelCh:
99            return
100         }
101         if r.ShouldResync == nil || r.ShouldResync() {
102            klog.V(4).Infof("%s: forcing resync", r.name)
103            if err := r.store.Resync(); err != nil {
104               resyncerrc <- err
105               return
106            }
107         }
108         cleanup()
109         resyncCh, cleanup = r.resyncChan()
110      }
111   }()
112
113   for {
114      select {
115      case <-stopCh:
116         return nil
117      default:
118      }
119      // 超时时间是 5-10分钟
120      timeoutSeconds := int64(minWatchTimeout.Seconds() * (rand.Float64() + 1.0))
121      options = metav1.ListOptions{
122         ResourceVersion: resourceVersion,
123         // 如果超时没有接收到任何 Event,这时候需要停止 watch,避免一直挂着
124         TimeoutSeconds: &timeoutSeconds,
125         // 用于降低 apiserver 压力,bookmark 类型响应的对象主要只有 RV 信息
126         AllowWatchBookmarks: true,
127      }
128
129      start := r.clock.Now()
130      // 调用 watch
131      w, err := r.listerWatcher.Watch(options)
132      if err != nil {
133         // 这时候直接 re-list 已经没有用了,apiserver 暂时拒绝服务
134         if utilnet.IsConnectionRefused(err) || apierrors.IsTooManyRequests(err) {
135            <-r.initConnBackoffManager.Backoff().C()
136            continue
137         }
138         return err
139      }
140      // 核心逻辑之一,后面单独会讲到
141      if err := r.watchHandler(start, w, &resourceVersion, resyncerrc, stopCh); err != nil {
142         if err != errorStopRequested {
143            switch {
144            case isExpiredError(err):
145               klog.V(4).Infof("%s: watch of %v closed with: %v", r.name, r.expectedTypeName, err)
146            case apierrors.IsTooManyRequests(err):
147               klog.V(2).Infof("%s: watch of %v returned 429 - backing off", r.name, r.expectedTypeName)
148               <-r.initConnBackoffManager.Backoff().C()
149               continue
150            default:
151               klog.Warningf("%s: watch of %v ended with: %v", r.name, r.expectedTypeName, err)
152            }
153         }
154         return nil
155      }
156   }
157}

Reflector.watchHandler()

watchHandler() 方法中完成了将 watch 到的 Event 根据其 EventType 分别调用 DeltaFIFOAdd()/Update/Delete() 等方法完成对象追加到 DeltaFIFO 队列的过程。watchHandler() 方法的调用在一个 for 循环中,所以一次 watchHandler() 工作流程完成后,函数退出,新一轮的调用会传递进来新的 watch.InterfaceresourceVersion 等,我们具体来看。

  • client-go/tools/cache/reflector.go:459
 1func (r *Reflector) watchHandler(start time.Time, w watch.Interface, resourceVersion *string, errc chan error, stopCh <-chan struct{}) error {
2   eventCount := 0
3
4   // 当前函数返回时需要关闭 watch.Interface,因为新一轮的调用会传递新的 watch.Interface 进来
5   defer w.Stop()
6
7loop:
8   for {
9      select {
10      case <-stopCh:
11         return errorStopRequested
12      case err := <-errc:
13         return err
14        // 接收 event
15      case event, ok := <-w.ResultChan():
16         if !ok {
17            break loop
18         }
19         // 如果是 "ERROR"
20         if event.Type == watch.Error {
21            return apierrors.FromObject(event.Object)
22         }
23         // 创建 Reflector 的时候会指定一个 expectedType
24         if r.expectedType != nil {
25            // 类型不匹配
26            if e, a := r.expectedType, reflect.TypeOf(event.Object); e != a {
27               utilruntime.HandleError(fmt.Errorf("%s: expected type %v, but watch event object had type %v", r.name, e, a))
28               continue
29            }
30         }
31         // 没有对应 Golang 结构体的对象可以通过这种方式来指定期望类型
32         if r.expectedGVK != nil {
33            if e, a := *r.expectedGVK, event.Object.GetObjectKind().GroupVersionKind(); e != a {
34               utilruntime.HandleError(fmt.Errorf("%s: expected gvk %v, but watch event object had gvk %v", r.name, e, a))
35               continue
36            }
37         }
38         meta, err := meta.Accessor(event.Object)
39         if err != nil {
40            utilruntime.HandleError(fmt.Errorf("%s: unable to understand watch event %#v", r.name, event))
41            continue
42         }
43         // 新的 ResourceVersion
44         newResourceVersion := meta.GetResourceVersion()
45         switch event.Type {
46         // 调用 DeltaFIFO 的 Add/Update/Delete 等方法完成不同类型 Event 等处理,我们在《Kubernetes client-go 源码分析 - DeltaFIFO》详细介绍过 DeltaFIFO 对应的 Add/Update/Delete 是如何实现的
47         case watch.Added:
48            err := r.store.Add(event.Object)
49            if err != nil {
50               utilruntime.HandleError(fmt.Errorf("%s: unable to add watch event object (%#v) to store: %v", r.name, event.Object, err))
51            }
52         case watch.Modified:
53            err := r.store.Update(event.Object)
54            if err != nil {
55               utilruntime.HandleError(fmt.Errorf("%s: unable to update watch event object (%#v) to store: %v", r.name, event.Object, err))
56            }
57         case watch.Deleted:
58            err := r.store.Delete(event.Object)
59            if err != nil {
60               utilruntime.HandleError(fmt.Errorf("%s: unable to delete watch event object (%#v) from store: %v", r.name, event.Object, err))
61            }
62         case watch.Bookmark:
63         default:
64            utilruntime.HandleError(fmt.Errorf("%s: unable to understand watch event %#v", r.name, event))
65         }
66         // 更新 resourceVersion
67         *resourceVersion = newResourceVersion
68         r.setLastSyncResourceVersion(newResourceVersion)
69         if rvu, ok := r.store.(ResourceVersionUpdater); ok {
70            rvu.UpdateResourceVersion(newResourceVersion)
71         }
72         eventCount++
73      }
74   }
75   // 耗时
76   watchDuration := r.clock.Since(start)
77   // 1s 就结束了,而且没有收到 event,属于异常情况
78   if watchDuration < 1*time.Second && eventCount == 0 {
79      return fmt.Errorf("very short watch: %s: Unexpected watch close - watch lasted less than a second and no items received", r.name)
80   }
81   klog.V(4).Infof("%s: Watch close - %v total %v items received", r.name, r.expectedTypeName, eventCount)
82   return nil
83}

NewReflector()

继续来看下 Reflector 的初始化。NewReflector() 的参数里有一个 ListerWatcher 类型的 lw,还有有一个 expectedType 和 store,lw 就是我们在《Kubernetes client-go 源码分析 - ListWatcher》中介绍的那个 ListerWatcher,expectedType指定期望关注的类型,而 store 是一个 DeltaFIFO,我们在《Kubernetes client-go 源码分析 - DeltaFIFO》中也有详细的介绍过。加在一起大致可以预想到 Reflector 通过 ListWatcher 提供的能力去 list-watch apiserver,然后将 Event 加到 DeltaFIFO 中。

  • client-go/tools/cache/reflector.go:166
 1func NewReflector(lw ListerWatcher, expectedType interface{}, store Store, resyncPeriod time.Duration) *Reflector {
2   // 直接调用下面的 NewNamedReflector
3   return NewNamedReflector(naming.GetNameFromCallsite(internalPackages...), lw, expectedType, store, resyncPeriod)
4}
5
6func NewNamedReflector(name string, lw ListerWatcher, expectedType interface{}, store Store, resyncPeriod time.Duration) *Reflector {
7   realClock := &clock.RealClock{}
8   r := &Reflector{
9      name:          name,
10      listerWatcher: lw,
11      store:         store,
12      // 重试机制,这里可以有效降低 apiserver 的负载,也就是重试间隔会越来越长
13      backoffManager:         wait.NewExponentialBackoffManager(800*time.Millisecond, 30*time.Second, 2*time.Minute, 2.0, 1.0, realClock),
14      initConnBackoffManager: wait.NewExponentialBackoffManager(800*time.Millisecond, 30*time.Second, 2*time.Minute, 2.0, 1.0, realClock),
15      resyncPeriod:           resyncPeriod,
16      clock:                  realClock,
17      watchErrorHandler:      WatchErrorHandler(DefaultWatchErrorHandler),
18   }
19   r.setExpectedType(expectedType)
20   return r
21}

小结

如文章开头的图中所示,Reflector 的职责很清晰,要做的事情是保持 DeltaFIFO 中的 items 持续更新,具体实现是通过 ListWatcher 提供的 list-watch 能力来 list 指定类型的资源,这时候会产生一系列 Sync 事件,然后通过 list 到的 ResourceVersion 来开启 watch 过程,而 watch 到新的事件后,会和前面提到的 Sync 事件一样,都通过 DeltaFIFO 提供的方法构造相应的 DeltaType 添加到 DeltaFIFO 中。当然前面提到的更新也并不是直接修改 DeltaFIFO 中已经存在的 items,而是添加一个新的 DeltaType 到队列中。另外 DeltaFIFO 中添加新 DeltaType 的时候也会有一定的去重机制,我们以前在 ListWatcher 和 DeltaFIFO 中分别介绍过这两个组件的工作逻辑,有了这个基础后再看 Reflector 的工作流就相对轻松很多了。这里还有一个细节就是 watch 过程不是一劳永逸的,watch 到新的 event 后,会拿着对象的新 ResourceVersion 重新开启一轮新的 watch 过程。当然这里的 watch 调用也有超时机制,一系列的健壮性措施,所以我们脱离 Reflector(Informer) 直接使用 list-watch 还是很难手撕一套健壮的代码出来。

(转载请保留本文原始链接 https://www.danielhu.cn)

Kubernetes client-go 源码分析 - Reflector的更多相关文章

  1. Kubernetes client-go Informer 源码分析

    概述ControllerController 的初始化Controller 的启动processLoopHandleDeltas()SharedIndexInformersharedIndexerIn ...

  2. Kubernetes client-go DeltaFIFO 源码分析

    概述Queue 接口DeltaFIFO元素增删改 - queueActionLocked()Pop()Replace() 概述 源码版本信息 Project: kubernetes Branch: m ...

  3. Kubernetes client-go workqueue 源码分析

    概述Queue接口和结构体setAdd()Get()Done()DelayingQueue接口和结构体waitForNewDelayingQueuewaitingLoop()AddAfter()Rat ...

  4. Kubernetes Deployment 源码分析(二)

    概述startDeploymentController 入口逻辑DeploymentController 对象DeploymentController 类型定义DeploymentController ...

  5. Eureka 源码分析之 Eureka Client

    文章首发于微信公众号<程序员果果> 地址:https://mp.weixin.qq.com/s/47TUd96NMz67_PCDyvyInQ 简介 Eureka是一种基于REST(Repr ...

  6. SSO单点登录系列1:cas客户端源码分析cas-client-java-2.1.1.jar

    落雨 cas 单点登录 希望能给以后来研究cas的兄弟留下一点思路,也算是研究了两天的成果,外国人的代码写的很晦涩,翻译下来也没有时间继续跟进,所以有错误的还请大家跟帖和我讨论,qq 39426378 ...

  7. k8s client-go源码分析 informer源码分析(3)-Reflector源码分析

    k8s client-go源码分析 informer源码分析(3)-Reflector源码分析 1.Reflector概述 Reflector从kube-apiserver中list&watc ...

  8. client-go客户端自定义开发Kubernetes及源码分析

    介绍 client-go 是一种能够与 Kubernetes 集群通信的客户端,通过它可以对 Kubernetes 集群中各资源类型进行 CRUD 操作,它有三大 client 类,分别为:Clien ...

  9. 【原】Spark中Client源码分析(二)

    继续前一篇的内容.前一篇内容为: Spark中Client源码分析(一)http://www.cnblogs.com/yourarebest/p/5313006.html DriverClient中的 ...

随机推荐

  1. IO流学习笔记(一)之FileWriter与FileReader

    IO流用来处理设备之间的数据传输 Java对数据的操作是通过流的方式 Java用于操作流的对象都在IO包中 流按照操作数据分为两种:字节流和字符流 流按流向分为:输入流和输出流 输入流和输出流是相对于 ...

  2. shp平滑处理

    在做图像数据处理时,经常会有栅格数据转矢量数据的操作,转换后的矢量文件会存在锯齿状边缘,不太美观,因此常常需要对矢量(shp)文件做平滑处理. 1 利用arcgis实现shp的平滑和简化 ArcToo ...

  3. python类、继承

    Python 是一种面向对象的编程语言.Python 中的几乎所有东西都是对象,拥有属性和方法.类(Class)类似对象构造函数,或者是用于创建对象的"蓝图". 一.python ...

  4. 20210804 noip30

    考场 第一眼感觉 T1 是状压 DP,弃了.T2 好像也是 DP???看上去 T3 比较可做. 倒序开题.T3 暴力是 \(O(pn\log p)\)(枚举 \(x\),二分答案,看能否分成合法的不超 ...

  5. [考试总结]noip模拟40

    最近真的是爆炸啊... 到现在还是有不少没改出来.... 所以先写一下 \(T1\) 的题解.... 送花 我们移动右端点,之后我们用线段树维护全局最大值. 之后还要记录上次的位置和上上次的位置. 之 ...

  6. C# Dapper基本三层架构使用 (四、Web UI层)

    三层架构的好处,一套代码无论WinForm还是Web都可以通用,只写前台逻辑就可以了,现在展示Web调用三层的示例 首先在项目中创建一个Web MVC5项目,目前项目目录如下 在Web项目Web.co ...

  7. 聊聊ReentrantLock基于AQS的公平锁和非公平锁的实现区别

    ReentrantLock锁的实现是基于AQS实现的,所以先简单说下AQS: AQS是AbstractQueuedSynchronizer缩写,顾名思义:抽象的队列同步器,它是JUC里面许多同步工具类 ...

  8. Linux系列(37) - 源码包与RPM包区别(1)

    源码包是不能使用[service]命令来启动服务,因为源码包的安装位置由用户指定 源码包一般安装在: /usr/local/软件名/ ,源码包安装的服务,只能用绝对路径进行服务的管理 rpm包安装后, ...

  9. 使用uView UI+UniApp开发微信小程序--微信授权绑定和一键登录系统

    在前面随笔<使用uView UI+UniApp开发微信小程序>和<使用uView UI+UniApp开发微信小程序--判断用户是否登录并跳转>介绍了微信小程序的常规登录处理和验 ...

  10. 【大咖直播】Elastic 企业搜索实战工作坊(第一期)

    借助 App Search 提供的内置功能,您可轻松打造卓越的搜索体验.直观的相关度调整以及开箱即用的搜索分析,不仅可以优化所提供的内容,其提供的 API 还可帮助您将位于各处的所有内容源关联在一起. ...