一文搞懂 Kubernetes shareInformer 通知机制的实现

176 阅读12分钟

我们在前文 一文搞懂 Kubernetes 的负载均衡 中可以看到监听节点变化的是 informer 机制,本文我们来看看informer机制是如何实现的。

informer监听configmap

我们先看看怎么使用 client-go informer 来监听configmap的变化

func ConfigWatcher(ctx context.Context, namespace, configMapName string, onChangeFunc ConfigMapOnChangeFunc) (cancel func()) {
	// 获取 api server的客户端
	clientSet, err := GetClient()
	if err != nil {
		panic(err)
	}

	// 实例化 configmap 的 informer
	listWatcher := k8sCache.NewListWatchFromClient(
		clientSet.CoreV1().RESTClient(),
		"configmaps",
		namespace,
		fields.Everything(),
	)

	// 实例化 informer
	informer := k8sCache.NewSharedInformer(
		listWatcher,
		&corev1.ConfigMap{},
		0, // No resync
	)

	// 监听处理方法
	_, err = informer.AddEventHandler(k8sCache.ResourceEventHandlerFuncs{
		// 更新的方法
		UpdateFunc: func(oldObj, newObj interface{}) {
			log.Info().Msg("ConfigMap updated")
			oldConfigMap, ok := oldObj.(*corev1.ConfigMap)
			newConfigMap, ok := newObj.(*corev1.ConfigMap)
			// 更新方法外部传入
			err = onChangeFunc(namespace, configMapName, oldConfigMap, newConfigMap, updateConfigMap)
		},
		// 处理删除的方法
		DeleteFunc: func(obj interface{}) {
			log.Info().Msg("ConfigMap deleted")
			// 更新方法由外部传入
			err = onChangeFunc(namespace, configMapName, nil, nil, deleteConfigMap)
		},
	})

	// 取消informer的方法返回给外面
	stopCh := make(chan struct{})
	cancel = func() {
		close(stopCh)
	}
	// 启动 informer
	go informer.Run(stopCh)
	return cancel
}

这里使用了 shareIndexInformer 来初始化,与 indexInformer 的区别是可以共享 reflector ,通过增加事件处理函数来处理资源变化。

informer的实现流程

整体实现流程

ShareInformer和Informer的区别

shareIndexInformer 对事件先进行缓存、转发和处理,而 informer 则是直接对事件进行处理。**

informer在processLoop中就直接调用变更处理方法对Event进行处理

informer 实例化出 controller 并运行

func (s *sharedIndexInformer) Run(stopCh <-chan struct{}) {

	func() {
		// 将锁写在函数范围内,函数执行结束立马释放锁,避免了死锁,编码的一个小技巧
		s.startedLock.Lock()
		defer s.startedLock.Unlock()

		// 初始化DeltaFifo
		fifo := NewDeltaFIFOWithOptions(DeltaFIFOOptions{})

		cfg := &Config{
			// 处理变更的方法
			Process:           s.HandleDeltas,
		}

		// 初始化 controller
		s.controller = New(cfg)
	}()

	// 处理监听的事件
	wg.StartWithChannel(processorStopCh, s.processor.run)
	// 启动 controller
	s.controller.Run(stopCh)
}

controller 通过 Reflector的Run方法来向api server拉取变更

func (c *controller) Run(stopCh <-chan struct{}) {
	// 初始化 Reflector
	r := NewReflectorWithOptions(
		c.config.ListerWatcher,
		c.config.ObjectType,
		c.config.Queue,
	)

	// 将Reflector 赋值给 controller
	c.reflectorMutex.Lock()
	c.reflector = r
	c.reflectorMutex.Unlock()

	var wg wait.Group
	// 运行reflector run 方法
	wg.StartWithChannel(stopCh, r.Run)
	// 每一秒循环执行 controller 的 processLoop
	wait.Until(c.processLoop, time.Second, stopCh)
	wg.Wait()
}

