Client-go学习--Informer中的Reflector

3,750 阅读20分钟

Reflector

写在前面

本文将围绕着Informer架构中的Reflector进行详细讲解。需要读者掌握client-go基本使用。源码摘抄部分只摘抄了笔者认为比较核心的部分,感兴趣的小伙伴可以阅读源代码,以加深对组件的掌握。

由于是对源码进行解析,一千个人心中有一千种哈姆雷特,如果有哪里写的不对,意思不准确,欢迎大家指正。至此感谢。

本文基于以下环境

go1.20

k8s.io/api v0.27.1

k8s.io/apimachinery v0.27.1

k8s.io/client-go v0.27.1

1.引子

我们先来看一张图:

image.png

我们对k8s进行二次开发时,informer组件是最为核心的组件,它涵盖了从k8sApiServer获取资源,将资源通过Queue中进行缓存,在store中进行存放,通过Processor中对应的Eventhandle进行处理。继续上一篇内容,本篇我们将对Reflector源码进行探究,研究一下在Informer中,是如何获取k8s资源的?获取了资源又是如何进行处理的?

2.Reflector

Reflector组件位于client-go/tools/cache包中,该组件主要是围绕着如何获取k8s资源,以及如何对后续k8s资源的变更进行监听。本质上还是通过我们上篇讲的List/Watch进行网络请求获取资源。如果有不熟悉的朋友可以看看我上一篇文章中对k8sAPI中的知识铺垫。

2.1 一个小栗子

我们先尝试原生使用Reflector进行获取k8s资源。代码如下:

import (
	"fmt"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/fields"
	"k8s.io/apimachinery/pkg/util/wait"
	"k8s.io/client-go/tools/cache"
	"kube/kubecli"
)

const podResource = "pods"

func ReflectorRun() {
	// 这里是我对client-go中的客户端连接进行了一个封装
    // 你们可以直接使用原生获取client
	cliset := kubecli.DefaultKubeClient().ClientSet()

	// 1.create store
	store := cache.NewStore(func(obj interface{}) (string, error) {
		pod := obj.(*corev1.Pod)
		return pod.Name, nil
	})

	// 2.create watch
	lwc := cache.NewListWatchFromClient(
		cliset.CoreV1().RESTClient(),
		podResource,
		corev1.NamespaceDefault,
		fields.Everything(),
	)

	// 3.create delta FIFO queue
	df := cache.NewDeltaFIFOWithOptions(
		cache.DeltaFIFOOptions{
			KeyFunction:  cache.MetaNamespaceKeyFunc,
			KnownObjects: store,
		},
	)

	// 4.create reflector
	// reflector 会将其存入df的store中
	reflector := cache.NewReflectorWithOptions(
		lwc,
		&corev1.Pod{},
		df,
		cache.ReflectorOptions{},
	)

	go func() {
        // reflector运行
		reflector.Run(wait.NeverStop)
	}()

	for {
		df.Pop(func(obj interface{}, isInInitialList bool) error {
			deltas := obj.(cache.Deltas)
			for _, delta := range deltas {
				pod := delta.Object.(*corev1.Pod)
				switch delta.Type {
				case cache.Sync:
					fmt.Printf("enventType: %+v  podName: %+v \n", delta.Type, pod.Name)
				case cache.Added:
					fmt.Printf("enventType: %+v  podName: %+v \n", delta.Type, pod.Name)
				case cache.Updated:
					fmt.Printf("enventType: %+v  podName: %+v \n", delta.Type, pod.Name)
				}
			}
			return nil
		})
	}
}

以上一共有几个很关键的组件:

  • Store: 可以对Reflector获取的资源进行存储
  • ListWatch: 对ApiServer发起List/Watch请求的实际实现
  • DelataFIFO: 类似于缓存的FIFO队列,可以对list的全量同步资源和watch的增量同步资源进行缓存,并且可以对同一个资源的不同状态进行追踪。

这些组件后续我会通过文章详细讲解其源码和实现原理细节。本章仅对Reflector进行讲解

我们运行一下上述代码可以看到输出,在代码刚启动时,可以看到enventType都是Sync/Replaced,这是因为Reflector正在进行第一次的全量更新(也可以通过emitDeltaTypeReplaced字段设置,所有有些会输出Replaced)。如果后续对一个资源进行更新(删除/新增/更新)都可以获取对应的输出。

enventType: Sync  podName: first-trigger-pipelinerunwbps6-pipeline-first-trigger-task-pod 
enventType: Sync  podName: webhook-test 
enventType: Sync  podName: first-trigger-pipelinerunl675b-pipeline-first-trigger-task-pod 
enventType: Sync  podName: el-first-trigger-listener-5fdc66645c-747qt 
enventType: Sync  podName: nginx-cluster-db7d5495d-mvdgd 
enventType: Sync  podName: nginx-cluster-db7d5495d-tjrqw 
enventType: Sync  podName: first-trigger-pipelinerunppnz6-pipeline-first-trigger-task-pod 
enventType: Sync  podName: first-trigger-pipelineruncrs9c-pipeline-first-trigger-task-pod 
enventType: Sync  podName: nginx-cluster-db7d5495d-hp5gj 

