背景
相信接触过云原生的朋友都写过 kubernetes controller。笔者平时编写 controller 过程中最常用的是 controller-runtime 框架,而 controller-runtime 框架底层便是今天的主角——informer 机制。
因为 informer 机制,以及上层的 controller-runtime 非常庞大,所以笔者只能按照多篇文章来慢慢讲解。本文的重点是以 client-go 中提供的 example 为例,分析官网图中的各个步骤。
Informer 原理
先上个官网图,估计很多读者看到了会很闷逼,为啥会有两个队列,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 函数:
- 初始化 cacheClient 用以后续的 listAndWatch;
- 初始化了 workerqueue(限速队列);
- 创建 indexer 和 informer。indexer 用以缓存 apiserver 的数据,同时通过索引的方式加快 list 效率;informer 则是后续过程的主 Object;
- 将 default 命名空间下的 mypod Pod 加入到 indexer 中(示例,假设之前处理了 indexer);
- 启动 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 方法。
- 调用 informer.Run,启动 informer;
- 启动 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 是起什么作用的。
这些问题每个拿出来都能讲一天,后续慢慢再补吧,感谢观看。