func (r *Reflector) Run(stopCh <-chan struct{}) {
	wait.BackoffUntil(func() {
		// 循环执行 ListAndWatch方法监听变化
		if err := r.ListAndWatch(stopCh); err != nil {
			//...
		}
	}, r.backoffManager, true, stopCh)
}

func (r *Reflector) ListAndWatch(stopCh <-chan struct{}) error {
	if r.UseWatchList {
		w, err = r.watchList(stopCh)
	}

	// 不是WatchList则走下面的list,拉取该资源下全量的内容
	if fallbackToList {
		err = r.list(stopCh)
	}
	return r.watch(w, stopCh, resyncerrc)
}

list获取对应资源的内容并不是每次都是从最开始获取,而是从relistResourceVersion 的位置开始获取,如果为空则全量拉取

func (r *Reflector) list(stopCh <-chan struct{}) error {
	// 设置重新拉取的version
	options := metav1.ListOptions{ResourceVersion: r.relistResourceVersion()}

	var list runtime.Object
	go func() {
		// 通过分页进行拉取
		pager := pager.New(pager.SimplePageFunc(func(opts metav1.ListOptions) (runtime.Object, error) {
			return r.listerWatcher.List(opts)
		}))
	}()
	// 获取最新版本的信息
	listMetaInterface, err := meta.ListAccessor(list)
	if err != nil {
		return fmt.Errorf("unable to understand list result %#v: %v", list, err)
	}
	resourceVersion = listMetaInterface.GetResourceVersion()
	// 把数据解析成items
	items, err := meta.ExtractListWithAlloc(list)

	//写入到 store中
	if err := r.syncWith(items, resourceVersion); err != nil {
		// ...
	}

	// 设置重新拉取资源的version
	r.setLastSyncResourceVersion(resourceVersion)
	return nil
}

// 同步items到 DeltaFIFO中
func (r *Reflector) syncWith(items []runtime.Object, resourceVersion string) error {
	found := make([]interface{}, 0, len(items))
	for _, item := range items {
		found = append(found, item)
	}
	return r.store.Replace(found, resourceVersion)
}

这里我们先暂时不管 DeltaFIFO的实现,后续会将,这里先专注整体流程,把它当作是一个存储即可

watchList 监听变化

func (r *Reflector) watch(w watch.Interface, stopCh <-chan struct{}, resyncerrc chan error) error {
	for {
		if w == nil {
			options := metav1.ListOptions{
				ResourceVersion: r.LastSyncResourceVersion(),
			}
			// 监听变更,把变更扔到watch.ResultChan中
			w, err = r.listerWatcher.Watch(options)
		}
		// 监听新增的事件Add到deltaFIFO中
		err = watchHandler(start, w, r.store, r.expectedType, r.expectedGVK, r.name, r.typeDescription, r.setLastSyncResourceVersion, nil, r.clock, resyncerrc, stopCh)
	}
}

通过 ListWatcher 客户端通过 HTTP chunked 方法接收API的数据并进行解码,传入到 ResultChan 中:

type StreamWatcher struct {
	result   chan Event
}

// NewStreamWatcher creates a StreamWatcher from the given decoder.
func NewStreamWatcher(d Decoder, r Reporter) *StreamWatcher {
	sw := &StreamWatcher{
		source:   d,
		result: make(chan Event),
	}
	go sw.receive()
	return sw
}

// 获取Chan中的数据
func (sw *StreamWatcher) ResultChan() <-chan Event {
	return sw.result
}

// 接收decode数据并传入到chan中
func (sw *StreamWatcher) receive() {
	for {
		action, obj, err := sw.source.Decode()
		case sw.result <- Event{
			Type:   action,
			Object: obj,
		}:
		}
	}
}

api server通过chunked分块传输,处理客户端请求

