Controller通过client-go实现事件监听和业务处理

341 阅读15分钟

整体架构图

controller_view.png

Informer

Informer 是一个用于在 Kubernetes 集群中跟踪和处理对象(如 Pod、Service、Node 等)变化的工具。它提供了一种方便的方式来监视和响应集群中资源的增加、修改和删除操作。Reflector与 Kubernetes API Server 进行交互后存入Delta FIFO的数据,Informer 通过从Delta FIFO获取并处理对数据进行处理,从而感知资源的变化,并触发相应的事件处理逻辑。我们可以使用 Informer 来构建自定义的控制器,实现对集群中资源的实时监控和处理。

Reflector

Reflector 是一个用于从 Kubernetes API Server 获取资源对象并将其反射到本地缓存中的工具。它负责定期向 API Server 发出请求,获取资源的变化并更新本地缓存,以便应用程序可以通过缓存快速访问和操作资源对象。

Reflector 通过 Watch API 进行资源的持续监视,并处理来自 API Server 的事件流。它使用增量同步的方式,仅将变化的资源对象同步到本地缓存中,以减少网络流量和资源消耗。

Reflector 还提供了一些灵活的配置选项,例如可以定义要监视的资源类型、命名空间、标签选择器等。它还支持自定义的事件处理函数,以便应用程序可以根据获取到的资源对象进行处理。

Reflector 在应用程序中起到了关键的作用,它帮助应用程序实时跟踪和同步 Kubernetes 集群中的资源变化,提供了高效的资源访问方式,同时减轻了对 API Server 的压力。

Delta FIFO Queue

Delta FIFO 队列是 client-go 中的一种队列实现,用于在资源对象的增删改操作中跟踪变更的信息。它基于 Delta 算法,能够高效地记录和处理资源对象的增量变化。

Delta FIFO 队列通过存储增删改操作的 Delta 数据来维护资源对象的变更历史。当有新的资源对象变更事件到达时,Delta FIFO 队列会将其转换为 Delta 数据,并将其添加到队列中。

队列中的 Delta 数据按照严格的先进先出(FIFO)顺序进行处理。每个 Delta 数据都包含了资源对象的关键信息,例如资源类型、命名空间、名称和变更类型(增加、删除、修改)。这样,应用程序可以按照变更的顺序对资源对象进行处理,保证操作的一致性和正确性。

Delta FIFO 队列在 client-go 中被广泛应用于控制器和调谐器等组件中,用于跟踪和处理资源对象的变更。它提供了一种可靠的机制,确保资源对象的增删改操作按照正确的顺序进行处理,并减少了对 API Server 的请求次数,提高了应用程序的性能和效率。

Indexer

在 client-go 中,Indexer 是一个用于对 Kubernetes 资源对象进行索引和查询的工具。它提供了一种高效的方式来根据自定义的索引键检索资源对象,而无需遍历整个资源列表。

Indexer 将资源对象存储在内部的索引数据结构中,通过指定的索引键将每个资源对象映射到相应的索引位置。这使得根据索引键进行查询变得非常快速和高效。

Indexer 还支持根据多个索引键进行复合查询,可以使用逻辑运算符(如 AND、OR)组合多个索引条件进行精确的过滤和检索。

除了索引和查询功能,Indexer 还具备对资源对象的增删改操作的能力,它会自动更新索引数据结构以保持与资源列表的同步。

在应用程序中使用 Indexer,可以方便地根据自定义的索引键进行资源对象的快速检索,避免了遍历整个资源列表的性能开销。它在控制器、调谐器和其他需要根据特定条件查询资源对象的组件中被广泛使用。

Cache(Local Store)

Cache 是一个用于存储和管理 Kubernetes 资源对象的本地缓存机制。它提供了一种在客户端应用程序中缓存和跟踪集群中资源对象的方式,以减少对 API Server 的请求次数并提高性能。

Cache 在应用程序启动时会从 API Server 中获取初始化数据,并在后续的操作中保持与 API Server 的同步。它会自动处理资源对象的增加、修改和删除操作,并更新本地缓存的数据,以便应用程序可以快速读取和操作资源对象,而无需频繁地与 API Server 进行通信。

