在开发controller时,Workqueue的使用是必需的,一般使用方式是在回调函数中,将数据入队到workqueue中,然后在起多个worker,去get queue中的数据进行处理。
本文结合workqueue源码,对workqueue的设计实现进行详细解析。
1、workqueue的去重和标记功能,利用set实现。
workqueue中怎么实现set:利用golang
type empty struct{} type t interface{} type set map[t]empty
其中empty是空类型, 因为sizeof(struct{})=0
t是interface,泛型。表示支持多种类型。
set用map实现,其中key为t,也就是泛型。empty为空。因为set的本质就是去重的数组,因此利用map的key不重复的特性实现。而value就是空即可。
2、workqueue的实现
workqueue支持三种队列,并提供三种接口,不同队列实现可应对不同的使用场景,分别介绍如下。
inferface:FIFO队列接口,先进先出队列,并支持去重机制
delayinginferface:延迟队列接口,基于interface接口封装,延迟一段时间再入队。
ratelimitingInterface:限速队列接口,基于delayinginferface封装,支持入队时进行速率限制。
FIFO队列
k8s.io/client-go/util/workqueue/queue.go
type Interface interface { Add(item interface{}) Len() int Get() (item interface{}, shutdown bool) Done(item interface{}) ShutDown() ShuttingDown() bool }
方法说明如下
Add:给队列添加元素
Len:返回当前queue的长度
Get:获得queue头部元素
Done:标记队列的元素已经被业务controller逻辑处理完毕
ShutDown:关闭队列
ShuttingDown:查询队列是否正在关闭
FiFO的结构体如下
type Type struct { // queue defines the order in which we will work on items. Every // element of queue should be in the dirty set and not in the // processing set. queue []t // dirty defines all of the items that need to be processed. dirty set // Things that are currently being processed are in the processing set. // These things may be simultaneously in the dirty set. When we finish // processing something and remove it from this set, we'll check if // it's in the dirty set, and if so, add it to the queue. processing set cond *sync.Cond shuttingDown bool metrics queueMetrics unfinishedWorkUpdatePeriod time.Duration clock clock.Clock }
最主要的字段就是queue、dirty和processing。
其中queue字段是实际存储元素的地方,是slice结构,用于保证元素有序。
dirty字段非常关键,保证去重,也保证在处理元素完毕之前,被添加了多次,也会只被处理一次。
processing字段用于标记机制,标记一个元素是否被处理。
FIFO的存储过程如下所示
通过Add方法,向FIFO队列插入1、2、3这三个元素,此时队列中的queue和dirty字段都存有这三个元素,processsing字段为空。然后通过get方法获取最先进入的元素(也就是1),此时queue和dirty存有2、3元素,1放入到processing中,表示该元素正在被处理,当处理完后,通过done方法标记该元素已经被处理完成。此时processing 中的1元素就会被删除。
那么workqueue的去重功能到底是干什么的,又是怎么实现的呢?
先看下Add的方法
func (q *Type) Add(item interface{}) { q.cond.L.Lock() defer q.cond.L.Unlock() if q.shuttingDown { return } //先判断该items是否在dirty里,如果在则说明该对象已经有在排队了,且尚未被处理。则不加入,直接返回。这就是去重了。 if q.dirty.has(item) { return } q.metrics.add(item) //如果不在dirty里,则加入到dirty里。 q.dirty.insert(item) if q.processing.has(item) { //判断是否在processing里,也就是是否正在被处理,如果是,则说明正在处理的元素是脏旧数据,因为处理过程可能已经get了,是旧数据。而item是最新数据。这时返回,不要入到queue里 return } //否则,则说明该元素没有正在被处理的,是干净的元素,则加入到queue中。 q.queue = append(q.queue, item) q.cond.Signal() }
可以看到,dirty的确是作为去重的屏障了。
但是当dirty没有该数据,而processing正在有数据被处理,则说明正在处理的元素是旧的脏数据。
因为我们controller里,处理时,从queue get key后,一般会调用client-go去get该key的对象数据。如果在这个get方法后,这个对象变化了,重新Add进来,这时Add的item就是新数据了。正在处理的数据就是脏数据了。这个时候,数据不会存入queue中,但已经在dirty中存了,这也是dirty的作用。
processing的作用就是来保证只有一个数据会在queue里处理,这是第二处去重。为什么这样做呢?
一般,我们会起多个worker(协程)去get queue数据处理。那么如果同时两个worker get queue的数据,然后同时处理一新一旧两个数据,就可能发生冲突。
当旧数据处理完成后,通过Done方法,标记处理完成,如果dirty字段存在该元素,就会把该元素再追加到queue的尾部,再进行处理,因为这次的元素才是新数据。
func (q *Type) Done(item interface{}) { q.cond.L.Lock() defer q.cond.L.Unlock() q.metrics.done(item) //done后,把元素从processing移除 q.processing.delete(item) //判断dirty是否还有该元素,有的话,则入queue再进行处理 if q.dirty.has(item) { q.queue = append(q.queue, item) q.cond.Signal() } }