Kubernetes Informer 机制(一)

129 阅读5分钟

背景

相信接触过云原生的朋友都写过 kubernetes controller。笔者平时编写 controller 过程中最常用的是 controller-runtime 框架,而 controller-runtime 框架底层便是今天的主角——informer 机制。

因为 informer 机制,以及上层的 controller-runtime 非常庞大,所以笔者只能按照多篇文章来慢慢讲解。本文的重点是以 client-go 中提供的 example 为例,分析官网图中的各个步骤。

Informer 原理

image.png 先上个官网图,估计很多读者看到了会很闷逼,为啥会有两个队列,indexer 又是个啥。

首先明确中间的虚线,上半部分是 client-go 中的实现(其实 java 中也有类似的 informer 库),下半部分是各 controller 的实现,比如常用的 controller-runtime 框架其实是帮助开发者实现了图中的大部分逻辑,开发者只需要关心 process 部分即可。再比如说 volcano 调度器代码中的 controller 就没有用到 controller-runtime,而是直接使用了 informer sdk(参见 Volcano 源码解读(一)控制器

咱们从 client-go 提供的 example 看起。该 example 中就是通过 informer sdk 来实现 pod watch 逻辑。源码地址:exmaple

进入到 main 函数:

  1. 初始化 cacheClient 用以后续的 listAndWatch;
  2. 初始化了 workerqueue(限速队列);
  3. 创建 indexer 和 informer。indexer 用以缓存 apiserver 的数据,同时通过索引的方式加快 list 效率;informer 则是后续过程的主 Object;
  4. 将 default 命名空间下的 mypod Pod 加入到 indexer 中(示例,假设之前处理了 indexer);
  5. 启动 controller。 精简后的代码如下。
// examples/workqueue/main.go
func main() {
    // create the pod watcher
    podListWatcher := cache.NewListWatchFromClient(clientset.CoreV1().RESTClient(), "pods", v1.NamespaceDefault, fields.Everything())

    // create the workqueue
    queue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter())

    indexer, informer := cache.NewIndexerInformer(podListWatcher, &v1.Pod{}, 0, cache.ResourceEventHandlerFuncs{
    // ...
    }, cache.Indexers{})

    controller := NewController(queue, indexer, informer)

    indexer.Add(&v1.Pod{
       ObjectMeta: meta_v1.ObjectMeta{
          Name:      "mypod",
          Namespace: v1.NamespaceDefault,
       },
    })

    go controller.Run(1, stop)
}

直接进入到 controller.Run 方法。

  1. 调用 informer.Run,启动 informer;
  2. 启动 runWorker,worker 中包含了主要的业务逻辑。
// examples/workqueue/main.go
func (c *Controller) Run(workers int, stopCh chan struct{}) {
    go c.informer.Run(stopCh)


    for i := 0; i < workers; i++ {
       go wait.Until(c.runWorker, time.Second, stopCh)
    }
}

再走到 infromer.Run 中,此处创建了 NewReflector Object(与架构图能够对应)并执行了 Reflector.Run。Reflector.Run 中就可以看到架构图中的 步骤1 List & Watch,ListAndWatch。

// tools/cache/reflector.go
func (r *Reflector) Run(stopCh <-chan struct{}) {
    wait.BackoffUntil(func() {
       if err := r.ListAndWatch(stopCh); err != nil {
          r.watchErrorHandler(r, err)
       }
    }, r.backoffManager, true, stopCh)
}

进入到 ListAndWatch,再进入到 watchHandler,此处声明了针对增量 Object 的处理。此处正对应架构图中的 步骤2 Add Object

// tools/cache/reflector.go
func (r *Reflector) watchHandler(start time.Time, w watch.Interface, resourceVersion *string, errc chan error, stopCh <-chan struct{}) error {
loop:
    for {
       select {
       case <-stopCh:
          return errorStopRequested
       case err := <-errc:
          return err
       case event, ok := <-w.ResultChan():
          if !ok {
             break loop
          }
          if event.Type == watch.Error {
             return apierrors.FromObject(event.Object)
          }
          if r.expectedType != nil {
             if e, a := r.expectedType, reflect.TypeOf(event.Object); e != a {
                utilruntime.HandleError(fmt.Errorf("%s: expected type %v, but watch event object had type %v", r.name, e, a))
                continue
             }
          }
          if r.expectedGVK != nil {
             if e, a := *r.expectedGVK, event.Object.GetObjectKind().GroupVersionKind(); e != a {
                utilruntime.HandleError(fmt.Errorf("%s: expected gvk %v, but watch event object had gvk %v", r.name, e, a))
                continue
             }
          }
          meta, err := meta.Accessor(event.Object)
          if err != nil {
             utilruntime.HandleError(fmt.Errorf("%s: unable to understand watch event %#v", r.name, event))
             continue
          }
          newResourceVersion := meta.GetResourceVersion()
          switch event.Type {
          case watch.Added:
             err := r.store.Add(event.Object)
             if err != nil {
                utilruntime.HandleError(fmt.Errorf("%s: unable to add watch event object (%#v) to store: %v", r.name, event.Object, err))
             }
          case watch.Modified:
             err := r.store.Update(event.Object)
             if err != nil {
                utilruntime.HandleError(fmt.Errorf("%s: unable to update watch event object (%#v) to store: %v", r.name, event.Object, err))
             }
          case watch.Deleted:
             // TODO: Will any consumers need access to the "last known
             // state", which is passed in event.Object? If so, may need
             // to change this.
             err := r.store.Delete(event.Object)
             if err != nil {
                utilruntime.HandleError(fmt.Errorf("%s: unable to delete watch event object (%#v) from store: %v", r.name, event.Object, err))
             }
          case watch.Bookmark:
             // A `Bookmark` means watch has synced here, just update the resourceVersion
          default:
             utilruntime.HandleError(fmt.Errorf("%s: unable to understand watch event %#v", r.name, event))
          }
       }
    }
}