使用 Cache,开发人员可以轻松地获取和操作已缓存的资源对象,而无需每次操作都与 API Server 进行通信,从而提高应用程序的性能和响应速度。同时,Cache 还提供了一些便捷的方法来查询和筛选资源对象,以满足应用程序的需求。

Queue

Queue 是一个用于在Client-go中实现工作队列的工具。它提供了一种机制来管理要处理的任务,并确保任务按顺序进行处理。

Queue 主要用于管理需要异步处理的事件或操作。应用程序可以将待处理的任务添加到队列中,然后按照需要从队列中取出任务进行处理。Queue 还提供了一些实用的功能,如任务的优先级排序、任务的延迟处理等。

使用 Queue,可以有效地控制并发处理任务,避免资源竞争和冲突。它可以帮助应用程序实现更高效的异步处理,提高吞吐量和响应能力。

Client-go 中的 Queue 可以与 Informer、Controller 等结合使用,帮助我们构建高效、可靠的控制器或处理逻辑,以处理 Kubernetes 集群中的事件和操作。

为什么获取资源对象的变化不建议使用watch API

client-go中watch源码解析

在client-go中使用watch API用于监控特定资源,通过资源的事件发生和变化,感知资源的状态变化并及时反馈给客户端,这种方式会加大APIServer的处理请求的压力,因为这个API会一直轮询APIServer以获取信息

    // <https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/client-go/kubernetes/typed/core/v1/pod.go#L105>
    func (c *pods) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) {
    	var timeout time.Duration
    	if opts.TimeoutSeconds != nil {
    		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
    	}
    	opts.Watch = true
    	return c.client.Get().
    		Namespace(c.ns).
    		Resource("pods").
    		VersionedParams(&opts, scheme.ParameterCodec).
    		Timeout(timeout).
    		Watch(ctx)
    }
    // <https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/client-go/rest/request.go#L703>
    func (r *Request) Watch(ctx context.Context) (watch.Interface, error) {
    	...
    	for {
    		if err := retry.Before(ctx, r); err != nil {
    			return nil, retry.WrapPreviousError(err)
    		}

    		req, err := r.newHTTPRequest(ctx)
    		if err != nil {
    			return nil, err
    		}

    		resp, err := client.Do(req)
    		retry.After(ctx, r, resp, err)
    		if err == nil && resp.StatusCode == http.StatusOK {
    			return r.newStreamWatcher(resp)
    		}
    		....

在StreamWatcher的receive方法中,通过轮询方式解码收到的响应,并将其解析为包括Type和Object的Event,客户端通过监听result channel获取Event,直到收到sw.done的退出信号后退出

    // <https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apimachinery/pkg/watch/streamwatcher.go#L100>
    func (sw *StreamWatcher) receive() {
    	defer utilruntime.HandleCrash()
    	defer close(sw.result)
    	defer sw.Stop()
    	for {
    		action, obj, err := sw.source.Decode()
    		if err != nil {
    			switch err {
    			case io.EOF:
    				// watch closed normally
    			case io.ErrUnexpectedEOF:
    				klog.V(1).Infof("Unexpected EOF during watch stream event decoding: %v", err)
    			default:
    				if net.IsProbableEOF(err) || net.IsTimeout(err) {
    					klog.V(5).Infof("Unable to decode an event from the watch stream: %v", err)
    				} else {
    					select {
    					case <-sw.done:
    					case sw.result <- Event{
    						Type:   Error,
    						Object: sw.reporter.AsObject(fmt.Errorf("unable to decode an event from the watch stream: %v", err)),
    					}:
    					}
    				}
    			}
    			return
    		}
    		select {
    		case <-sw.done:
    			return
    		case sw.result <- Event{
    			Type:   action,
    			Object: obj,
    		}:
    		}
    	}
    }

对于每次的得到response,进行是否需要重试判断,如果需要重试就会继续轮询

    // <https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/client-go/rest/request.go#L744>
        if retry.IsNextRetry(ctx, r, req, resp, err, isErrRetryableFunc) {
                return false, nil
        }

所以我们一般不使用watch获取Kubernetes资源信息,而是通过更加高效的Informer实现资源变化的信息获取, Informer 的好处:

  • 通过DeltaFIFO Queue从Reflector同步资源event事件,从而减轻 APIServer 的压力
  • 自动处理包括错误处理在内的监听事件的整个过程
  • 将对象以线程安全的方式保存在存储器中
  • 提供可用的Handler,帮助我们获取对象

在整个资源变化监听和处理的控制流程中,Informer起到关键的作用包括:

  • 注册特定资源的监听事件
  • 同步来自 Reflector 的监听事件
  • 通过Queue分发监听事件,通知自定义控制器处理事件

注册监听资源变化的handler

// <https://github1s.com/kubernetes/client-go/blob/master/tools/cache/controller.go#L235-L255>

// OnAdd calls AddFunc if it's not nil.
func (r ResourceEventHandlerFuncs) OnAdd(obj interface{}, isInInitialList bool) {
	if r.AddFunc != nil {
		r.AddFunc(obj)
	}
}

// OnUpdate calls UpdateFunc if it's not nil.
func (r ResourceEventHandlerFuncs) OnUpdate(oldObj, newObj interface{}) {
	if r.UpdateFunc != nil {
		r.UpdateFunc(oldObj, newObj)
	}
}

// OnDelete calls DeleteFunc if it's not nil.
func (r ResourceEventHandlerFuncs) OnDelete(obj interface{}) {
	if r.DeleteFunc != nil {
		r.DeleteFunc(obj)
	}
}

<https://github1s.com/kubernetes/client-go/blob/master/tools/cache/shared_informer.go#L560-L562>
func (s *sharedIndexInformer) AddEventHandler(handler ResourceEventHandler) (ResourceEventHandlerRegistration, error) {
	return s.AddEventHandlerWithResyncPeriod(handler, s.defaultEventHandlerResyncPeriod)
}

通过给handler添加listener监听事件

<https://github1s.com/kubernetes/client-go/blob/master/tools/cache/shared_informer.go#L623>
func (s *sharedIndexInformer) AddEventHandlerWithResyncPeriod(handler ResourceEventHandler, resyncPeriod time.Duration) (ResourceEventHandlerRegistration, error) {
...
	listener := newProcessListener(handler, resyncPeriod, determineResyncPeriod(resyncPeriod, s.resyncCheckPeriod), s.clock.Now(), initialBufferSize, s.HasSynced)

	if !s.started {
		return s.processor.addListener(listener), nil
	}

	s.blockDeltas.Lock()
	defer s.blockDeltas.Unlock()

	handle := s.processor.addListener(listener)
	for _, item := range s.indexer.List() {
		listener.add(addNotification{newObj: item, isInInitialList: true})
	}
	return handle, nil
}

通过addCh监听添加事件,并将事件发送给nextCh

<https://github1s.com/kubernetes/client-go/blob/master/tools/cache/shared_informer.go#L929-L964>
func (p *processorListener) add(notification interface{}) {
	if a, ok := notification.(addNotification); ok && a.isInInitialList {
		p.syncTracker.Start()
	}
	p.addCh <- notification
}

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:
			// Notification dispatched
			var ok bool
			notification, ok = p.pendingNotifications.ReadOne()
			if !ok { // Nothing to pop
				nextCh = nil // Disable this select case
			}
		case notificationToAdd, ok := <-p.addCh:
			if !ok {
				return
			}
			if notification == nil { // No notification to pop (and pendingNotifications is empty)
				// Optimize the case - skip adding to pendingNotifications
				notification = notificationToAdd
				nextCh = p.nextCh
			} else { // There is already a notification waiting to be dispatched
				p.pendingNotifications.WriteOne(notificationToAdd)
			}
		}
	}
}

