之前介绍过informer的流程,文章在 informer介绍。今天梳理一下他的源码和流程。
一、概念
什么是 Informer
informer 是 client-go 中的核心工具包,informer 其实就是一个带有本地缓存和索引机制的,可以注册 EventHandler 的 client 本地缓存被称为 Store,索引被称为 Index 。使用 informer 的目的是为了减轻 apiserver 数据交互的压力而抽象出来的一个 cache 层,客户端对 apiserver 数据的 读取和监听操作都通过本地 informer进行。
为什么需要 Informer 机制
Kubernetes 各个组件都是通过 REST API 跟 API Server 交互通信的,而如果每次每一个组件都直接跟 API Server 交互去读取/写入到后端的etcd的话,会对 API Server 以及etcd造成非常大的负担。 而 Informer 机制是为了保证各个组件之间通信的实时性、可靠性,并且减缓对 API Server和etcd 的负担。
informer和configch(newSourceApiserverFromLW)区别
之前介绍过一篇configch的文章 configch源码。这篇文章主要是介绍newSourceApiserverFromLW方法,与kubelet交互后进行更新删除等。并且newSourceApiserverFromLW只监听一个api对象(pod)
而informer则监听整个namespace下的api对象。但是informer不会做出动作,只是监听后维护自己的缓存。方便用户去使用最新数据,避免每次都http访问api-server浪费资源
二、注册及接收
大概步骤如下
- 启动informer
- 注册processLoop和reflector
- reflector开始LIST&WATCH。同时processLoop开始循环pop队列数据(步骤三)
- watch到的数据进行对比处理,不存在的存入的queue队列中
注册
2.1.start注册
代码位置:vendor/k8s.io/client-go/informer/factory.go
func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) {
f.lock.Lock()
defer f.lock.Unlock()
if f.shuttingDown {
return
}
for informerType, informer := range f.informers {
if !f.startedInformers[informerType] {
f.wg.Add(1)
informer := informer
go func() {
defer f.wg.Done()
//启动informer
informer.Run(stopCh)
}()
f.startedInformers[informerType] = true
}
}
}
2.2 注册processLoop和reflector
代码位置:vendor/k8s.io/client-go/tools/cache/controller.go
//完成一些初始化,总要的是下面两个函数
//启动监听
wg.StartWithChannel(stopCh, r.Run)
//这里是循环delta FIFO的queue,然后pop出来最新的
wait.Until(c.processLoop, time.Second, stopCh)
2.3 reflector开始LIST&WATCH
代码位置:vendor/k8s.io/client-go/tools/cache/reflector.go
这里开始LIST&WATCH
wait.BackoffUntil(func() {
if err := r.ListAndWatch(stopCh); err != nil {
r.watchErrorHandler(r, err)
}
}, r.backoffManager, true, stopCh)
....省略
//开始watch
return r.watch(w, stopCh, resyncerrc)
...省略
//watch函数开始
err = watchHandler(start, w, r.store, r.expectedType, r.expectedGVK, r.name, r.typeDescription, r.setLastSyncResourceVersion, nil, r.clock, resyncerrc, stopCh)
2.4 watch处理数据
还是上面reflector文件,开始处理接收到的最新watch的信息
switch event.Type {
case watch.Added:
err := store.Add(event.Object)
if err != nil {
utilruntime.HandleError(fmt.Errorf("%s: unable to add watch event object (%#v) to store: %v", name, event.Object, err))
}
case watch.Modified:
err := store.Update(event.Object)
if err != nil {
utilruntime.HandleError(fmt.Errorf("%s: unable to update watch event object (%#v) to store: %v", name, event.Object, err))
}
case watch.Deleted:
err := store.Delete(event.Object)
if err != nil {
utilruntime.HandleError(fmt.Errorf("%s: unable to delete watch event object (%#v) from store: %v", name, event.Object, err))
}
case watch.Bookmark:
if _, ok := meta.GetAnnotations()["k8s.io/initial-events-end"]; ok {
if exitOnInitialEventsEndBookmark != nil {
*exitOnInitialEventsEndBookmark = true
}
}
default:
utilruntime.HandleError(fmt.Errorf("%s: unable to understand watch event %#v", name, event))
}
代码位置:vendor/k8s.io/client-go/tools/cache/delta_fifo.go
进入到更新。这里通过queueActionLocked函数验证验证一下obj是否存在,不存在则推倒queue队列中
func (f *DeltaFIFO) Update(obj interface{}) error {
f.lock.Lock()
defer f.lock.Unlock()
f.populated = true
return f.queueActionLocked(Updated, obj)
}
...省略部分代码
//queueActionLocked中就是获取到item映射出来的这个obj信息(items中存的数据和queue数据是一个。等到pop的时候这两个数据都会删掉),如果不存在则推送到queue中,如果存在则替换。
func (f *DeltaFIFO) queueActionLocked(actionType DeltaType, obj interface{}) error {
id, err := f.KeyOf(obj)
if err != nil {
return KeyError{obj, err}
}
//根据id查到映射的api对象,然后推倒newDeltas中。如果是新的,则不进行对比,如果是老的(2个deltas)则进行去重。dedupDeltas里的逻辑就是如果是删除的话只保留一个,如果是添加则直接返回
oldDeltas := f.items[id]
newDeltas := append(oldDeltas, Delta{actionType, obj})
newDeltas = dedupDeltas(newDeltas)
//这里百分百会大于0,不会小于0
if len(newDeltas) > 0 {
if _, exists := f.items[id]; !exists {
f.queue = append(f.queue, id)
}
f.items[id] = newDeltas
//让wait的cond开始工作
f.cond.Broadcast()
} else {
if oldDeltas == nil {
klog.Errorf("Impossible dedupDeltas for id=%q: oldDeltas=%#+v, obj=%#+v; ignoring", id, oldDeltas, obj)
return nil
}
klog.Errorf("Impossible dedupDeltas for id=%q: oldDeltas=%#+v, obj=%#+v; breaking invariant by storing empty Deltas", id, oldDeltas, obj)
f.items[id] = newDeltas
return fmt.Errorf("Impossible dedupDeltas for id=%q: oldDeltas=%#+v, obj=%#+v; broke DeltaFIFO invariant by storing empty Deltas", id, oldDeltas, obj)
}
return nil
到这里,接收的工作就完成了,已经将最新的数据推送到了queue中,下面就是开始处理这个queue了
三、弹出队列处理
- 开启queue队列的循环接收
- 获取到最新队列信息开始处理
- 更新本地indexer缓存
- 分发给监听informer的用户去触发回调函数
- 如果用户定义的回调能够返回ErrRequeue类型错误,则会把这个资源信息重新推入到队列然后重新发送给用户
3.1 开始循环接收数据
func (c *controller) processLoop() {
for {
//无限循环处理queue数据
obj, err := c.config.Queue.Pop(PopProcessFunc(c.config.Process))
}
}
3.2 这里面也是个for循环,如果队列大于0,则读取出来通过process处理,否则等待队列有新数据加入
代码位置 vendor/k8s.io/client-go/tools/cache/delta_fifo.go
for {
for len(f.queue) == 0 {
if f.closed {
return nil, ErrFIFOClosed
}
f.cond.Wait()
}
id := f.queue[0]
f.queue = f.queue[1:]
depth := len(f.queue)
//减少队列长度,四、replace中会介绍这个。这个等于0代表没有需要同步的了
if f.initialPopulationCount > 0 {
f.initialPopulationCount--
}
item, ok := f.items[id]
delete(f.items, id)
//进入这里
err := process(item, isInInitialList)
//如果用户定义的回调能够返回ErrRequeue类型错误,则会把这个资源信息重新推入到队列然后重新发送给用户
if e, ok := err.(ErrRequeue); ok {
f.addIfNotPresent(id, item)
err = e.Err
}
}
3.3 注册的回调。判断映射是否存在,存在的话则进行处理
代码位置 vendor/k8s.io/client-go/tools/cache/shared_informer.go
func (s *sharedIndexInformer) HandleDeltas(obj interface{}, isInInitialList bool) error {
s.blockDeltas.Lock()
defer s.blockDeltas.Unlock()
if deltas, ok := obj.(Deltas); ok {
//如果映射存在,则进行处理
return processDeltas(s, s.indexer, deltas, isInInitialList)
}
return errors.New("object given as Process argument is not Deltas")
}
3.4 处理本地缓存和回调
代码位置:vendor/k8s.io/client-go/tools/cache/controller.go
func processDeltas(
handler ResourceEventHandler,
clientState Store,
deltas Deltas,
isInInitialList bool,
) error {
for _, d := range deltas {
obj := d.Object
switch d.Type {
//如果是添加、同步、替换、更新,则进入这里
case Sync, Replaced, Added, Updated:
if old, exists, err := clientState.Get(obj); err == nil && exists {
//更新indexer的本地缓存(步骤3.5)
if err := clientState.Update(obj); err != nil {
return err
}
//处理回调(步骤3.6)
handler.OnUpdate(old, obj)
} else {
if err := clientState.Add(obj); err != nil {
return err
}
handler.OnAdd(obj, isInInitialList)
}
case Deleted:
if err := clientState.Delete(obj); err != nil {
return err
}
handler.OnDelete(obj)
}
}
return nil
}
3.5 更新本地indexer(Storage)缓存
代码位置:vendor/k8s.io/client-go/tools/cache/controller.go
func (c *cache) Update(obj interface{}) error {
key, err := c.keyFunc(obj)
if err != nil {
return KeyError{obj, err}
}
c.cacheStorage.Update(key, obj)
return nil
}
3.6处理回调
func (s *sharedIndexInformer) OnUpdate(old, new interface{}) {
isSync := false
if accessor, err := meta.Accessor(new); err == nil {
if oldAccessor, err := meta.Accessor(old); err == nil {
// Events that didn't change resourceVersion are treated as resync events
// and only propagated to listeners that requested resync
isSync = accessor.GetResourceVersion() == oldAccessor.GetResourceVersion()
}
}
s.cacheMutationDetector.AddObject(new)
//这个是重点,分发给监听者
s.processor.distribute(updateNotification{oldObj: old, newObj: new}, isSync)
}
3.7 分发给监听者
这个图介绍了监听者如何处理队列顺序(3.8步骤讲解)
- 把消息分发给监听者
- 接收消息推送到add管道
func (p *sharedProcessor) distribute(obj interface{}, sync bool) {
p.listenersLock.RLock()
defer p.listenersLock.RUnlock()
//p.listeners代表有几个监听者。这里循环所有监听者,把最新信息发送给监听者。
for listener, isSyncing := range p.listeners {
switch {
case !sync:
listener.add(obj)
case isSyncing:
listener.add(obj)
default:
}
}
}
func (p *processorListener) add(notification interface{}) {
if a, ok := notification.(addNotification); ok && a.isInInitialList {
p.syncTracker.Start()
}
p.addCh <- notification //推送到add管道
}
3.8监听者的处理 (最上面图中5-6中间的处理)
- 为每个监听者创建run和pop函数
- run函数负责循环nextCh管道,如果有数据就触发handler
- pop函数分别case1和case2
- case1负责监听nextCh是否阻塞(阻塞说明有数据处理在推送)如果没阻塞,拿到等待推送的值,如果没有值置空nextCh变量
- case2负责接收addCh管道的值,如果有数据,发现 notification为空(case1 是关闭的),重新启动case1、否则写入到阻塞队列中
func (p *sharedProcessor) run(stopCh <-chan struct{}) {
func() {
p.listenersLock.RLock()
defer p.listenersLock.RUnlock()
for listener := range p.listeners {
p.wg.Start(listener.run)//每个监听者创建一个run
p.wg.Start(listener.pop)//每个监听者创建一个pop
}
p.listenersStarted = true
}()
}
func (p *processorListener) run() {
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))
}
}
close(stopCh)
}, 1*time.Second, stopCh)
}
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://如果nextCh没阻塞中,则进行推送
var ok bool
//从队列读取一个值,下次推送到nextCh中
notification, ok = p.pendingNotifications.ReadOne()
if !ok {
//如果没值了,取消nextCh变量
nextCh = nil
}
//从addCh管道拿到最新值
case notificationToAdd, ok := <-p.addCh:
if !ok {
return
}
if notification == nil {
//如果没有等待的,重新定义nextCh变量进行推送
notification = notificationToAdd
nextCh = p.nextCh
} else {
//如果nextCh阻塞中,则写入到队列等待
p.pendingNotifications.WriteOne(notificationToAdd)
}
}
}
}
到这里就处理结束了。如果想要使用监听者,需要用户自己建立informer进行监听。然后添加注册函数,就可以接收了
四、replace和resync
replace是用来操作来更新缓存中的对象,以确保对象完全更新,而不仅仅是部分更新。在第一次运行时会启动
resync是每隔 10 分钟进行一次重新同步操作,即调用 List 方法从 API Server 中拉取完整的资源列表。
4.1 replace
- 先生成一个set集合用来存放数据
- 遍历LIST传过来的所有list数据并生成key,同时添加到set集合中
- 对队列中的对象做删除检测。这里是避免在与apiserver断开连接时错过了删除信息的意外而重新检测
- 如果有本地缓存库,则进行与本地缓存的比对,如果本地缓存有数据,但是list没有,则删除这个数据
- initialPopulationCount计算一下总量和有问题需要删除的总量(这里是为了以后判断是否sync成功)
func (f *DeltaFIFO) Replace(list []interface{}, _ string) error {
f.lock.Lock()
defer f.lock.Unlock()
//先生成一个set集合用来存放数据
keys := make(sets.String, len(list))
action := Sync
if f.emitDeltaTypeReplaced {
action = Replaced
}
for _, item := range list {
key, err := f.KeyOf(item)
if err != nil {
return KeyError{item, err}
}
keys.Insert(key)
if err := f.queueActionLocked(action, item); err != nil {
return fmt.Errorf("couldn't enqueue object: %v", err)
}
}
//对队列中的对象做删除检测。这里是避免在与apiserver断开连接时错过了删除信息的意外而重新检测
queuedDeletions := 0
for k, oldItem := range f.items {
if keys.Has(k) {
continue
}
if n := oldItem.Newest(); n != nil {
deletedObj = n.Object
if d, ok := deletedObj.(DeletedFinalStateUnknown); ok {
deletedObj = d.Obj
}
}
queuedDeletions++
if err := f.queueActionLocked(Deleted, DeletedFinalStateUnknown{k, deletedObj}); err != nil {
return err
}
}
if f.knownObjects != nil {
//如果有本地缓存库,则进行与本地缓存的比对,如果本地缓存有数据,但是list没有,则删除这个数据
knownKeys := f.knownObjects.ListKeys()
for _, k := range knownKeys {
if keys.Has(k) {
continue
}
if len(f.items[k]) > 0 {
continue
}
deletedObj, exists, err := f.knownObjects.GetByKey(k)
if err != nil {
deletedObj = nil
} else if !exists {
deletedObj = nil
}
queuedDeletions++
if err := f.queueActionLocked(Deleted, DeletedFinalStateUnknown{k, deletedObj}); err != nil {
return err
}
}
}
if !f.populated {
f.populated = true
f.initialPopulationCount = keys.Len() + queuedDeletions
}
return nil
}
4.2 resync
默认每10分钟同步一次。这个时间间隔可以通过 ResyncPeriodFunc 函数进行调整
- 遍历所有数据
- 如果这个数据在items映射里存在,则不管这个信息(因为如果items中存在,则代表是api-server发过来的最新的还没处理,这里需要跳过而不是覆盖)
- 如果不是则同步这个数据。其实就是把本地缓存库的数据再次放入队列中,重新消费做一次update的动作
func (f *DeltaFIFO) Resync() error {
f.lock.Lock()
defer f.lock.Unlock()
if f.knownObjects == nil {
return nil
}
//获得所有缓存库数据
keys := f.knownObjects.ListKeys()
for _, k := range keys {
//遍历所有数据去验证
if err := f.syncKeyLocked(k); err != nil {
return err
}
}
return nil
}
func (f *DeltaFIFO) syncKeyLocked(key string) error {
obj, exists, err := f.knownObjects.GetByKey(key)
if err != nil {
klog.Errorf("Unexpected error %v during lookup of key %v, unable to queue object for sync", err, key)
return nil
} else if !exists {
klog.Infof("Key %v does not exist in known objects store, unable to queue object for sync", key)
return nil
}
//获得这个数据的id
id, err := f.KeyOf(obj)
if err != nil {
return KeyError{obj, err}
}
//如果这个数据在items映射里存在,则不管这个信息(因为如果items中存在,则代表是api-server发过来的最新的还没处理,这里需要跳过而不是覆盖)
if len(f.items[id]) > 0 {
return nil
}
//如果不是则同步这个数据。其实就是把本地缓存库的数据再次放入队列中,重新消费做一次update的动作
if err := f.queueActionLocked(Sync, obj); err != nil {
return fmt.Errorf("couldn't queue object: %v", err)
}
return nil
}
对于重新把本地缓存库的数据再次放入队列中,重新消费做一次update的动作可能有些疑问,这里举个例子。
如果本地缓存中a=1,然后出发了用户的add操作。但是用户有一些一次性的特殊情况需要处理把a=1改成了a=2。等过了10分钟resync同步了,这时候就会把a=1再次推到用户的add操作,用户就可以继续使用a=1了
但是大部分情况是为了用户端的回调失败或未执行时,确保再次触发统一操作。