2.2 Reflector Struct

这一个小节我们看一下Reflector结构,以下主要展示最常用,重要的字段,感兴趣的小伙伴可以查看源码进行深入了解学习。

type Reflector struct {
	// Name标识Reflector
	name string
	// 我们希望放置在store中的类型的名称。如果提供,名称将是expectedGVK的字符串化,否则将是expectedType的字符串化。
	// 它仅用于显示,不应用于解析或比较。
	// gvk的字符串化
	typeDescription string
	// 我们期望放置在存储库中的类型的示例对象。 例如: &corev1.Pod{}
	expectedType reflect.Type
	// 如果是非结构化的,我们希望放在`store`中的对象的GVK。
	expectedGVK *schema.GroupVersionKind
	// watch的指定存储`store`
	store Store
	// 用于执行list和watch接口
    // 可以传入自定义的实现
	listerWatcher ListerWatcher
	// 重新同步的周期时间
	resyncPeriod time.Duration
	// paginatedResult定义是否应该对列表调用强制分页。
	// 它是根据初始列表调用的结果设置的。
	paginatedResult bool
	// lastSyncResourceVersion是与底层存储同步时最后观察到的资源版本令牌,
	// 它是线程安全的,但不与底层存储同步 -> 底层存储同步一定情况下,会慢于server端资源
	// 存储的是最后一次watch的资源令牌
	// 类似etcd的revision
	lastSyncResourceVersion string
	// 如果使用lastSyncResourceVersion的前一个list或watch请求失败并出现“过期”或“资源版本太大”错误。
	// 则isLastSyncResourceVersionUnavailable为true。
	// 资源不可获取的标志
	isLastSyncResourceVersionUnavailable bool
	// lastSyncResourceVersionMutex保护对lastSyncResourceVersion的读写访问
	// 用于对lastSyncResourceVersion令牌的读写锁
	lastSyncResourceVersionMutex sync.RWMutex
	// ListAndWatch连接错误时候调用
	watchErrorHandler WatchErrorHandler
    
    ...
    ...
}

在这些字段中,其中lastSyncResourceVersionlisterWatcher相互搭配获取k8s资源。与我们通过HTTP请求获取资源类似。

2.3 Reflector New

我们看一下创建Reflector的过程,在这个func中,我们对Reflector的属性进行设置,其中比较关键几个点,ListWatch对指定资源进行全量同步和增量同步,expectedType是为了后续watch增量同步进行比对,查看watch的资源和expectedType是否匹配,expectedGVK用于判断watch资源的GVK是否匹配。

// 以下摘取部分代码
func NewReflectorWithOptions(lw ListerWatcher, expectedType interface{}, store Store, options ReflectorOptions) *Reflector {
    
  	... 
    // watch异常处理器
	if options.WatchErrorHandler == nil {
		options.WatchErrorHandler = DefaultWatchErrorHandler
	}

	// 创建一个新的Reflector
	r := &Reflector{
		name:            options.Name,
		resyncPeriod:    options.ResyncPeriod,
		typeDescription: options.TypeDescription,
		// listWatch对象 (底层用于检测使用)
		listerWatcher: lw,
		// 存储对象 检测完的数据进行存储
		store: store,
		// 使用默认的WatchErrorHandle
		watchErrorHandler: options.WatchErrorHandler,
		// 根据反射获取源类型
		expectedType: reflect.TypeOf(expectedType),
        .......
	}


	// 判断 type的GVK
        // 这里是通过断言成unstructured
        // 进而获取内嵌的GVK
	if r.expectedGVK == nil {
		r.expectedGVK = getExpectedGVKFromObject(expectedType)
	}
	return r
}

2.4 Reflector Run

这小节我们看一下Reflector的入口。我们在2.1的例子中也看到,Run方法实际上是传入一个控制停止的chan。这个stopCh非常关键,贯穿了整个Reflector内部组件,可以用于控制内部的同步/watch/sync等。Run方法中通过一个BackoffUntil,其作用是: 如果发生错误则进行回退 等待再进行调用,基于指数回退算法,意味着每次重试时,时间间隔会逐渐增加,以避免过度频繁地尝试。直到该函数返回一个成功的结果或达到最大重试次数。避免失败无限制的重试,导致不断消耗Server的资源。