监听nextCh并处理接受到的添加事件,判断对应的事件类型,调用对应的handler处理事件

<https://github1s.com/kubernetes/client-go/blob/master/tools/cache/shared_informer.go#L966-L991>
func (p *processorListener) run() {
	// this call blocks until the channel is closed.  When a panic happens during the notification
	// we will catch it, **the offending item will be skipped!**, and after a short delay (one second)
	// the next notification will be attempted.  This is usually better than the alternative of never
	// delivering again.
	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))
			}
		}
		// the only way to get here is if the p.nextCh is empty and closed
		close(stopCh)
	}, 1*time.Second, stopCh)
}

在自定义控制器的一侧,我们只需要从队列中取出对象,解析具体对象信息,然后进行自己的业务处理。

SharedInformerFactory

SharedInformerFactory 主要负责创建各种 Informer,例如 Deployment Informer,Informer通过resync周期性地从 APIServer 同步资源信息。

    // <https://github1s.com/kubernetes/client-go/blob/master/informers/factory.go#L125>
    // NewSharedInformerFactoryWithOptions constructs a new instance of a SharedInformerFactory with additional options.
    func NewSharedInformerFactoryWithOptions(client kubernetes.Interface, defaultResync time.Duration, options ...SharedInformerOption) SharedInformerFactory {
    	factory := &sharedInformerFactory{
    		client:           client,
    		namespace:        v1.NamespaceAll,
    		defaultResync:    defaultResync,
    		informers:        make(map[reflect.Type]cache.SharedIndexInformer),
    		startedInformers: make(map[reflect.Type]bool),
    		customResync:     make(map[reflect.Type]time.Duration),
    	}

    	// Apply all options
    	for _, opt := range options {
    		factory = opt(factory)
    	}

    	return factory
    }

