Bootstrap

k8s 源码分析 informer篇

之前介绍过informer的流程,文章在 informer介绍。今天梳理一下他的源码和流程。

请添加图片描述

一、概念

什么是 Informer
informer 是 client-go 中的核心工具包,informer 其实就是一个带有本地缓存和索引机制的,可以注册 EventHandler 的 client 本地缓存被称为 Store,索引被称为 Index 。使用 informer 的目的是为了减轻 apiserver 数据交互的压力而抽象出来的一个 cache 层,客户端对 apiserver 数据的 读取和监听操作都通过本地 informer进行。

为什么需要 Informer 机制

Kubernetes 各个组件都是通过 REST API 跟 API Server 交互通信的,而如果每次每一个组件都直接跟 API Server 交互去读取/写入到后端的etcd的话,会对 API Server 以及etcd造成非常大的负担。 而 Informer 机制是为了保证各个组件之间通信的实时性、可靠性,并且减缓对 API Server和etcd 的负担。

informer和configch(newSourceApiserverFromLW)区别
之前介绍过一篇configch的文章 configch源码。这篇文章主要是介绍newSourceApiserverFromLW方法,与kubelet交互后进行更新删除等。并且newSourceApiserverFromLW只监听一个api对象(pod)
而informer则监听整个namespace下的api对象。但是informer不会做出动作,只是监听后维护自己的缓存。方便用户去使用最新数据,避免每次都http访问api-server浪费资源

二、注册及接收

大概步骤如下

  • 启动informer
  • 注册processLoop和reflector
  • reflector开始LIST&WATCH。同时processLoop开始循环pop队列数据(步骤三)
  • watch到的数据进行对比处理,不存在的存入的queue队列中

注册
2.1.start注册
代码位置:vendor/k8s.io/client-go/informer/factory.go

func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) {
	f.lock.Lock()
	defer f.lock.Unlock()
	if f.shuttingDown {
		return
	}
	for informerType, informer := range f.informers {
		if !f.startedInformers[informerType] {
			f.wg.Add(1)
			informer := informer
			go func() {
				defer f.wg.Done()
				//启动informer
				informer.Run(stopCh)
			}()
			f.startedInformers[informerType] = true
		}
	}
}

2.2 注册processLoop和reflector
代码位置:vendor/k8s.io/client-go/tools/cache/controller.go

//完成一些初始化,总要的是下面两个函数
    //启动监听
    wg.StartWithChannel(stopCh, r.Run)
    //这里是循环delta FIFO的queue,然后pop出来最新的
	wait.Until(c.processLoop, time.Second, stopCh)

2.3 reflector开始LIST&WATCH
代码位置:vendor/k8s.io/client-go/tools/cache/reflector.go
这里开始LIST&WATCH

	wait.BackoffUntil(func() {
		if err := r.ListAndWatch(stopCh); err != nil {
			r.watchErrorHandler(r, err)
		}
	}, r.backoffManager, true, stopCh)
   ....省略
   //开始watch
   return r.watch(w, stopCh, resyncerrc)
   ...省略
   //watch函数开始
   		err = watchHandler(start, w, r.store, r.expectedType, r.expectedGVK, r.name, r.typeDescription, r.setLastSyncResourceVersion, nil, r.clock, resyncerrc, stopCh)

2.4 watch处理数据
还是上面reflector文件,开始处理接收到的最新watch的信息

switch event.Type {
			case watch.Added:
				err := store.Add(event.Object)
				if err != nil {
					utilruntime.HandleError(fmt.Errorf("%s: unable to add watch event object (%#v) to store: %v", name, event.Object, err))
				}
			case watch.Modified:
				err := store.Update(event.Object)
				if err != nil {
					utilruntime.HandleError(fmt.Errorf("%s: unable to update watch event object (%#v) to store: %v", name, event.Object, err))
				}
			case watch.Deleted:
				err := store.Delete(event.Object)
				if err != nil {
					utilruntime.HandleError(fmt.Errorf("%s: unable to delete watch event object (%#v) from store: %v", name, event.Object, err))
				}
			case watch.Bookmark:
			
				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))
			}

代码位置:vendor/k8s.io/client-go/tools/cache/delta_fifo.go
进入到更新。这里通过queueActionLocked函数验证验证一下obj是否存在,不存在则推倒queue队列中