// Run重复使用reflector的listanWatch来获取所有对象和后续增量。
// 当stopCh关闭时,Run将退出。
func (r *Reflector) Run(stopCh <-chan struct{}) {
	// 如果发生错误则进行回退 等待再进行调用
	// 基于指数回退算法,意味着每次重试时,时间间隔会逐渐增加,以避免过度频繁地尝试。
	// 直到该函数返回一个成功的结果或达到最大重试次数。
	wait.BackoffUntil(func() {
		// 调用list and watch
		if err := r.ListAndWatch(stopCh); err != nil {
			r.watchErrorHandler(r, err)
		}
	}, r.backoffManager, true, stopCh)
}

2.5 Reflector ListAndWatch

ListAndWatch方法涵盖了Relfector的主要逻辑函数:

  • ⭐ 首先是进行全量更新,判断是通过流获取还是块获取。(本文讲解块方式获取)

  • ⭐ 然后是开启一个新协程将全量更新的资源,同步到底层的store

  • ⭐ 最后通过loop进行监测watch指定的资源后续更新。

// listwatch首先列出所有项目,并在调用时获取资源版本`resourceVersion` ,然后使用资源版本`resourceVersion`进行监视。
// 如果listwatch甚至没有尝试初始化watch,它将返回错误。
// 假设client第一次获取到 resourceVersion = 20000
// 那么后续watch将会基于这个resourceVersion版本进行watch
// 其中: stopCh 是一个很重要的控制通道,用于控制大部分组件的生命
func (r *Reflector) ListAndWatch(stopCh <-chan struct{}) error {
    ...
    
	var err error
	var w watch.Interface
	// 是否使用流检测
	fallbackToList := !r.UseWatchList
	// 使用`流`获取
	if r.UseWatchList {
        ...
		}
	}

	// 使用普通的`块`获取 内部开启了协程作为第一次的全量获取(List)
	if fallbackToList {
		err = r.list(stopCh)
		if err != nil {
			return err
		}
	}

	// 重新同步错误chan
	resyncerrc := make(chan error, 1)
	// 取消任务chan
	cancelCh := make(chan struct{})
	defer close(cancelCh)
	// ⭐: store同步方法
	go r.startResync(stopCh, cancelCh, resyncerrc)

	// ⭐: 核心二 `watch`方法
	return r.watch(w, stopCh, resyncerrc)
}

3. Reflector Core

本小节解析Reflector最为核心的方法,由于这部分称得上是Reflector最精华的部分,所以我会拆分成小节进行讲解。

3.1 List

我们先来看看List全量更新,到底底层是怎么操作的。先来看看上半部分。

首先通过一个新协程进行处理list,通过构建一个分页器,进行对应分页的处理。然后通过pager进行调用真正的list请求。此时会判断,如果资源超时或者410错误,则会重新发起一次获取最新资源的请求。随后关闭listChchan。让外部的select通道监听关闭信号。继续往下执行

	// resourceVersion版本
	var resourceVersion string
	// 初始化设置版本为 "0" 获取任何版本的ListOptions
	options := metav1.ListOptions{ResourceVersion: r.relistResourceVersion()}

	var list runtime.Object
	var paginatedResult bool
	// 此函数中公用err
	var err error

	// 用于接受结束分页的信号chan
	listCh := make(chan struct{}, 1)
	// 用于接受 发生错误的信号chan
	panicCh := make(chan interface{}, 1)

	// 单独开启一个协程去获取list
	go func() {

		defer func() {
			// 发生错误则放入 panicChan
			if r := recover(); r != nil {
				panicCh <- r
			}
		}()
        
		// 尝试收集列表块,如果listwatcher支持, (实际上就是类似分页查询)
		// 如果不支持,第一个列表请求将返回完整的响应。
		// 构造一个分页器
		pager := pager.New(pager.SimplePageFunc(func(opts metav1.ListOptions) (runtime.Object, error) {
			// 返回lw的listWatch的list
            // 这个list函数是进行实际发起请求的函数
			return r.listerWatcher.List(opts)
		}))

		switch {
		// 查看是否开启分块
		case r.WatchListPageSize != 0:
			pager.PageSize = r.WatchListPageSize
            
		// 查看是否强制开启分块
		case r.paginatedResult:
            
		// 证明开启了检测指定版本
		case options.ResourceVersion != "" && options.ResourceVersion != "0":
			// 用户没有显式请求分页。
			// 使用ResourceVersion != "",我们有可能从`watch`缓存中列出列表,但我们只在Limit未设置时才这样做(对于ResourceVersion != "0")。
			// pager中默认设置为500
			pager.PageSize = 0
		}

        // ⭐: 真正发起请求,调用了pager中的list函数,
		list, paginatedResult, err = pager.List(context.Background(), options)

		// 判断是否`资源过期错误` 或者 `资源过大错误`
		if isExpiredError(err) || isTooLargeResourceVersionError(err) {
			// 设置同步资源标志为不可用
			r.setIsLastSyncResourceVersionUnavailable(true)
			// 如果用于列出的资源版本不可用请立即重试。如果分页列表调用由于延续页上的“Expired”错误而失败,
			// 或者缓存可能尚未与提供的资源版本同步。
			// 因此,我们需要返回到resourceVersion=""获取最新版本资源来恢复。
			// 重新发起一次请求
			list, paginatedResult, err = pager.List(context.Background(), metav1.ListOptions{ResourceVersion: r.relistResourceVersion()})
		}
		close(listCh)
	}()

	// 检测信号
	// 1.外部停止信号
	// 2.panic错误
	// 3.list获取结束
	select {
	case <-stopCh:
		return nil
	case r := <-panicCh:
		panic(r)
	case <-listCh:
	}

