informer中的WorkQueue机制的实现分析与源码解读(2)

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)中去。

为了便于理解,这里先展示延迟队列的机制图,后面再逐步分析这个机制的源码实现。
delayQueue

为了实现这个机制,源码中启动一个协程循环任务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. 总结

  1. 延时队列的实现,是在workqueue的普通队列FIFO的基础上实现了对元素延时加入工作队列FIFO的能力。Get()方法从只能从FIFO队列获取元素,不能从缓冲通道与优先级队列的里面获取元素。

  2. 延时队列的实现,借助了三个数据结构,一个是缓冲通道waitingForAddCh、一个是优先级队列PQ、一个是普通队列FIFO。前2个结构可以看出是缓存,FIFO存储的才是可以被业务处理的元素。

  3. 延时队列的源码实现,最重要的是AddAfter()方法与waitingloop()。AddAfter(item,duration)方法指定了元素以及需要延时多久后才能被加入到工作队列FIFO。waitingloop()的作用是判断缓存中的元素(这里说的缓存包括缓冲通道和PQ),是否到期,如果到期就移到FIFO。
    在这里插入图片描述

  4. 进过对源码抽丝剥茧之后,最后可以归类并简化出延时队列的机制如下示意图。

以上就是对于延时队列的个人愚见,经供参考,谢谢阅览。

  • 16
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值