k8s的控制器模式

控制器是什么,kubernetes官方解释是:一个永不终止的控制循环,它持续管理着集群的状态,通过apiserver获取系统的状态,并且不断尝试以达到预期状态,比如副本控制器,namespace控制器,serviceaccounts控制器。

比如部署时设置副本为3,则会持续watch并通过扩容、销毁等动作最终达到3. 也可以将其理解为如下伪代码:

for {
  desired := getDesiredState()
  current := getCurrentState()
  makeChanges(desired, current)
}

在这里插入图片描述

控制器的关键组件

有两个关键组件,分别是informer/SharedInformer和Workqueue,前者观察kubernetes对象当前的状态变化并发送事件到workqueue,然后这些事件会被worker们从上到下依次处理。

Informer

前面说到控制器的一个主要作用是去观察kubernetes对象的状态并与预期状态对比,然后做出相应动作以促使当前状态符合预期状态,这就需要控制器不断的向apiserver发起请求,但这样会带来巨大的性能压力,因此为满足这些需求,kubernetes社区通过client-go中的cache库来解决。

控制器并不需要持续的发请求以观察对象,它只需关注对象在新建、更新、删除时的事件,因此client-go提供了Listwatcher接口以提供一个initial list然后开始对指定资源进行观察,如:

lw := cache.NewListWatchFromClient(
      client,       //客户端
      &v1.Pod{}, // 被监控的资源类型
      api.NamespaceAll,  // 被监控的namespace
      fieldSelector)   // 选择器,减少匹配的资源数量

这些事件都会被informer消费,一个典型的informer结构如下:

store, controller := cache.NewInformer {
	&cache.ListWatch{},
	&v1.Pod{},      //监控的资源类型
	resyncPeriod,   //如果非0,则自动定期relist对象
	cache.ResourceEventHandlerFuncs{}, // ResourceEventHandler 事件发送给此对象处理
}

尽管Informer在当前的kubernetes代码中不多见,更多的是在用SharedInformer,但它依然是一个很重要的概念,尤其是当你要自己写一个控制器的时候,接下来是三个构成Informer的主要组件:

ListWatcher

ListWatcher是一个针对特定namespace下的特定资源的list和watch方法的混合体,通过ListWatcher,控制器能够只关注它指定关心的资源,field selector是一个过滤类型,它会过滤出符合条件的资源,ListWatcher的结构如下:

// ListerWatcher是任何支持对一个资源进行init list,并进行watch的对象
type ListerWatcher interface {
    List(options metav1.ListOptions) (runtime.Object, error)
        // watch能保证对资源进行持续不断的监控
    Watch(options metav1.ListOptions) (watch.Interface, error)
}
cache.ListWatch {
	listFunc := func(options metav1.ListOptions) (runtime.Object, error) {
		return client.Get().
			Namespace(namespace).
			Resource(resource).
			VersionedParams(&options, metav1.ParameterCodec).
			FieldsSelectorParam(fieldSelector).
			Do().
			Get()
	}
	watchFunc := func(options metav1.ListOptions) (watch.Interface, error) {
		options.Watch = true
		return client.Get().
			Namespace(namespace).
			Resource(resource).
			VersionedParams(&options, metav1.ParameterCodec).
			FieldsSelectorParam(fieldSelector).
			Watch()
	}
}

Resource Event Handler

Resource Event Handler是控制器处理特定资源通知的地方,如:

type ResourceEventHandlerFuncs struct {
  // 当资源第一次加入到Informer的缓存后调用
	AddFunc    func(obj interface{})
	//当既有资源被修改时调用。oldObj是资源的上一个状态,newObj则是新状态,
	// resync时此方法也被调用,即使对象没有任何变化
	UpdateFunc func(oldObj, newObj interface{})
	// 当既有资源被删除时调用,obj是对象的最后状态,如果最后状态未知则返回DeletedFinalStateUnknown
	DeleteFunc func(obj interface{})
}

AddFunc会在有新的资源被创建时被调用 UpdateFunc会在已存在资源有变更时被调用,它获取资源的最终状态(如果能获取到的话),否则它会获取对象的一个DeletedFinalStateUnknown类型,这个在watch被关闭并且错过了删除事件、且控制器直到重新list都没注意到的情况下发生。

ResyncPeriod

ResyncPeriod定义控制器更新缓存中所有item并调用UpdateFunc的频率,它提供了一种能够周期性的对比当前状态和预期状态的配置。它在控制器错过更新或之前动作失败的场景下很有用,但需要注意对资源的控制,频率过高可能导致CPU负载上涨。