然后我们继续看下半部分。下半部分相对简单一些,核心就是将上半部分获取的list数据进行提取元数据,获取resourceVersion(供后续watch使用),提取资源list,调用Refelctor的syncWith(),将其同步到store的实现类中。


	// 上半部分的err不为空直接返回
	if err != nil {
		klog.Warningf("%s: failed to list %v: %v", r.name, r.typeDescription, err)
		return fmt.Errorf("failed to list %v: %w", r.typeDescription, err)
	}
	
	...

	// 设置成可用可获取
	r.setIsLastSyncResourceVersionUnavailable(false) // list was successful

	// 获取整个list的元数据
	listMetaInterface, err := meta.ListAccessor(list)

	if err != nil {
		return fmt.Errorf("unable to understand list result %#v: %v", list, err)
	}

	// 获取list的 资源版本(resourceVersion)
	resourceVersion = listMetaInterface.GetResourceVersion()

	// 提取列表item项
	items, err := meta.ExtractList(list)
	if err != nil {
		return fmt.Errorf("unable to understand list result %#v (%v)", list, err)
	}

	// 这一步是进行同步功能 同步到store的实现中
	// 同步时,会将items项和resourceVersion同步传入
	// 第一次全量同步就会进行信息的同步 如果开启了 resync 则会进行持续性同步
	if err := r.syncWith(items, resourceVersion); err != nil {
		return fmt.Errorf("unable to sync list result: %v", err)
	}

	// 设置上一次获取之后的资源版本 `revision`/`resourceVersion`
	r.setLastSyncResourceVersion(resourceVersion)

	return nil

3.2 Watch

继续来看一下watch函数中,主要逻辑就是通过resourceVersion来调用watch函数,拿到w之后传入watchHandler,在watchHandler中详细处理后续监听到的event事件。

// watch simply starts a watch request with the server.
// Watch只是向服务器启动一个监视请求。
func (r *Reflector) watch(w watch.Interface, stopCh <-chan struct{}, resyncerrc chan error) error {
	var err error
	... 
	for {
		// 给stopCh一个停止循环的机会,即使在continue语句出错的情况下也是如此
		select {
		// 该stop由Run方法执行
		// 我们可以理解成 如果Run方法需要结束 则 watch也同步进行结束
		case <-stopCh:
			return nil
		default:
		}

		// w == nil 证明是使用普通的block方式获取list
		if w == nil {
			// 设置随机时间
			timeoutSeconds := int64(minWatchTimeout.Seconds() * (rand.Float64() + 1.0))

			// 3大特征
			// resourceVersion/TimeoutSeconds/Bookmarks
			options := metav1.ListOptions{
				// 这里获取的是上一个list请求中的resourceVersion版本
				ResourceVersion: r.LastSyncResourceVersion(),

				// 我们要避免挂起watch的情况。停止在`超时窗口`内未接收任何事件的任何监视程序。
				// 通过使用上述设置的随机时间,避免在该时间内未接受到信息 导致watch空悬
				TimeoutSeconds: &timeoutSeconds,
                
				// 为了在watch重启时减少kube-apisserver上的负载,
				// 您可以启用`watch书签`。Reflector根本不假设返回书签(如果服务器不支持watch书签,它将忽略此字段)。
				// 默认开启书签
				AllowWatchBookmarks: true,
			}

			// 调用watch函数 发起watch操作
			w, err = r.listerWatcher.Watch(options)

			if err != nil {
				// 判断是否是watch错误`err`重试
				if canRetry := isWatchErrorRetriable(err); canRetry {
					klog.V(4).Infof("%s: watch of %v returned %v - backing off", r.name, r.typeDescription, err)
					// 判断stop或进行回退
					select {
					case <-stopCh:
						return nil
					case <-r.initConnBackoffManager.Backoff().C():
						continue
					}
				}
				return err
			}
		}

        // ⭐: 核心watch函数 
		err = watchHandler(start, w, r.store, r.expectedType, r.expectedGVK,
			r.name, r.typeDescription, r.setLastSyncResourceVersion, nil,
			r.clock, resyncerrc, stopCh)

		// Ensure that watch will not be reused across iterations.
		// 确保watch不会跨迭代重用。
		// 停止stop
		// watchHandle发生错误 或者stopCh接受到停止的信号
		w.Stop()
		// 将w置为nil
		w = nil

		// 接下来就是进行错误判断
		.......
	}
}

