1. 概述
前一篇文章介绍了workqueue中的普通队列FIFO,同时为了便于理解延时队列,也专门用一篇文章介绍了go语言中堆和优先队列的实现。接下来我们就具体学习下workqueue中的第二种队列: 延时队列
client-go 的 util/workqueue 包里主要有三个队列,分别是普通队列Queue,延时队列DelayingQueue,限速队列RateLimitingQueue,后一个队列以前一个队列的实现为基础,层层添加新功能。
延迟队列本质也是一个队列,是在FIFO普通队列的基础之上,扩展了新的功能,可以对加入队列的数据进行延时处理。
老规矩,我们不仅要学习延时队列的原理机制,同时也要了解源码实现。所以先找到client-go中的延迟队列的源码: k8s.io/client-go/util/workqueue/delaying_queue.go
2. 延时队列的实现与源码解读
2.1 延迟队列的接口定义
DelayingInterface继承了FIFO普通队列的功能,只是在此基础之上实现了AddAfter方法,该方法的作用是等待一段时间后,再加入元素到工作队列。
// DelayingInterface is an Interface that can Add an item at a later time. This makes it easier to
// requeue items after failures without ending up in a hot-loop.
// DelayingInterface具有延迟一段时间add一个元素的能力
type DelayingInterface interface {
// 继承了FIFO普通队列的功能
Interface
// AddAfter adds an item to the workqueue after the indicated duration has passed
// AddAfter的作用是先把元素加入缓存中,等待一段时间后,再把元素移到到工作队列
AddAfter(item interface{}, duration time.Duration)
}
普通队列FIFO的接口的定义如下,前一篇文章已经介绍过,这里不再赘述。
// 普通队列的接口定义
type Interface interface {
Add(item interface{}) // 添加元素到队列
Len() int // 返回队列长度
Get() (item interface{}, shutdown bool) // 从队列取出一个元素(会从队列弹出)
Done(item interface{}) // 标记元素已经处理
ShutDown() // 关闭队列
ShuttingDown() bool // 判断队列是否正在关闭中
}
2.2 delayingType结构体
delayingType结构体是延迟队列接口的具体实现,接下来我们分析delayingType类型的定义。
// delayingType wraps an Interface and provides delayed re-enquing
// delayingType 封装了继承了Interface接口,并提供了延迟重新入队列的能力
type delayingType struct {
// 继承普通队列的方法
Interface
// clock tracks time for delayed firing
// 时钟用于跟踪延迟触发的时间
clock clock.Clock
// stopCh lets us signal a shutdown to the waiting loop
// stopCh 发送一个信号让waiting loop的循环停止
stopCh chan struct{}
// stopOnce guarantees we only signal shutdown a single time
// stopOnce 可以确保关停信号只发送一次
stopOnce sync.Once
// heartbeat ensures we wait no more than maxWait before firing
// 用于确保触发前,等待的时间不长于maxWait
heartbeat clock.Ticker
// waitingForAddCh is a buffered channel that feeds waitingForAdd
// waitingForAddCh 是一个buufer通道, 元素添加到通道前需要先封装成waitFor结构体
waitingForAddCh chan *waitFor
// metrics counts the number of retries
// 记录重试的次数
metrics retryMetrics
}
// waitFor 持有要添加的数据和应该添加的时间
type waitFor struct {
data t // 添加的元素数据
readyAt time.Time // 在什么时间添加到队列中,借助readyAt用于判断什么时候把元素加入队列
index int // 优先级队列(heap)中的索引
}
2.3 延迟队列的构造函数
从DelayingInterface的构造函数源码,可以看到初始化的delayingType对象中,包括了普通队列Interface、waitingForAddCh缓冲通道、计时器clock,以及用于控制延迟队列停止的stopCh等。
// NewDelayingQueueWithCustomClock constructs a new named workqueue
// with ability to inject real or fake clock for testing purposes
func NewDelayingQueueWithCustomClock(clock clock.Clock, name string) DelayingInterface {
ret := &delayingType{
Interface: NewNamed(name), // 继承workqueue的普通队列
clock: clock,
heartbeat: clock.NewTicker(maxWait),
stopCh: make(chan struct{}),
waitingForAddCh: make(chan *waitFor, 1000), // 设置缓冲通道的大小为1000
metrics: newRetryMetrics(name),
}
// 启动一个协程形式的循环任务,循环任务的作用是判断缓冲通道waitingForAddCh的元素的延时是否到期,如果到期就放入普通队列FIFO,如果没到期就放入优先队列PQ.
// waitLoop()同时也会轮询判断优先级队列PQ的队首元素的延时是否到期,如果到期就将对象取出放入FIFO队列里面去
go ret.waitingLoop()
return ret
}
在延时队列的实现 delayingType 结构体中,包含一个普通队列 Interface 的实现,然后最重要的一个属性就是 waitingForAddCh
,这是一个 buffered channel。当用户执行AddAfter(item,duration)
后,item与duration将被封装成 waitFor
放到缓冲通道中。意思就是当到了指定的时间后就将元素添加到FIFO普通队列中去进行处理,还没有到时间的话就放到优先级队列(waitForPriorityQueue
)中去。
为了便于理解,这里先展示延迟队列的机制图,后面再逐步分析这个机制的源码实现。
为了实现这个机制,源码中启动一个协程循环任务waitingloop;同时借助了三个数据结构,一个是缓冲通道waitingForAddCh、一个是优先级队列PQ、一个是普通队列FIFO。
-
缓冲通道:
waitingForAddCh
本质是一个channel,size大小默认是1000,当生产者调用AddWait(item,duration)后,item会被组装为waitFor结构。如果waitingForAddCh存放的元素不超过1000,waitFor对象会直接放入,否则将阻塞等待。 -
优先级队列:
waitForPriorityQueue
是利用堆实现的一个优先级队列。队列内部按延时duration进行排序,延时越小越优先被弹出(pop)。waitingloop会循环判断优先级队列队首元素的延时是否到期,如果到期就取出并入到FIFO,否则就继续循环等待。优先级队列的作用很关键,能够让我们取出队列最小元素的时间复杂度仅是O(1)。需要充分理解其实现机制,请参考我之前的文章:《go语言源码解读之数据结构堆》
-
普通队列:
FIFO
是一种普通的先进先出队列。但wokqueue中的FIFO同时又具有”有序“、”去重“、”并发性“、”标记“和"metric"等能力。如需了解具体实现,请参考我前一篇文章的介绍《informer中的WorkQueue机制的实现分析与源码解读(1)》 -
循环处理任务:
waitingloop()
是实现延时功能最重要的方法。通俗的说waitingloop()
,一方面循环处理缓冲通道里面的元素: 如果元素延时到期就移到FIFO,否则就移到PQ;另一方面也循环处理优先级队列PQ里面的元素,如果元素的延时到期就移到FIFO。
2.4 优先级队列的定义与实现
waitForPriorityQueue
(PQ) 是一个有序的 waitFor 的集合。为了实现优先级队列的堆性质,源码中利用了go语言标准库container/heap.go提供的堆接口,waitForPriorityQueue
需要实现五个方法: Len(),Less(),Swap(),Push(),Pop().这里不再赘述,请见我之前的文章:《go语言源码解读之数据结构堆》
// waitForPriorityQueue implements a priority queue for waitFor items.
//
// waitForPriorityQueue implements heap.Interface. The item occurring next in
// time (i.e., the item with the smallest readyAt) is at the root (index 0).
// Peek returns this minimum item at index 0. Pop returns the minimum item after
// it has been removed from the queue and placed at index Len()-1 by
// container/heap. Push adds an item at index Len(), and container/heap
// percolates it into the correct location.
type waitForPriorityQueue []*waitFor
func (pq waitForPriorityQueue) Len() int {
return len(pq)
}
func (pq waitForPriorityQueue) Less(i, j int) bool {
return pq[i].readyAt.Before(pq[j].readyAt)
}
func (pq waitForPriorityQueue) Swap(i, j int) {
pq[i], pq[j] = pq[j], pq[i]
pq[i].index = i
pq[j].index = j
}
// Push adds an item to the queue. Push should not be called directly; instead,
// use `heap.Push`.
func (pq *waitForPriorityQueue) Push(x interface{}) {
n := len(*pq)
item := x.(*waitFor)
item.index = n
*pq = append(*pq, item)
}
// Pop removes an item from the queue. Pop should not be called directly;
// instead, use `heap.Pop`.
func (pq *waitForPriorityQueue) Pop() interface{} {
n := len(*pq)
item := (*pq)[n-1]
item.index = -1
*pq = (*pq)[0:(n - 1)]
return item
}
// Peek returns the item at the beginning of the queue, without removing the
// item or otherwise mutating the queue. It is safe to call directly.
// Peek()用于返回队首元素的值,注意不会从队列中移除。 利用这个函数,waitingloop可以检查队首元素是否延时到期
func (pq waitForPriorityQueue) Peek() interface{} {
return pq[0]
}
从上面示意图看出,流程主要有AddAfter()向延时队列添加元素,waitingloop延时队列处理元素、Get()函数从队列取出元素。下面就让我们展开分析。
2.5 AddAfter()方法
AddAfter(item, duration)指定了具体加入元素item,以及要延时处理的具体时间duration。 通俗的说,就是我希望item元素,先放入缓存中,等待duration时间之后,再移到FIFO被业务拿去处理。 接下来我们看看AddAfter()的源码
// AddAfter adds the given item to the work queue after the given delay
// AddAfter 指定了具体加入元素item,已经要延时处理的具体时间duration。
func (q *delayingType) AddAfter(item interface{}, duration time.Duration) {
// don't add if we're already shutting down
// 如果延时队列处于关闭中就直接退出函数
if q.ShuttingDown() {
return
}
// 标记没重试的次数
q.metrics.retry()
// immediately add things with no delay
// 如果设置的延时duration小于或等于0,就直接加入到FIFO普通队里中去
if duration <= 0 {
q.Add(item)
return
}
select {
case <-q.stopCh:
// unblock if ShutDown() is called
// 把item与duration组装成waitFor结构,并加入到缓冲通道waitingForAddCh
case q.waitingForAddCh <- &waitFor{data: item, readyAt: q.clock.Now().Add(duration)}:
}
}
2.6 waitingLoop()方法
当waitFor对象被AddAfter()方法加入到waitingForAddCh通道之后,就轮到waitingLoop()上场处理了。上文已经简单说明了waitingloop()的主要作用有2个:
- 不断轮询缓冲通道``waitingForAddCh
判断每一个元素,看是否到期了,如果到期就放入普通队列
FIFO,如果没到期就放入优先队列
waitForPriorityQueue`. - 同时也会轮询判断优先级队列
waitForPriorityQueue
的队首元素的延时是否到期,如果到期就将对象取出放入FIFO队列里面去.
接下来我们看waitingLoop()
的源码实现
// waitingLoop runs until the workqueue is shutdown and keeps a check on the list of items to be added.
func (q *delayingType) waitingLoop() {
defer utilruntime.HandleCrash()
// Make a placeholder channel to use when there are no items in our list
// 创建一个占位符通道,如果缓冲通道与优先级队列中都没有元素的时候,利用never变量实现长时间等待(即case <-never:)
never := make(<-chan time.Time)
// Make a timer that expires when the item at the head of the waiting queue is ready
// 构造一个定时器,当等待队列头部的元素准备好时,该定时器就会失效
var nextReadyAtTimer clock.Timer
// 用堆实现一个按延时排序的优先级队列,是一个小顶堆。队首元素为延时最小的元素
waitingForQueue := &waitForPriorityQueue{}
heap.Init(waitingForQueue)
// 用来避免元素重复添加,如果重复添加了就只更新时间
waitingEntryByData := map[t]*waitFor{}
// 启动一个无限循环任务
for {
// 如果延时队列处于关闭中,则退出循环任务
if q.Interface.ShuttingDown() {
return
}
// 获取当期时间
now := q.clock.Now()
// Add ready entries
// 内部启动再启动一个循环任务,这个for循环,会将waitingForQueue的已经到期的元素都放到FIFO中去
// 剩余没到期的元素,将会留着waitingForQueue里面,等待下次处理。
for waitingForQueue.Len() > 0 {
// 判断队首的延时是否到期,如果没有就退出
entry := waitingForQueue.Peek().(*waitFor)
if entry.readyAt.After(now) {
break
}
// 如果队首到期就从优先级队列移到FIFO
entry = heap.Pop(waitingForQueue).(*waitFor)
q.Add(entry.data)
delete(waitingEntryByData, entry.data)
}
// 如果执行完上面的for循环,到这里时表示waitingForQueue中已经到期的元素已经都被处理完成(移到FIFO),留下的
// 都是未到期的元素
// Set up a wait for the first item's readyAt (if one exists)
// 设置一个占位符通道
nextReadyAt := never
// 如果优先队列中还有元素,那就用第一个元素指定的时间减去当前时间作为等待时间
// 因为优先队列是用时间排序的,后面的元素需要等待的时间更长,所以先处理排序靠前面的元素
if waitingForQueue.Len() > 0 {
if nextReadyAtTimer != nil {
nextReadyAtTimer.Stop()
}
// entry的值为队首元素
entry := waitingForQueue.Peek().(*waitFor)
// 第一个元素的时间减去当前时间作为等待时间
nextReadyAtTimer = q.clock.NewTimer(entry.readyAt.Sub(now))
nextReadyAt = nextReadyAtTimer.C()
}
select {
case <-q.stopCh:
return
// 定时器,每过一段时间没有任何数据,那就再执行一次大循环
case <-q.heartbeat.C():
// continue the loop, which will add ready items
// 如果优先级队列中还有元素,则nextReadyAt的值nextReadyAtTimer.C(),等待队首元素延时到期;否则nextReadyAt的值never,会阻塞在这里等待一直等待
case <-nextReadyAt:
// continue the loop, which will add ready items
// 如果 waitingForAddCh 有元素,就取出一个处理
case waitEntry := <-q.waitingForAddCh:
// 如果从 waitingForAddCh 取出的元素的延时没到期,就放入 优先级队列
if waitEntry.readyAt.After(q.clock.Now()) {
insert(waitingForQueue, waitingEntryByData, waitEntry)
} else {
// 如果从 waitingForAddCh 取出的元素的延时已经到期,就放入 FIFO
q.Add(waitEntry.data)
}
// 下面的代码的作用是依次把 waitingForAddCh 里面的元素全部取出来.延时到期的就加入FIFO,没到期的就加入PQ
// 如果没有数据了就直接退出
drained := false
for !drained {
select {
case waitEntry := <-q.waitingForAddCh:
if waitEntry.readyAt.After(q.clock.Now()) {
insert(waitingForQueue, waitingEntryByData, waitEntry)
} else {
q.Add(waitEntry.data)
}
default:
drained = true
}
}
}
}
}
// insert adds the entry to the priority queue, or updates the readyAt if it already exists in the queue
// 插入元素到FIFO队列,如果已经存在了则更新时间
func insert(q *waitForPriorityQueue, knownEntries map[t]*waitFor, entry *waitFor) {
// if the entry already exists, update the time only if it would cause the item to be queued sooner
existing, exists := knownEntries[entry.data]
if exists {
// 元素存在,就比较谁的时间靠后就用谁的时间
if existing.readyAt.After(entry.readyAt) {
existing.readyAt = entry.readyAt
// 如果元素的延时时间变了,这需要重新调整优先级队列
heap.Fix(q, existing.index)
}
return
}
// 把元素放入FIFO队列中
heap.Push(q, entry)
// 并记录在上面的 map 里面,用于判断是否存在
knownEntries[entry.data] = entry
}
经过分析waitingLoop()的源码,发现过程稍微有点复杂,为了便于理解,做了一个示意图。
waitingloop()中实现了2个for循环,为了表示方便,分别称为外层大循环,内部小循环。
-
外层大循环的作用:轮询判断缓冲通道
waitingForAddCh
的每一个元素,看是否到期了,如果到期就放入普通队列FIFO
,如果没到期就放入优先队列PQ
. -
内部小循环的作用: 轮询判断优先级队列PQ的队首元素的延时是否到期,如果到期就将对象取出放入FIFO队列里面去.
3. 总结
-
延时队列的实现,是在workqueue的普通队列FIFO的基础上实现了对元素延时加入工作队列FIFO的能力。Get()方法从只能从FIFO队列获取元素,不能从缓冲通道与优先级队列的里面获取元素。
-
延时队列的实现,借助了三个数据结构,一个是缓冲通道waitingForAddCh、一个是优先级队列PQ、一个是普通队列FIFO。前2个结构可以看出是缓存,FIFO存储的才是可以被业务处理的元素。
-
延时队列的源码实现,最重要的是AddAfter()方法与waitingloop()。AddAfter(item,duration)方法指定了元素以及需要延时多久后才能被加入到工作队列FIFO。waitingloop()的作用是判断缓存中的元素(这里说的缓存包括缓冲通道和PQ),是否到期,如果到期就移到FIFO。
-
进过对源码抽丝剥茧之后,最后可以归类并简化出延时队列的机制如下示意图。
以上就是对于延时队列的个人愚见,经供参考,谢谢阅览。