func (s *WatchServer) HandleHTTP(w http.ResponseWriter, req *http.Request) {
	w.Header().Set("Content-Type", s.MediaType)
	w.Header().Set("Transfer-Encoding", "chunked")
	w.WriteHeader(http.StatusOK)

	// 要监听的范围
	kind := s.Scope.Kind
	watchEncoder := newWatchEncoder(req.Context(), kind, s.EmbeddedEncoder, s.Encoder, framer)
	ch := s.Watching.ResultChan()

	for {
		select {
		case event, ok := <-ch:
			if !ok {
				return
			}
			// api server服务端对事件进行编码
			if err := watchEncoder.Encode(event); err != nil {
				//...
			}

			if len(ch) == 0 {
				flusher.Flush()
			}
		}
	}
}

通过 http chunked 来实现数据流监听

$ curl -i http://{kube-api-server-ip}:{kube-api-server-port}/api/v1/watch/pods
HTTP/1.1 200 OK
Content-Type: application/json
Transfer-Encoding: chunked
Date: Thu, 02 Jan 2020 20:22:59 GMT
Transfer-Encoding: chunked

{"type":"ADDED", "object":{"kind":"Pod","apiVersion":"v1",...}}
{"type":"ADDED", "object":{"kind":"Pod","apiVersion":"v1",...}}
{"type":"MODIFIED", "object":{"kind":"Pod","apiVersion":"v1",...}}

watchHandler 则将 ResultChan 的变更读取出来之后存储到 DeltasFIFO 中

func watchHandler(start time.Time,
	w watch.Interface,
	store Store,
	// 这里省略其他参数,我们先看watch如何将数据读取后放入到队列中
) error {

loop:
	for {
		select {
		// 读取 Chan里面变更的数据,通过事件类型不同调用不同方法加入到队列中
		case event, ok := <-w.ResultChan():
			switch event.Type {
			case watch.Added:
				err := store.Add(event.Object)
			case watch.Modified:
				err := store.Update(event.Object)
			case watch.Deleted:
				err := store.Delete(event.Object)
		}
	}
	return nil
}

到这里我们已经知道了客户端informer是如何从api-server获取变更

processLoop 同步变更

我们重新看一下informer的Run方法,里面同时启动了 processor 的方法

func (s *sharedIndexInformer) Run(stopCh <-chan struct{}) {
	func() {
		cfg := &Config{
			Process:           s.HandleDeltas,
		}
	}()

	processorStopCh := make(chan struct{})
	wg.StartWithChannel(processorStopCh, s.processor.run)
}

func (c *controller) processLoop() {
	for {
		// 从队列中获取需要消费的数据
		obj, err := c.config.Queue.Pop(PopProcessFunc(c.config.Process))
	}
}

// Process的值为HandleDeltas
func (s *sharedIndexInformer) HandleDeltas(obj interface{}, isInInitialList bool) error {
	if deltas, ok := obj.(Deltas); ok {
		return processDeltas(s, s.indexer, deltas, isInInitialList)
	}
}

// 处理变更的数据
func processDeltas(
	handler ResourceEventHandler,
	clientState Store,
	deltas Deltas,
	isInInitialList bool,
) error {
	for _, d := range deltas {
		switch d.Type {
		case Sync, Replaced, Added, Updated:
			if old, exists, err := clientState.Get(obj); err == nil && exists {
				handler.OnUpdate(old, obj)
			} else {
				handler.OnAdd(obj, isInInitialList)
			}
		case Deleted:
			handler.OnDelete(obj)
		}
	}
	return nil
}
// 这里是将变更的数据通过distribute分发到 addCh中
func (s *sharedIndexInformer) OnAdd(obj interface{}, isInInitialList bool) {  
	s.processor.distribute(addNotification{newObj: obj, isInInitialList: isInInitialList}, false)
}

distribute 将数据加入到 addCh 中

// distribute 把事件传递给所有listeners
func (p *sharedProcessor) distribute(obj interface{}, sync bool) {
	for listener, isSyncing := range p.listeners {
		switch {
		case isSyncing:
			// 同步到每一个监听器
			listener.add(obj)
		}
	}
}

func (p *processorListener) add(notification interface{}) {
	p.addCh <- notification
}

启动协程对数据进行获取和消费

func (p *sharedProcessor) run(stopCh <-chan struct{}) {
	func() {
		for listener := range p.listeners {
			// 获取到变更的数据并进行更新
			p.wg.Start(listener.run)
			p.wg.Start(listener.pop)
		}
		p.listenersStarted = true
	}()
	<-stopCh
}

pop将 addCh 里的数据写入到 nextCh 中

func (p *processorListener) pop() {
	var nextCh chan<- interface{}
	var notification interface{}
	for {
		select {
		case nextCh <- notification:
			var ok bool
			// 获取待消费的消息
			notification, ok = p.pendingNotifications.ReadOne()
		case notificationToAdd, ok := <-p.addCh:
			if notification == nil { 
				// 没有等待的消息,则直接写入到nextCh中
				notification = notificationToAdd
				nextCh = p.nextCh
			} else { 
				// 写入待消费的消息
				p.pendingNotifications.WriteOne(notificationToAdd)
			}
		}
	}
}

run方法消费 nextCh 里的数据

func (p *processorListener) run() {
	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)
			case deleteNotification:
				p.handler.OnDelete(notification.oldObj)
			}
		}
		close(stopCh)
	}, 1*time.Second, stopCh)
}