3.3 watchHandler

此函数是watch的核心逻辑函数,可以看到,传入的字段都特别多。以下只对核心的流程进行摘取,如有兴趣,可以去看一下详细的源码流程。该函数大体逻辑是: 主要是通过传入的w,从w的resultCh中获取资源,然后对其资源进行匹配,看看该资源与expectedType,expectedType是否相符合。然后通过判断该event的事件(Add/Modified/Delete),将调用对应的store实现方法。随后设置resourceVersion。

// watchHandler watches w and sets setLastSyncResourceVersion
// watchHandler 监视w并设置setLastSyncResourceVersion
// *核心方法3*
// 用于获取事件并传入我们自定义的store实现中
func watchHandler(
	start time.Time,
	w watch.Interface,
	store Store,
	expectedType reflect.Type,
	expectedType *schema.GroupVersionKind,
	name string,
	expectedTypeName string,
	setLastSyncResourceVersion func(string),
	exitOnInitialEventsEndBookmark *bool,
	clock clock.Clock,
	errc chan error,
	stopCh <-chan struct{},
) error {

	// eventCount 记录事件数量
	eventCount := 0

	// 核心逻辑流程
loop:
	for {
		// 会卡在这里等待信号进入 并不会无限循环下去
		select {
		// 接受到停止信号 返回停止err `errors.New("stop requested")`
		case <-stopCh:
			return errorStopRequested
		// 接受到resyncerrc,停止返回`resyncerrc`
		case err := <-errc:
			return err
		// 从watch的通道中接受Result事件`event`
		case event, ok := <-w.ResultChan():
			if !ok {
				// 跳转loop 重新循环接受
				break loop
			}

			// watch的发生错误 将对象包装后返回
			if event.Type == watch.Error {
				return apierrors.FromObject(event.Object)
			}

			// 以下分支从几个方面进行判断

			// 判断对象类型
			if expectedType != nil {
				// 并不是期望`expectType`
				// 既 传入的type于watch的type不符
				// 并且 获取type可通过反射进行获取
				if e, a := expectedType, reflect.TypeOf(event.Object); e != a {
					utilruntime.HandleError(fmt.Errorf("%s: expected type %v, but watch event object had type %v", name, e, a))
					continue
				}
			}

			// 判断gvk
			if expectedGVK != nil {
				// 判断GVK是否相等
				if e, a := *expectedGVK, event.Object.GetObjectKind().GroupVersionKind(); e != a {
					utilruntime.HandleError(fmt.Errorf("%s: expected gvk %v, but watch event object had gvk %v", name, e, a))
					continue
				}
			}

			// 提取metadata数据
			meta, err := meta.Accessor(event.Object)
			if err != nil {
				utilruntime.HandleError(fmt.Errorf("%s: unable to understand watch event %#v", name, event))
				continue
			}

			// 获取该对象的 resourceVersion
			resourceVersion := meta.GetResourceVersion()

			// 开始根据eventType进行分发信息
			switch event.Type {
			// ADD 
			case watch.Added:
				err := store.Add(event.Object)
				...
                
			// Modified 
			case watch.Modified:
				err := store.Update(event.Object)
				...
                
			// Deleted
			case watch.Deleted:
				err := store.Delete(event.Object)
				...
            
            // Bookmark
			case watch.Bookmark:
				// A `Bookmark` means watch has synced here, just update the resourceVersion
				// 一个“Bookmark”意味着watch已经同步在这里,只需更新资源版本
				// 携带书签
				if _, ok := meta.GetAnnotations()["k8s.io/initial-events-end"]; ok {
					if exitOnInitialEventsEndBookmark != nil {
						*exitOnInitialEventsEndBookmark = true
					}
				}
                
			// 不属于任何类型 报错
			default:
				utilruntime.HandleError(fmt.Errorf("%s: unable to understand watch event %#v", name, event))
			}

			// 重新设置 `resourceVersion`
			// 将新的 `resourceVersion` 重新设置进reflector中
			setLastSyncResourceVersion(resourceVersion)

			// 调用`store`UpdateResourceVersion 将新的resourceVersion设置进去
			if rvu, ok := store.(ResourceVersionUpdater); ok {
				rvu.UpdateResourceVersion(resourceVersion)
			}

			// eventCount计数器+1
			eventCount++

			// 判断书签
			if exitOnInitialEventsEndBookmark != nil && *exitOnInitialEventsEndBookmark {
				watchDuration := clock.Since(start)
				klog.V(4).Infof("exiting %v Watch because received the bookmark that marks the end of initial events stream, total %v items received in %v", name, eventCount, watchDuration)
				return nil
			}
		}
	}

	watchDuration := clock.Since(start)

	// 如果watch耗时小于1s 和 eventCount事件计数 == 0
	// 则进行报错
	if watchDuration < 1*time.Second && eventCount == 0 {
		return fmt.Errorf("very short watch: %s: Unexpected watch close - watch lasted less than a second and no items received", name)
	}
	klog.V(4).Infof("%s: Watch close - %v total %v items received", name, expectedTypeName, eventCount)
	return nil
}