func (f *DeltaFIFO) Update(obj interface{}) error {
	f.lock.Lock()
	defer f.lock.Unlock()
	f.populated = true
	return f.queueActionLocked(Updated, obj)
}
...省略部分代码
//queueActionLocked中就是获取到item映射出来的这个obj信息(items中存的数据和queue数据是一个。等到pop的时候这两个数据都会删掉),如果不存在则推送到queue中,如果存在则替换。
func (f *DeltaFIFO) queueActionLocked(actionType DeltaType, obj interface{}) error {
	id, err := f.KeyOf(obj)
	if err != nil {
		return KeyError{obj, err}
	}
	//根据id查到映射的api对象,然后推倒newDeltas中。如果是新的,则不进行对比,如果是老的(2个deltas)则进行去重。dedupDeltas里的逻辑就是如果是删除的话只保留一个,如果是添加则直接返回
	oldDeltas := f.items[id]
	newDeltas := append(oldDeltas, Delta{actionType, obj})
	newDeltas = dedupDeltas(newDeltas)
	//这里百分百会大于0,不会小于0
	if len(newDeltas) > 0 {
		if _, exists := f.items[id]; !exists {
			f.queue = append(f.queue, id)
		}
		f.items[id] = newDeltas
		//让wait的cond开始工作
		f.cond.Broadcast()
	} else {
		if oldDeltas == nil {
			klog.Errorf("Impossible dedupDeltas for id=%q: oldDeltas=%#+v, obj=%#+v; ignoring", id, oldDeltas, obj)
			return nil
		}
		klog.Errorf("Impossible dedupDeltas for id=%q: oldDeltas=%#+v, obj=%#+v; breaking invariant by storing empty Deltas", id, oldDeltas, obj)
		f.items[id] = newDeltas
		return fmt.Errorf("Impossible dedupDeltas for id=%q: oldDeltas=%#+v, obj=%#+v; broke DeltaFIFO invariant by storing empty Deltas", id, oldDeltas, obj)
	}
	return nil

到这里,接收的工作就完成了,已经将最新的数据推送到了queue中,下面就是开始处理这个queue了

三、弹出队列处理

  • 开启queue队列的循环接收
  • 获取到最新队列信息开始处理
  • 更新本地indexer缓存
  • 分发给监听informer的用户去触发回调函数
  • 如果用户定义的回调能够返回ErrRequeue类型错误,则会把这个资源信息重新推入到队列然后重新发送给用户

3.1 开始循环接收数据

func (c *controller) processLoop() {
	for {
	    //无限循环处理queue数据
		obj, err := c.config.Queue.Pop(PopProcessFunc(c.config.Process))
		}
}

3.2 这里面也是个for循环,如果队列大于0,则读取出来通过process处理,否则等待队列有新数据加入
代码位置 vendor/k8s.io/client-go/tools/cache/delta_fifo.go

for {
		for len(f.queue) == 0 {
			if f.closed {
				return nil, ErrFIFOClosed
			}
			f.cond.Wait()
		}
		id := f.queue[0]
		f.queue = f.queue[1:]
		depth := len(f.queue)
		//减少队列长度,四、replace中会介绍这个。这个等于0代表没有需要同步的了
		if f.initialPopulationCount > 0 {
			f.initialPopulationCount--
		}
		item, ok := f.items[id]
		delete(f.items, id)
		//进入这里
		err := process(item, isInInitialList)
		//如果用户定义的回调能够返回ErrRequeue类型错误,则会把这个资源信息重新推入到队列然后重新发送给用户
		if e, ok := err.(ErrRequeue); ok {
			f.addIfNotPresent(id, item)
			err = e.Err
		}
}

3.3 注册的回调。判断映射是否存在,存在的话则进行处理
代码位置 vendor/k8s.io/client-go/tools/cache/shared_informer.go

func (s *sharedIndexInformer) HandleDeltas(obj interface{}, isInInitialList bool) error {
	s.blockDeltas.Lock()
	defer s.blockDeltas.Unlock()

	if deltas, ok := obj.(Deltas); ok {
	    //如果映射存在,则进行处理
		return processDeltas(s, s.indexer, deltas, isInInitialList)
	}
	return errors.New("object given as Process argument is not Deltas")
}

3.4 处理本地缓存和回调
代码位置:vendor/k8s.io/client-go/tools/cache/controller.go

func processDeltas(
	handler ResourceEventHandler,
	clientState Store,
	deltas Deltas,
	isInInitialList bool,
) error {
	for _, d := range deltas {
		obj := d.Object

		switch d.Type {
		 //如果是添加、同步、替换、更新,则进入这里
		case Sync, Replaced, Added, Updated:
			if old, exists, err := clientState.Get(obj); err == nil && exists {
			    //更新indexer的本地缓存(步骤3.5)
				if err := clientState.Update(obj); err != nil {
					return err
				}
				//处理回调(步骤3.6)
				handler.OnUpdate(old, obj)
			} else {
				if err := clientState.Add(obj); err != nil {
					return err
				}
				handler.OnAdd(obj, isInInitialList)
			}
		case Deleted:
			if err := clientState.Delete(obj); err != nil {
				return err
			}
			handler.OnDelete(obj)
		}
	}
	return nil
}

3.5 更新本地indexer(Storage)缓存
代码位置:vendor/k8s.io/client-go/tools/cache/controller.go

func (c *cache) Update(obj interface{}) error {
	key, err := c.keyFunc(obj)
	if err != nil {
		return KeyError{obj, err}
	}
	c.cacheStorage.Update(key, obj)
	return nil
}

3.6处理回调

func (s *sharedIndexInformer) OnUpdate(old, new interface{}) {
	isSync := false
	if accessor, err := meta.Accessor(new); err == nil {
		if oldAccessor, err := meta.Accessor(old); err == nil {
			// Events that didn't change resourceVersion are treated as resync events
			// and only propagated to listeners that requested resync
			isSync = accessor.GetResourceVersion() == oldAccessor.GetResourceVersion()
		}
	}
	s.cacheMutationDetector.AddObject(new)
	//这个是重点,分发给监听者
	s.processor.distribute(updateNotification{oldObj: old, newObj: new}, isSync)
}

3.7 分发给监听者
这个图介绍了监听者如何处理队列顺序(3.8步骤讲解)
在这里插入图片描述

  • 把消息分发给监听者
  • 接收消息推送到add管道
func (p *sharedProcessor) distribute(obj interface{}, sync bool) {
	p.listenersLock.RLock()
	defer p.listenersLock.RUnlock()
    //p.listeners代表有几个监听者。这里循环所有监听者,把最新信息发送给监听者。
	for listener, isSyncing := range p.listeners {
		switch {
		case !sync:
			listener.add(obj)
		case isSyncing:		
			listener.add(obj)
		default:
			
		}
	}
}

func (p *processorListener) add(notification interface{}) {
	if a, ok := notification.(addNotification); ok && a.isInInitialList {
		p.syncTracker.Start()
	}
	p.addCh <- notification //推送到add管道
}

3.8监听者的处理 (最上面图中5-6中间的处理)

  • 为每个监听者创建run和pop函数
  • run函数负责循环nextCh管道,如果有数据就触发handler
  • pop函数分别case1和case2
  • case1负责监听nextCh是否阻塞(阻塞说明有数据处理在推送)如果没阻塞,拿到等待推送的值,如果没有值置空nextCh变量
  • case2负责接收addCh管道的值,如果有数据,发现 notification为空(case1 是关闭的),重新启动case1、否则写入到阻塞队列中
func (p *sharedProcessor) run(stopCh <-chan struct{}) {
	func() {
		p.listenersLock.RLock()
		defer p.listenersLock.RUnlock()
		for listener := range p.listeners {
			p.wg.Start(listener.run)//每个监听者创建一个run
			p.wg.Start(listener.pop)//每个监听者创建一个pop
		}
		p.listenersStarted = true
	}()
}

func (p *processorListener) run() {
	stopCh := make(chan struct{})
	wait.Until(func() {
		for next := range p.nextCh {
			switch notification := next.(type) {
			case updateNotification://触发更新
				p.handler.OnUpdate(notification.oldObj, notification.newObj)
			case addNotification:
				p.handler.OnAdd(notification.newObj, notification.isInInitialList)
				if notification.isInInitialList {
					p.syncTracker.Finished()
				}
			case deleteNotification:
				p.handler.OnDelete(notification.oldObj)
			default:
				utilruntime.HandleError(fmt.Errorf("unrecognized notification: %T", next))
			}
		}
		close(stopCh)
	}, 1*time.Second, stopCh)
}


func (p *processorListener) pop() {
	defer utilruntime.HandleCrash()
	defer close(p.nextCh) // Tell .run() to stop

	var nextCh chan<- interface{}
	var notification interface{}
	for {
		select {
		case nextCh <- notification://如果nextCh没阻塞中,则进行推送
			var ok bool
			//从队列读取一个值,下次推送到nextCh中
			notification, ok = p.pendingNotifications.ReadOne()
			if !ok { 
			//如果没值了,取消nextCh变量
				nextCh = nil 
			}
			//从addCh管道拿到最新值
		case notificationToAdd, ok := <-p.addCh:
			if !ok {
				return
			}
			if notification == nil { 
				//如果没有等待的,重新定义nextCh变量进行推送
				notification = notificationToAdd
				nextCh = p.nextCh
			} else { 
				//如果nextCh阻塞中,则写入到队列等待
				p.pendingNotifications.WriteOne(notificationToAdd)
			}
		}
	}
}

到这里就处理结束了。如果想要使用监听者,需要用户自己建立informer进行监听。然后添加注册函数,就可以接收了

四、replace和resync

replace是用来操作来更新缓存中的对象,以确保对象完全更新,而不仅仅是部分更新。在第一次运行时会启动
resync是每隔 10 分钟进行一次重新同步操作,即调用 List 方法从 API Server 中拉取完整的资源列表。

4.1 replace

  • 先生成一个set集合用来存放数据
  • 遍历LIST传过来的所有list数据并生成key,同时添加到set集合中
  • 对队列中的对象做删除检测。这里是避免在与apiserver断开连接时错过了删除信息的意外而重新检测
  • 如果有本地缓存库,则进行与本地缓存的比对,如果本地缓存有数据,但是list没有,则删除这个数据
  • initialPopulationCount计算一下总量和有问题需要删除的总量(这里是为了以后判断是否sync成功)
func (f *DeltaFIFO) Replace(list []interface{}, _ string) error {
	f.lock.Lock()
	defer f.lock.Unlock()
	//先生成一个set集合用来存放数据
	keys := make(sets.String, len(list))
	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.queueActionLocked(action, item); err != nil {
			return fmt.Errorf("couldn't enqueue object: %v", err)
		}
	}
    //对队列中的对象做删除检测。这里是避免在与apiserver断开连接时错过了删除信息的意外而重新检测
	queuedDeletions := 0
	for k, oldItem := range f.items {
		if keys.Has(k) {
			continue
		}
		if n := oldItem.Newest(); n != nil {
			deletedObj = n.Object
			if d, ok := deletedObj.(DeletedFinalStateUnknown); ok {
				deletedObj = d.Obj
			}
		}
		queuedDeletions++
		if err := f.queueActionLocked(Deleted, DeletedFinalStateUnknown{k, deletedObj}); err != nil {
			return err
		}
	}
	if f.knownObjects != nil {
	    //如果有本地缓存库,则进行与本地缓存的比对,如果本地缓存有数据,但是list没有,则删除这个数据
		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
			}
		}
	}
	if !f.populated {
		f.populated = true
		f.initialPopulationCount = keys.Len() + queuedDeletions
	}
	return nil
}