通过informer监听deployment的创建和删除

创建一个deployment的informer,并注册对应的事件监听器handler,然后启动informer

    func deployInformer(clientset *kubernetes.Clientset) v1.DeploymentInformer {
    	sharedInformers := informers.NewSharedInformerFactory(clientset, 1*time.Second)
    	depInformer := sharedInformers.Apps().V1().Deployments()
    	ch := make(<-chan struct{})
    	sharedInformers.Start(ch)
    	sharedInformers.WaitForCacheSync(ch)
    	return depInformer
    }

当informer启动后,会自动创建并发安全的缓存,存储从APIServer同步的Event数据,informer可以通过lister从缓存中获取Event数据

    func listWith(depInformer v1.DeploymentInformer) {
    	name := "nginx-deployment"
    	deploy, err := depInformer.Lister().Deployments("default").Get(name)
    	if apierrors.IsNotFound(err) {
    		log.Println("not found")
    		return
    	}
    	if err != nil {
    		return
    	}
    	log.Println("name: ", deploy.Name)
    }

注册添加和删除事件,并启动informer

    func deployInformer(clientset *kubernetes.Clientset) v1.DeploymentInformer {
    	sharedInformers := informers.NewSharedInformerFactory(clientset, 1*time.Second)
    	depInformer := sharedInformers.Apps().V1().Deployments()
    	depInformer.Informer().AddEventHandler(
    		cache.ResourceEventHandlerFuncs{
    			AddFunc: func(item interface{}) {
    				log.Println("add item")
    			},
    			DeleteFunc: func(item interface{}) {
    				log.Println("delete item")
    			},
    		},
    	)
    	ch := make(<-chan struct{})
    	sharedInformers.Start(ch)
    	sharedInformers.WaitForCacheSync(ch)
    	return depInformer
    }

Client-go监听集群中发生的资源事件

启动程序

    func main() {
    	clientset, err := NewClient()
    	if err != nil {
    		log.Fatal(err)
    	}
    	depInformer, queue := deployInformer(clientset)
    	for {
    		// list(clientset)
    		listWith(depInformer)
    		time.Sleep(10 * time.Second)
    	}
    }

创建一个deployment

    ➜  client-go-app ka -f nginx-deployment.yaml
    deployment.apps/nginx-deployment created
    ➜  client-go-app kg deploy
    NAME               READY   UP-TO-DATE   AVAILABLE   AGE
    nginx-deployment   3/3     3            3           15s