3.4 小总结

至此,我们通过Reflector核心方法大概了解了其如何获取k8s资源。首先通过将List()函数传入分页器中,通过分页器封装进行调用,获取到对应资源的list,随后将其同步到store中完成全量更新。然后调用watch,通过循环,从watch的resultCh中不断监听进行获取指定资源,经过资源类型判断后再根据事件类型调用对应的store方法,实现增量更新。

4.Other

4.1 Pager

在List流程中,我们看到其使用了Pager封装list。我们可以看一下pager的源码,看看其是如何封装我们外部传入的ListWatcher。

// ⭐: ListPageFunc 返回给定list options的list Obj
// 传入ctx,options 获取的runtime.Object为list obj对象
type ListPageFunc func(ctx context.Context, opts metav1.ListOptions) (runtime.Object, error)

// SimplePageFunc 将一个无上下文的列表函数改编为一个接受上下文的列表函数。
func SimplePageFunc(fn func(opts metav1.ListOptions) (runtime.Object, error)) ListPageFunc {
	return func(ctx context.Context, opts metav1.ListOptions) (runtime.Object, error) {
		return fn(opts)
	}
}

// ListPager帮助客户端代码将 `大型列表查询` 分解成多个PageSize或更小的块。
// ⭐: PageFn应该接受metav1.ListOptions。ListOptions支持`分页`并返回一个列表。
// ⭐: 分页器不会更改初始选项列表上的字段或标签选择器。
type ListPager struct {
	// 块大小
	PageSize int64

	// 分页Fn函数
	PageFn ListPageFunc
	
    // 如果资源超时,是否获取最新全部list(注: 这里是默认开启的)
	FullListIfExpired bool

	// Number of pages to buffer
	// 页面缓冲的大小
	PageBufferSize int32
}

我们通过看到其New()函数,可以看到默认已经配置了对应pageSize和FullListIfExpired。我们只需传入SimplePageFunc封装实际list后的函数即可。

func New(fn ListPageFunc) *ListPager {
	return &ListPager{
		PageSize: defaultPageSize,
		PageFn:   fn,
		// 如果超时,默认获取全部的资源List
		FullListIfExpired: true,
		PageBufferSize:    defaultPageBufferSize,
	}
}

再来看看pager中的List。我们在Reflector中传入了ListWatch的list实现,在此函数中,通过一个循环持续的使用分块获取的方式进行获取资源。如果发生资源超时错误,则直接获取资源的全部list。如果一次获取不完,则通过设置continue token重新进行循环获取。如果一次能获取完,则直接可以返回。