4.2 resync
默认每10分钟同步一次。这个时间间隔可以通过 ResyncPeriodFunc 函数进行调整

  • 遍历所有数据
  • 如果这个数据在items映射里存在,则不管这个信息(因为如果items中存在,则代表是api-server发过来的最新的还没处理,这里需要跳过而不是覆盖)
  • 如果不是则同步这个数据。其实就是把本地缓存库的数据再次放入队列中,重新消费做一次update的动作
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 {
		klog.Errorf("Unexpected error %v during lookup of key %v, unable to queue object for sync", err, key)
		return nil
	} else if !exists {
		klog.Infof("Key %v does not exist in known objects store, unable to queue object for sync", key)
		return nil
	}
	//获得这个数据的id
	id, err := f.KeyOf(obj)
	if err != nil {
		return KeyError{obj, err}
	}
	//如果这个数据在items映射里存在,则不管这个信息(因为如果items中存在,则代表是api-server发过来的最新的还没处理,这里需要跳过而不是覆盖)
	if len(f.items[id]) > 0 {
		return nil
	}
	//如果不是则同步这个数据。其实就是把本地缓存库的数据再次放入队列中,重新消费做一次update的动作
	if err := f.queueActionLocked(Sync, obj); err != nil {
		return fmt.Errorf("couldn't queue object: %v", err)
	}
	return nil
}

对于重新把本地缓存库的数据再次放入队列中,重新消费做一次update的动作可能有些疑问,这里举个例子。
如果本地缓存中a=1,然后出发了用户的add操作。但是用户有一些一次性的特殊情况需要处理把a=1改成了a=2。等过了10分钟resync同步了,这时候就会把a=1再次推到用户的add操作,用户就可以继续使用a=1了
但是大部分情况是为了用户端的回调失败或未执行时,确保再次触发统一操作。

;