步骤3 Pop Object则在 processLoop 中,其在 controller.Run 时被调用,且每秒同步一次,此时可以发现 pop 动作是周期性执行的。

// tools/cache/controller.go
func (c *controller) processLoop() {
    for {
       obj, err := c.config.Queue.Pop(PopProcessFunc(c.config.Process))
       if err != nil {
          if err == ErrFIFOClosed {
             return
          }
          if c.config.RetryOnError {
             // This is the safe way to re-enqueue.
             c.config.Queue.AddIfNotPresent(obj)
          }
       }
    }
}

进入到 Pop 函数中,其中最重要的是 process 函数。process 是通过 hook 的方式被引用。实则就是 controller 中已定义的。

// tools/cache/controller.go
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)
    }
case Deleted:
    if err := clientState.Delete(obj); err != nil {
       return err
    }
    handler.OnDelete(obj)
}

步骤4 Add Key步骤5 Store Object Key 中的 Add 和 Store,则为 clientState 的操作。clientState 也是 indexer 的操作对象。

步骤6 Dispatch Event Handler Functions 则为对 handler 的调用,handler 在 main 中已定义:

// examples/workqueue/main.go
cache.ResourceEventHandlerFuncs{
    AddFunc: func(obj interface{}) {
       key, err := cache.MetaNamespaceKeyFunc(obj)
       if err == nil {
          queue.Add(key)
       }
    },
    UpdateFunc: func(old interface{}, new interface{}) {
       key, err := cache.MetaNamespaceKeyFunc(new)
       if err == nil {
          queue.Add(key)
       }
    },
    DeleteFunc: func(obj interface{}) {
       // IndexerInformer uses a delta queue, therefore for deletes we have to use this
       // key function.
       key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
       if err == nil {
          queue.Add(key)
       }
    },
}

步骤7 Enqueue Object Key 则对应 AddFunc 中的 queue.Add。步骤8 Get Key 则在 processNextItem 中体现。

func (c *Controller) processNextItem() bool {
    // Wait until there is a new item in the working queue
    key, quit := c.queue.Get()
    if quit {
       return false
    }
    // Tell the queue that we are done with processing this key. This unblocks the key for other workers
    // This allows safe parallel processing because two pods with the same key are never processed in
    // parallel.
    defer c.queue.Done(key)

    // Invoke the method containing the business logic
    err := c.syncToStdout(key.(string))
}

步骤9 get object for key 在 syncToStdout 中体现,此处可以编写业务逻辑。

func (c *Controller) syncToStdout(key string) error {
    obj, exists, err := c.indexer.GetByKey(key)
    if err != nil {
       klog.Errorf("Fetching object with key %s from store failed with %v", key, err)
       return err
    }

    if !exists {
       // Below we will warm up our cache with a Pod, so that we will see a delete for one pod
       fmt.Printf("Pod %s does not exist anymore\n", key)
    } else {
       // Note that you also have to check the uid if you have a local controlled resource, which
       // is dependent on the actual instance, to detect that a Pod was recreated with the same name
       fmt.Printf("Sync/Add/Update for Pod %s\n", obj.(*v1.Pod).GetName())
    }
    return nil
}

除了 syncToStdout 的正常逻辑,可以看到 handleErr 函数用以处理异常逻辑。

informer 太大了

本文从 client-go 官方提供的 controller 出发,找到了官网架构图各步骤的实现。

然而 informer 还是太大了,比如 indexer 是如何加快查询的,多个相同 informer 是如何处理的(sharedInformer),ListAndWatch 的原理是什么,Resync 是起什么作用的。

这些问题每个拿出来都能讲一天,后续慢慢再补吧,感谢观看。