DeltaFIFO

从上面的代码可以看到使用了 store 的 Add 、 Replace 、 Update 、 Delete 的方法来存储变更, Pop 方法来获取Event进行处理 ,所以我们来分析这几个方法是怎么存储这些数据的。

首先要找到store的具体实现,store在最外层传入的是DeltaFIFO

func (s *sharedIndexInformer) Run(stopCh <-chan struct{}) {
	func() {
		fifo := NewDeltaFIFOWithOptions(DeltaFIFOOptions{
			KnownObjects:          s.indexer, // 这里的实现是cache.cache,后续我们会进行分析
			EmitDeltaTypeReplaced: true,
			Transformer:           s.transform,
		})

		cfg := &Config{
			// 构建Reflector的时候会传入 Queue 来作为 Store
			Queue:             fifo,
		}
	}()
}

所以接下来我们分析 DeltaFIFO 的具体实现,我们先通过一张图来了解整体的流程

queueActionLocked

给队列上锁后进行的动作,这里的 actionType 包含了如下:

type DeltaType string
const(
	Added   DeltaType = "Added"
	Updated DeltaType = "Updated"
	Deleted DeltaType = "Deleted"
	Replaced DeltaType = "Replaced"
	Sync DeltaType = "Sync"
)

实际的处理函数则会将变化加入到队列中,然后进行去重的动作,最后唤醒消费者:

func (f *DeltaFIFO) queueActionLocked(actionType DeltaType, obj interface{}) error {
	// 获取items的key
	id, err := f.KeyOf(obj)

	// 将新的deltas 加入到deltas中
	oldDeltas := f.items[id]
	newDeltas := append(oldDeltas, Delta{actionType, obj})
	// Deltas进行一个去重的操作,尾部如果都是delete的对象,则认为是重复的
	newDeltas = dedupDeltas(newDeltas)

	if len(newDeltas) > 0 {
		// 如果原先的key不存在,则加入到key的队列中
		if _, exists := f.items[id]; !exists {
			f.queue = append(f.queue, id)
		}
		// 将对象的deltas更新成最新的值
		f.items[id] = newDeltas
		// 如果有在等待Pop的协程则唤醒
		f.cond.Broadcast()
	}else{
		// 这个分支不会出现,出现了的话做日志记录
	}
	return nil
}

Add

上面我们了解了 queueActionLocked 后,Add 、Update和Delete的操作都比较简单,我们这里直接看一个Add的实现,Update和Delete不做赘述。

func (f *DeltaFIFO) Add(obj interface{}) error {
	f.lock.Lock()
	defer f.lock.Unlock()
	f.populated = true
	return f.queueActionLocked(Added, obj)
}

Replace

该方法主要的作用是将传入的list加入到detlasFIFO中,并且将不存在List中的key全部删除