// List返回单个列表对象,但会尝试从服务器`检索较小的块`,以减少对服务器的影响。
// 如果块尝试失败,它将加载完整列表。选项中的限制字段,如果未设置,将默认为页面大小。
func (p *ListPager) List(ctx context.Context, options metav1.ListOptions) (runtime.Object, bool, error) {
	if options.Limit == 0 {
		// 如果没有设置则用默认的500 避免导致一次获取完整list导致消耗大量服务器性能
		options.Limit = p.PageSize
	}

	// 第一次是不会设置 resourceVersion 默认获取最新的
	requestedResourceVersion := options.ResourceVersion
	// 设置resourceVersion Match规则
	requestedResourceVersionMatch := options.ResourceVersionMatch

	// 这里的list是为了装循环多次获取的list
	// 如果只是循环一次,则不需要用到
	var list *metainternalversion.List

	// 标志分页结果是否成功
	paginatedResult := false

	// 核心的loop
	for {

		// ctx超时控制
		select {
		case <-ctx.Done():
			return nil, paginatedResult, ctx.Err()
		default:
		}

        // ⭐: 执行传入的PageFn,实际的List请求
		obj, err := p.PageFn(ctx, options)

		if err != nil {
			// ⭐: 只有在返回“Expired”(资源版本过老)错误时才回退到完整列表,FullListIfExpired为true,
			// 并且“Expired”错误发生在第2页或之后(因为完整列表旨在防止分页)。
			// 当由第一个页面请求建立的资源版本在后续列表请求期间退出压缩时,列表不会失败)。

			if !errors.IsResourceExpired(err) || !p.FullListIfExpired || options.Continue == "" {
				// 如果不是资源过时错误
				return nil, paginatedResult, err
			}
            
			// list在我们处理资源过期时,在请求的ResourceVersion处退回到完整列表。
			// 这里是处理缓存资源过期错误 直接进行全部获取
			options.Limit = 0
			options.Continue = ""
			options.ResourceVersion = requestedResourceVersion
			options.ResourceVersionMatch = requestedResourceVersionMatch
			// 再次直接发起完整的list请求
			result, err := p.PageFn(ctx, options)
			// 获取数据后直接返回
			return result, paginatedResult, err
		}

		// 获取list元数据
		m, err := meta.ListAccessor(obj)
		if err != nil {
			return nil, paginatedResult, fmt.Errorf("returned object must be a list: %v", err)
		}

		// 如果没有处理任何页面,则提前退出并返回得到的对象
		// ⭐: 后续没有需要获取的了 则直接进行退出返回
		if len(m.GetContinue()) == 0 && list == nil {
			// 证明第一次进来就能获取完 可以直接提前退出
			return obj, paginatedResult, nil
		}

		// ⭐: 以下逻辑代表一次获取不完
		// initialize the list and fill its contents
		// 初始化列表并填充其内容
		if list == nil {
			list = &metainternalversion.List{Items: make([]runtime.Object, 0, options.Limit+1)}
			list.ResourceVersion = m.GetResourceVersion()
			list.SelfLink = m.GetSelfLink()
		}

		if err := meta.EachListItem(obj, func(obj runtime.Object) error {
			// ⭐: 将每个item追加到list中
			list.Items = append(list.Items, obj)
			return nil
		}); err != nil {
			return nil, paginatedResult, err
		}

		// if we have no more items, return the list
		// 此时再进行判断 如果没有更多的项,则返回列表 (证明可能已经走到第二次了)
		if len(m.GetContinue()) == 0 {
			return list, paginatedResult, nil
		}

		// ⭐: 这里证明还有下一个continue 还可以继续进行获取
		// 设置下一个循环
		// 将continue设置给下一次进行获取
		// set the next loop up
		options.Continue = m.GetContinue()

		// 根据continue即可 不需要再指定version和version match
		// ⭐原因: 在使用continue时不允许指定资源版本
		options.ResourceVersion = ""
		options.ResourceVersionMatch = ""
		// At this point, result is already paginated.
		// 此时,结果已经分页。
		paginatedResult = true
	}
}

4.2 小试牛刀

根据以上的学习,相信大家对Reflector执行流程有一个大概了解。这节我们尝试本地模拟测试。

这里根据NewReflector实验对应的接口

func NewReflector(lw ListerWatcher, expectedType interface{}, store Store, resyncPeriod time.Duration) *Reflector {
	// 实际底层还是调用NewReflectorWithOptions 只是简略版...
	return NewReflectorWithOptions(lw, expectedType, store, ReflectorOptions{ResyncPeriod: resyncPeriod})
}

实现如下:

// ⭐: 实现ListerWatcher
type mockListWatch struct {
}

// ⭐: 实现watch中的watch.Interface
type mockWatchInterface struct {
}

// ⭐: 实现store
type mockStore struct {
}

func (m *mockWatchInterface) Stop() {

}

func (m *mockWatchInterface) ResultChan() <-chan watch.Event {
	return eventCh
}

func (m *mockStore) Add(obj interface{}) error {
	pod := obj.(*corev1.Pod)
	fmt.Printf("nowTime: %v Watch podName: %+v\n", time.Now().Format(time.DateTime), pod.Name)
	return nil
}

func (m *mockStore) Update(obj interface{}) error {
	return nil
}

func (m *mockStore) Delete(obj interface{}) error {
	return nil
}

func (m *mockStore) List() []interface{} {
	return nil
}

func (m *mockStore) ListKeys() []string {
	return nil
}

func (m *mockStore) Get(obj interface{}) (item interface{}, exists bool, err error) {
	return nil, false, nil
}

func (m *mockStore) GetByKey(key string) (item interface{}, exists bool, err error) {
	return nil, false, nil
}

func (m *mockStore) Resync() error {
	return nil
}

// 我们实现store即可
func (m *mockStore) Replace(i []interface{}, s string) error {
	println("NowTime: ", time.Now().Format(time.DateTime))
	for _, item := range i {
		pod := item.(*corev1.Pod)
		fmt.Printf("Replace PodName: %+v\n", pod.Name)
	}
	return nil
}

func (m *mockListWatch) List(options metav1.ListOptions) (runtime.Object, error) {
	return podList, nil
}

func (m *mockListWatch) Watch(options metav1.ListOptions) (watch.Interface, error) {
	return &mockWatchInterface{}, nil
}

我们准备一个有3个pod的podList(模拟全量更新),1个pod(模拟watch增量更新),eventCh用于发送对应的Pod。