我们可以看到lister感知到了deployment的创建,并调用了对应的事件handler,打印了相应的日志

    ➜  client-go-app go run main.go
    2024/03/24 13:21:28 kube config file path:  /home/going/.kube/config
    2024/03/24 13:21:28 add item
    2024/03/24 13:21:28 add item
    2024/03/24 13:21:28 add item
    2024/03/24 13:21:28 add item
    2024/03/24 13:21:28 add item

删除deployment

    ➜  client-go-app kd -f nginx-deployment.yaml
    deployment.apps "nginx-deployment" deleted

删除的事件也被能被lister感知

    ➜  client-go-app go run main.go
    2024/03/24 13:21:28 kube config file path:  /home/going/.kube/config
    2024/03/24 13:21:28 add item
    2024/03/24 13:21:28 add item
    2024/03/24 13:21:28 add item
    2024/03/24 13:21:28 add item
    2024/03/24 13:21:28 add item
    2024/03/24 13:21:28 not found

    2024/03/24 13:21:34 add item
    2024/03/24 13:21:38 name:  nginx-deployment
    2024/03/24 13:21:48 name:  nginx-deployment
    2024/03/24 13:21:50 delete item

client-go传递资源对象事件给Controller

Queue是 Informer 的关键组成部分,它被用于在 client-go 和自定义控制器之间同步监听事件,然后由调度器消费事件并进行业务逻辑的处理。

    func deployInformer(clientset *kubernetes.Clientset) (v1.DeploymentInformer, workqueue.RateLimitingInterface) {
    	sharedInformers := informers.NewSharedInformerFactory(clientset, 1*time.Second)
    	depInformer := sharedInformers.Apps().V1().Deployments()
    	queue := workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "test")
    	depInformer.Informer().AddEventHandler(
    		cache.ResourceEventHandlerFuncs{
    			AddFunc: func(item interface{}) {
    				log.Println("add item")
    				queue.Add(item)
    			},
    			DeleteFunc: func(item interface{}) {
    				log.Println("delete item")
    				queue.Add(item)
    			},
    		},
    	)
    	ch := make(<-chan struct{})
    	sharedInformers.Start(ch)
    	sharedInformers.WaitForCacheSync(ch)
    	return depInformer, queue
    }

Controller通过从Queue中弹出数据来获取事件,然后解析出的namespace和name进行lister操作来获取具体资源信息。

    func main() {
    	clientset, err := NewClient()
    	if err != nil {
    		log.Fatal(err)
    	}
    	depInformer, queue := deployInformer(clientset)
    	for {
    		// list(clientset)
    		item, shutdown := queue.Get()
    		if shutdown {
    			return
    		}
    		key, err := cache.MetaNamespaceKeyFunc(item)
    		if err != nil {
    			continue
    		}
    		ns, name, _ := cache.SplitMetaNamespaceKey(key)
    		listWith(depInformer, ns, name)
    		time.Sleep(10 * time.Second)
    	}
    }

通过部署deployment验证监听事件程序工作过程

启动程序

    ➜  client-go-app go run main.go
    2024/03/24 14:35:55 kube config file path:  /home/going/.kube/config

通过创建和删除deployment来测试整个流程

    ➜  client-go-app ka -f nginx-deployment.yaml
    deployment.apps/nginx-deployment created
    ➜  client-go-app kg deploy
    NAME               READY   UP-TO-DATE   AVAILABLE   AGE
    nginx-deployment   3/3     3            3           15s
    ➜  client-go-app kd -f nginx-deployment.yaml
    deployment.apps "nginx-deployment" deleted