func (f *DeltaFIFO) Replace(list []interface{}, _ string) error {
	keys := make(sets.String, len(list))

	// 这里的action为Replaced
	action := Sync
	if f.emitDeltaTypeReplaced {
		action = Replaced
	}

	// 将对象加入到items中
	for _, item := range list {
		// 用set保存这次加入对象的Key值,用于后面替换
		key, err := f.KeyOf(item)
		if err != nil {
			return KeyError{item, err}
		}
		keys.Insert(key)
		if err := f.queueActionLocked(action, item); err != nil {
			//...
		}
	}

	// 删除不是本次添加的Deltas
	queuedDeletions := 0
	for k, oldItem := range f.items {
		// 通过key判断对象是否存在
		if keys.Has(k) {
			continue
		}
		// 获取要删除的对象并,给队列添加一个 deleted动作,相当于把对象删除了
		var deletedObj interface{}
		if n := oldItem.Newest(); n != nil {}
		if err := f.queueActionLocked(Deleted, DeletedFinalStateUnknown{k, deletedObj}); err != nil {
			return err
		}
	}
	return nil
}

Pop

消费的时候会通过 Pop 方法来获取变更,这里传入的 PopProcessFunc 就是我们最开始看到的 HandleDeltas

func (f *DeltaFIFO) Pop(process PopProcessFunc) (interface{}, error) {
	for {
		// 如果队列是空则进行等待
		for len(f.queue) == 0 {
			f.cond.Wait()
		}
	
		// 获取队头的key,并将队头删除
		id := f.queue[0]
		f.queue = f.queue[1:]

		// 获取deltas给到处理函数,isInInitialList是第一次Replace插入的对象个数
		item, ok := f.items[id]
		err := process(item, isInInitialList)

		// 将detlas返回
		return item, err
	}
}

Indexer

cache.cache 是 indexed 的实现,在 DeltaFIFO 中用来查找已知的对象。同个对象的deltas动作在不同的 clientState 要执行的动作不同,比如Deltas虽然是 Add的类型,但是实际上对象已经在cache里面存在,则需要执行的是Update动作而不是 Add 的动作。