var (
	pod1 = &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "one", Labels: map[string]string{"CI.io": "dev"}}, Spec: corev1.PodSpec{NodeName: "node-1"}}
	pod2 = &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "two", Labels: map[string]string{"CI.io": "dev"}}}
	pod3 = &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "tre", Labels: map[string]string{"CI.io": "prod"}}}
	pod4 = &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "four", Annotations: map[string]string{"abc": "edf"}}}

	podList = &corev1.PodList{
		TypeMeta: metav1.TypeMeta{
			Kind:       "Pod",
			APIVersion: "v1",
		},
		ListMeta: metav1.ListMeta{
			ResourceVersion: "123456",
		},
		Items: []corev1.Pod{*pod1, *pod2, *pod3},
	}

	eventCh = make(chan watch.Event)
)

入口代码:

func TestMockReflector(t *testing.T) {
	mlw := &mockListWatch{}
	store := &mockStore{}
	reflector := NewReflector(mlw, &corev1.Pod{}, store, time.Duration(0))

	go func() {
		reflector.Run(wait.NeverStop)
	}()
    
    // 模拟3s后新增新的pod
	go func() {
		// after sleep 3s send new pod
		time.Sleep(time.Second * 3)
		eventCh <- watch.Event{
			Type:   watch.Added,
			Object: pod4,
		}
	}()

	time.Sleep(time.Minute)
}

执行输出:

//NowTime:  2023-05-27 16:54:06  Replace:
//PodName: one
//PodName: two
//PodName: tre
//nowTime: 2023-05-27 16:54:09 Watch podName: four

4.3 FullCode

package cache

import (
	"fmt"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/util/wait"
	"k8s.io/apimachinery/pkg/watch"
	"testing"
	"time"
)

var (
	pod1 = &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "one", Labels: map[string]string{"CI.io": "dev"}}, Spec: corev1.PodSpec{NodeName: "node-1"}}
	pod2 = &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "two", Labels: map[string]string{"CI.io": "dev"}}}
	pod3 = &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "tre", Labels: map[string]string{"CI.io": "prod"}}}
	pod4 = &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "four", Annotations: map[string]string{"abc": "edf"}}}

	podList = &corev1.PodList{
		TypeMeta: metav1.TypeMeta{
			Kind:       "Pod",
			APIVersion: "v1",
		},
		ListMeta: metav1.ListMeta{
			ResourceVersion: "123456",
		},
		Items: []corev1.Pod{*pod1, *pod2, *pod3},
	}

	eventCh = make(chan watch.Event)
)

type mockListWatch struct {
}

type mockWatchInterface struct {
}

func (m *mockWatchInterface) Stop() {

}

func (m *mockWatchInterface) ResultChan() <-chan watch.Event {
	return eventCh
}

type mockStore struct {
}

func (m *mockStore) Add(obj interface{}) error {
	pod := obj.(*corev1.Pod)
	fmt.Printf("nowTime: %v Watch podName: %+v\n", time.Now().Format(time.DateTime), pod.Name)
	return nil
}

func (m *mockStore) Update(obj interface{}) error {
	return nil
}

func (m *mockStore) Delete(obj interface{}) error {
	return nil
}

func (m *mockStore) List() []interface{} {
	return nil
}

func (m *mockStore) ListKeys() []string {
	return nil
}

func (m *mockStore) Get(obj interface{}) (item interface{}, exists bool, err error) {
	return nil, false, nil
}

func (m *mockStore) GetByKey(key string) (item interface{}, exists bool, err error) {
	return nil, false, nil
}

func (m *mockStore) Resync() error {
	return nil
}

// 我们实现store即可
func (m *mockStore) Replace(i []interface{}, s string) error {
	println("NowTime: ", time.Now().Format(time.DateTime))
	for _, item := range i {
		pod := item.(*corev1.Pod)
		fmt.Printf("Replace PodName: %+v\n", pod.Name)
	}
	return nil
}

func (m *mockListWatch) List(options metav1.ListOptions) (runtime.Object, error) {
	return podList, nil
}

func (m *mockListWatch) Watch(options metav1.ListOptions) (watch.Interface, error) {
	return &mockWatchInterface{}, nil
}

func TestMockReflector(t *testing.T) {
	mlw := &mockListWatch{}
	store := &mockStore{}
	reflector := NewReflector(mlw, &corev1.Pod{}, store, time.Duration(0))

	go func() {
		reflector.Run(wait.NeverStop)
	}()
	go func() {
		// after sleep 3s send new pod
		time.Sleep(time.Second * 3)
		eventCh <- watch.Event{
			Type:   watch.Added,
			Object: pod4,
		}
	}()
	time.Sleep(time.Minute)
}

写在最后

本文讲解了Reflector内部逻辑以及基本功能,下一篇将对store进行分析,探究一下store是如何存储Reflector获取的资源?

参考

ShadowYD: [K8S] client-go 的正确打开方式

kubernetesAPI