SharedInformer

informer会在本地为它的控制器创建一个专属的资源集合的缓存,但在kubernetes中,有许多控制器,每个都在关注很多类型的资源,这意味着重复,一类资源将被多个控制器关注并缓存。

所以SharedInformer帮助在众多控制器中创建一个共享的缓存,这样不仅降低重复的缓存数量也减轻了内存的负载。并且每个SharedInformer对上游资源都只创建一个独立的watch,而不管下游有多少事件的消费者,这样也降低了上游的负载。

SharedInformer自带用来接收指定资源增、删、改的通知钩子hook,并且提供了方便的方法用来访问共享缓存以及确定一个缓存的优先时间,这减少了对apiserver的调用,也减少了服务端的重复操作。

Workqueue

因为是多个控制器之间的共享缓存,因此SharedInformer无法追踪到每个控制器的进度,所以控制器必须提供它自己的队列和重试机制,因此大多数Resource Event Handler都会把item放在一个单消费者的workqueue中。 不管何时资源变化,Resource Event Handler都会放一个key到workqueue中,这个Key使用格式:<resource_namespace>/<resource_name>如果<resource_namespace>是空,则就只用<resource_name>,这样每个消费者就都可以从上到下取出这些key,确保不会有多个consumer同时消费一个key。 workqueue在client-go的client-go/util/workqueue中,它支持如:

  • 延迟队列,延后一段时间再将元素入队,由接口DelayingInterface提供
  • 时序队列
  • 限速队列,限定单位时间内能够入队的元素量,由接口RateLimitingInterface提供 如下创建一个限速队列:
queue :=
workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter())

workqueue提供了方便的方法去管理key,如下是key在其中的生命周期 workqueue中的key 即:当key消费失败时,会通过调用AddRateLimited()被重新放入队列,并通过预设的重试次数进行重试,如果成功则这个key就可以通过调用Forget()方法删除,但这个方法只是不让workqueue再去追踪这个事件的历史,为确保彻底删除还需要调用Done()方法。

这样就实现了workqueue从cache中消费,但问题是,什么时候开始启动worker去处理workqueue呢?控制器应在缓存完整同步后,才去调用Worker,处理Workqueue,原因是:

  1. 直到缓存同步完毕,列出的资源才是精确的
  2. 可以让针对单个资源的多次更新合并为一个,避免反复处理中间状态,浪费资源 如伪代码:
controller.informer = cache.NewSharedInformer(...)
controller.queue = workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter())

controller.informer.Run(stopCh)

if !cache.WaitForCacheSync(stopCh, controller.HasSynched)
{
	log.Errorf("Timed out waiting for caches to sync"))
}

// Now start processing
controller.runWorker()

开发自定义控制器案例:

接下来以kubewatch为例说明自定义controller的开发,通常控制器结构包含如下几点:

// Controller object
type Controller struct {
      logger       *logrus.Entry   //日志
      clientset    kubernetes.Interface //连接apiserver
      queue        workqueue.RateLimitingInterface
      informer     cache.SharedIndexInformer
      eventHandler handlers.Handler
}

新建SharedInformer

informer := cache.NewSharedIndexInformer(
      &cache.ListWatch{
             ListFunc: func(options meta_v1.ListOptions) (runtime.Object, error) {
                    return client.CoreV1().Pods(meta_v1.NamespaceAll).List(options)
             },
             WatchFunc: func(options meta_v1.ListOptions) (watch.Interface, error) {
                    return client.CoreV1().Pods(meta_v1.NamespaceAll).Watch(options)
             },
      },
      &api_v1.Pod{},
      0, //Skip resync
      cache.Indexers{},
)
  • ListWatcher告诉控制器想要list和观察所有namesapce下的所有pod
  • 跳过控制器缓存的resynchronization
  • 使用SharedIndexInformer而非SharedInformer是因为前者可允许控制器维护一个对缓存中所有对象的索引

生成controller

c := newResourceController(kubeClient, eventHandler, informer, "deployment")

新建workqueue

func newResourceController(client kubernetes.Interface, eventHandler handlers.Handler, informer cache.SharedIndexInformer, resourceType string) *Controller {
	queue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter())
}

给informer添加eventhandler

informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
      AddFunc: func(obj interface{}) {
             key, err := cache.MetaNamespaceKeyFunc(obj)
             if err == nil {
                    queue.Add(key)
             }
      },
      DeleteFunc: func(obj interface{}) {
             key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
             if err == nil {
                    queue.Add(key)
             }
      },
})

在workqueue中的事件通过它们对应的key来表示,key格式为:pod_namespace/pod_name

运行控制器

// Run will start the controller.
// StopCh channel is used to send interrupt signal to stop it.
func (c *Controller) Run(stopCh <-chan struct{}) {
      // 避免panic导致程序挂掉
      defer utilruntime.HandleCrash()
      defer c.queue.ShutDown()
      c.logger.Info("Starting kubewatch controller")
      //启动informer
      go c.informer.Run(stopCh)
      // 等待缓存同步完成后再启动worker
      if !cache.WaitForCacheSync(stopCh, c.HasSynced) {
             utilruntime.HandleError(fmt.Errorf("Timed out waiting for caches to sync"))
             return
      }
      c.logger.Info("Kubewatch controller synced and ready")
     // 每秒执行一次,直到接收到停止信号
      wait.Until(c.runWorker, time.Second, stopCh)
}

这里SharedInformer开始观察pod并发送他们的key到workqueue,接下来需定义worker如何取出key并做处理,参考之前的生命周期管理代码如下:

编写runworker

func (c *Controller) runWorker() {
// processNextWorkItem 会自动等待直到有可用的work
      for c.processNextItem() {
             // 持续循环
      }
}
// processNextWorkItem 一次从队列里处理一个key,如果队列为空则返回false
func (c *Controller) processNextItem() bool {
       // 从队列中取出一个key
      key, quit := c.queue.Get()
      if quit {
             return false
      }
       // 务必记得删除key
      defer c.queue.Done(key)
      // 处理key
      err := c.processItem(key.(string))
      if err == nil {
             // 如果没有异常,则告诉queue停止追踪
             c.queue.Forget(key)
      } else if c.queue.NumRequeues(key) < maxRetries {
             c.logger.Errorf("Error processing %s (will retry): %v", key, err)
             // 将key重新放入队列随后处理
c.queue.AddRateLimited(key)
      } else {
             // err != nil 并且超过重试次数
             c.logger.Errorf("Error processing %s (giving up): %v", key, err)
             c.queue.Forget(key)
             utilruntime.HandleError(err)
      }
      return true
}
func (c *Controller) processItem(key string) error {
      c.logger.Infof("Processing change to Pod %s", key)

      obj, exists, err := c.informer.GetIndexer().GetByKey(key)
      if err != nil {
             return fmt.Errorf("Error fetching object with key %s from store: %v", key, err)
      }
      if !exists {
             c.eventHandler.ObjectDeleted(obj)
             return nil
      }
      c.eventHandler.ObjectCreated(obj)
      return nil
}

发送通知到slack

func (s *Slack) ObjectCreated(obj interface{}) {
      notifySlack(s, obj, "created")
}

func (s *Slack) ObjectDeleted(obj interface{}) {
      notifySlack(s, obj, "deleted")
}

func notifySlack(s *Slack, obj interface{}, action string) {
      e := kbEvent.New(obj, action)
      api := slack.New(s.Token)
      params := slack.PostMessageParameters{}
      attachment := prepareSlackAttachment(e)

      params.Attachments = []slack.Attachment{attachment}
      params.AsUser = true
      channelID, timestamp, err := api.PostMessage(s.Channel, "", params)
      if err != nil {
             log.Printf("%s\n", err)
             return
      }

      log.Printf("Message successfully sent to channel %s at %s", channelID, timestamp)
}

验证

$ wget https://github.com/skippbox/kubewatch/releases/download/v0.0.3/kubewatch.yaml
$ kubectl create -f kubewatch.yaml
$ kubectl get pods
NAME        READY     STATUS    RESTARTS   AGE
kubewatch   1/1       Running   0          2m

效果: 在这里插入图片描述

留两个思考题:

  1. 上述案例只是实现了对特定类别资源(如pod)增删改的监听并做出通知,如果想要获取所有类别资源的增删改、并发送具体详情(如因OOM被Kill,如因healthCheck失败被Kill等)该怎么做?
  2. workqueue中的几种队列模式是如何实现的?

参考:

https://engineering.bitnami.com/articles/a-deep-dive-into-kubernetes-controllers.html https://engineering.bitnami.com/articles/kubewatch-an-example-of-kubernetes-custom-controller.html https://blog.gmem.cc/extend-kubernetes-with-custom-resources