func processDeltas(
	handler ResourceEventHandler,
	clientState Store,
	deltas Deltas,
	//...
) error {
	for _, d := range deltas {
		switch d.Type {
		case Sync, Replaced, Added, Updated:
			**// 这里会根据对象是否存在来看新增还是更新
			if old, exists, err := clientState.Get(obj); err == nil && exists {
				if err := clientState.Update(obj); err != nil {
					return err
				}
				handler.OnUpdate(old, obj)
			} else {
				if err := clientState.Add(obj); err != nil {
					return err
				}
				handler.OnAdd(obj, isInInitialList)
			}**
	}
}

至此,恭喜你已经学习了Informer整体的处理流程,我们通过一张UML类图来回顾一下关键的对象

  1. 蓝色框中的内容为controller的主要对象,统筹了整个informer的处理流程
  2. 绿色内容为监听变化处理的对象
  3. 橙色内容为与API server交互的对象抽象

Go编程小技巧

wait工具包

v1.29 代码路经 staging/src/k8s.io/apimachinery/pkg/util/wait

k8s抽象了自己的wait group:

type Group struct {
	wg sync.WaitGroup
}

func (g *Group) Wait() {
	g.wg.Wait()
}

// 这样封装后就不需要自己提前先Add,每次执行都会先做Add的动作
func (g *Group) Start(f func()) {
	g.wg.Add(1)
	go func() {
		defer g.wg.Done()
		f()
	}()
}

// 增加了stopCh的参数,这里也可以直接使用闭包实现
func (g *Group) StartWithChannel(stopCh <-chan struct{}, f func(stopCh <-chan struct{})) {
	g.Start(func() {
		f(stopCh)
	})
}

封装了周期性执行方法,在一定周期内会一直运行,直到 stopCh 收到信号后才停止

// 按period的时间周期执行f()
func Until(f func(), period time.Duration, stopCh <-chan struct{}) {
	JitterUntil(f, period, 0.0, true, stopCh)
}

func JitterUntil(f func(), period time.Duration, jitterFactor float64, sliding bool, stopCh <-chan struct{}) {
	BackoffUntil(f, NewJitteredBackoffManager(period, jitterFactor, &clock.RealClock{}), sliding, stopCh)
}

// 按一定时间周期去回退
func BackoffUntil(f func(), backoff BackoffManager, sliding bool, stopCh <-chan struct{}) {
	var t clock.Timer
	for {
		// 如果没有退出信号则一直处于循环中
		select {
		case <-stopCh:
			return
		default:
		}
		// 不滑动则period包含了f()运行的时间
		if !sliding {
			t = backoff.Backoff()
		}

		func() {
			defer runtime.HandleCrash()
			f()
		}()
	
		// 如果是滑动则在运行后才计算回退的时间,即等待时间周期 == period
		if sliding {
			t = backoff.Backoff()
		}

		// 这里有可能会发生竞争,为了缓解该问题,在重新开始执行的时候再次检查了stopCh
		select {
		case <-stopCh:
			if !t.Stop() {
				<-t.C()
			}
			return
		case <-t.C():
		}
	}
}

sync.Cond的使用

当资源未准备好的时候,可以通过 sync.Cond.Wait 进入等待,等待资源准备好了之后再通知协程启动。

我们来看一下下面模拟读写文件的例子

package concurrency

import (
	"fmt"
	"sync"
	"time"
)

var done bool

func read(c *sync.Cond) {
	c.L.Lock()
	// 写入未完成之前先陷入等待
	for !done {
		fmt.Println("read func wait")
		// 内部有unlock,等待被通知后重新唤起,所以临界资源仍然是安全的
		c.Wait()
	}

	fmt.Println("read : ", done)
	c.L.Unlock()
}

func write(c *sync.Cond) {
	c.L.Lock()
	time.Sleep(time.Second)
	fmt.Println("write func signal")
	// 写入完成标记
	done = true
	c.L.Unlock()
	// 唤醒等待的资源
	c.Broadcast()
}

通过测试来进行验证

func Test_write(t *testing.T) {
	cond := sync.NewCond(&sync.Mutex{})
	// 启动协程去读取,还未写入则不能读取成功
	go read(cond)

	go read(cond)

	time.Sleep(time.Second * 1)

	// 写入数据,完成后就能够读取成功
	write(cond)

	time.Sleep(time.Second * 3)
}

快速回顾

informer的机制总结起来其实就是通过version获取变更,然后将变更存储到队列中(DeltaFIFO)。

然后informer注册自己需要的处理方法(ResourceEventHandler),然后在循环中(processLoop)对变更进行相应的处理。

这个模式就是MQ的Producer和Consumer,最后再用一张比较简单的图来总结整个informer的流程

思考

1.为什么需要有DeltaFIFO,不能直接Watch吗?

要知道为什么需要有,可以先看看DeltaFIFO提供的功能。

它支持将变更的时间进行合并去重,避免了客户端收到重复的时间,减少了整体需要处理的事件,而直接 Watch 则需要处理所有事件。

2.Informer是如何保证事件不丢失的呢?

  • Watch 机制: 监听实时产生的变更,然后将变更加入到队列中,但是这时如果应用重启则会导致在内存中的数据丢失,就需要定期重新扫描最新状态来同步以及重新启动时 list 拉取所有已有的变更来补齐状态
  • Resync Period: Informer 支持设置一个定期重新同步(Resync)的时间间隔。在这个时间间隔内,Informer会主动向 API Server 查询资源对象的最新状态,以确保不会错过任何事件。虽然这并不能保证实时性,但是可以在一定程度上减小事件丢失的可能性。

写在最后

感谢你读到这里,如果想要看更多 Kubernetes 的文章可以订阅我的专栏: juejin.cn/column/7321…

Refrence

  1. 深入源码分析 kubernetes client-go list-watch 和 informer 机制的实现原理
  2. api-server的设计与实现
  3. How etcd works with and without Kubernetes
  4. 深入源码分析 kubernetes client-go sharedIndexInformer 和 SharedInformerFactory 的实现原理