对应的事件已经被监听到并执行相应的打印输出

      client-go-app go run main.go
    2024/03/24 14:35:55 kube config file path:  /home/going/.kube/config
    2024/03/24 14:35:55 add item
    2024/03/24 14:35:55 add item
    2024/03/24 14:35:55 add item
    2024/03/24 14:35:55 add item
    2024/03/24 14:35:55 add item
    2024/03/24 14:35:55 name:  lister
    2024/03/24 14:36:04 add item
    2024/03/24 14:36:05 name:  ingress-kong
    2024/03/24 14:36:15 name:  proxy-kong
    2024/03/24 14:36:25 name:  coredns
    2024/03/24 14:36:35 name:  local-path-provisioner
    2024/03/24 14:36:40 delete item
    2024/03/24 14:36:45 not found
    2024/03/24 14:36:55 not found

ResourceVersion

ResourceVersion 是表示资源当前状态的指纹,唯一标识资源的当前状态,一旦资源发生更改,它将会改变。例如,我们可以通过在标签中添加 lastResourceVersion: "874320" 来编辑 Pod。

      client-go-app kg pod
    NAME                     READY   STATUS    RESTARTS   AGE
    lister-9f8bf577b-8pbht   1/1     Running   0          19h
      client-go-app k edit pod lister-9f8bf577b-8pbht
    apiVersion: v1
    kind: Pod
    metadata:
      creationTimestamp: "2024-03-23T09:45:16Z"
      generateName: lister-9f8bf577b-
      labels:
        app: lister
        pod-template-hash: 9f8bf577b
        lastResourceVersion: "874320"  // modified version
      name: lister-9f8bf577b-8pbht
      namespace: default
      ownerReferences:
      - apiVersion: apps/v1
        blockOwnerDeletion: true
        controller: true
        kind: ReplicaSet
        name: lister-9f8bf577b
        uid: 408c0707-fbc6-41a5-92ab-9d3d3227503f
      resourceVersion: "874320"       // current version
      uid: c46ec159-8996-42b7-9469-64971a05bad4
      ...

在修改的内容中,我们记录了当前的 resourceVersion,即 "874320",然后保存并再次检查,我们可以看到 resourceVersion 已更新为 "1004329"。

    apiVersion: v1
    kind: Pod
    metadata:
      creationTimestamp: "2024-03-23T09:45:16Z"
      generateName: lister-9f8bf577b-
      labels:
        app: lister
        lastResourceVersion: "874320" // recorded version
        pod-template-hash: 9f8bf577b
      name: lister-9f8bf577b-8pbht
      namespace: default
      ownerReferences:
      - apiVersion: apps/v1
        blockOwnerDeletion: true
        controller: true
        kind: ReplicaSet
        name: lister-9f8bf577b
        uid: 408c0707-fbc6-41a5-92ab-9d3d3227503f
      resourceVersion: "1004329"    // updated version
      ...

需要明确的一点是,不要更新 Informer 的缓存资源,因为它由 Informer 维护并保存在Store中,为了避免 APIServer 和 Informer 之间的不一致性,它应该被视为只读。因此,正如注释所说,必须将其视为只读。

    // <https://github1s.com/kubernetes/client-go/blob/master/listers/apps/v1/deployment.go#L70>
    // DeploymentNamespaceLister helps list and get Deployments.
    // All objects returned here must be treated as read-only.
    type DeploymentNamespaceLister interface {
    	// List lists all Deployments in the indexer for a given namespace.
    	// Objects returned here must be treated as read-only.
    	List(selector labels.Selector) (ret []*v1.Deployment, err error)
    	// Get retrieves the Deployment from the indexer for a given namespace and name.
    	// Objects returned here must be treated as read-only.
    	Get(name string) (*v1.Deployment, error)
    	DeploymentNamespaceListerExpansion
    }

如果我们想筛选从 APIServer 获取的资源,可以使用 FilteredSharedInformerFactory。例如,我们可以筛选具有标签 app=test 并且仅存在于 test namespace的 Deployment 资源。

// <https://github1s.com/kubernetes/client-go/blob/master/metadata/metadatainformer/informer.go#L53>

    filteredSharedInformers := informers.NewFilteredSharedInformerFactory(
    		clientset, 1 * time.Minute, "test", internalinterfaces.TweakListOptionsFunc(
    			func(o *metav1.ListOptions){
    				o.LabelSelector = "app=test"
    				o.Kind = "Deployment"
    			}),
    	)