6. client-go 中DelayingQueue的源码分析

本文是胡涛大佬所出版的《Kubernetes Operator 进阶开发》,强烈推荐各位阅读原书,本文仅仅留作个人心得,如有侵权立马删除。


1 源码解析

让我们首先看看 DelayingQueue 的定义:

go 复制代码
type DelayingInterface interface {
	Interface
	// AddAfter adds an item to the workqueue after the indicated duration has passed
	AddAfter(item interface{}, duration time.Duration)
}

也就是在 Interface 之上多加了一个 AddAfter 的方法用来处理延迟。 具体实现如下:

go 复制代码
// delayingType wraps an Interface and provides delayed re-enquing
type delayingType struct {
	Interface

	// clock tracks time for delayed firing
	clock clock.Clock

	// stopCh lets us signal a shutdown to the waiting loop
	stopCh chan struct{}
	// stopOnce guarantees we only signal shutdown a single time
	stopOnce sync.Once

	// heartbeat ensures we wait no more than maxWait before firing
	heartbeat clock.Ticker

	// waitingForAddCh is a buffered channel that feeds waitingForAdd
	waitingForAddCh chan *waitFor

	// metrics counts the number of retries
	metrics retryMetrics
}

然后个人觉得这个结构中最重要的应该是关注这个 waitingForAddCh chan *waitFor 这个字段,因此我们来追踪一下这个字段的作用。

go 复制代码
// 首先什么时候往这个 chan 添加元素
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
	if duration <= 0 {
		q.Add(item)
		return
	}

	select {
	case <-q.stopCh:
		// unblock if ShutDown() is called
	case q.waitingForAddCh <- &waitFor{data: item, readyAt: q.clock.Now().Add(duration)}:
	}
}

这个大写的 AddAfter() 说明了这个就是给用户调用的,专门用来添加元素的,也就是用户首先将需要执行的 item 放入这个 waitFor 里面。

然后我们再来看这个 waitFor 什么时候会被消费,消费的场景是在这个叫做 waitingLoop 中间:

go 复制代码
func (q *delayingType) waitingLoop() {  
    defer utilruntime.HandleCrash()  
  
    // Make a placeholder channel to use when there are no items in our list  
    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.Len() > 0 {  
          entry := waitingForQueue.Peek().(*waitFor)  
          if entry.readyAt.After(now) {  
             break  
          }  
  
          entry = heap.Pop(waitingForQueue).(*waitFor)  
          q.Add(entry.data)  
          delete(waitingEntryByData, entry.data)  
       }  
  
       // 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 := 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  
  
       case <-nextReadyAt:  
          // continue the loop, which will add ready items  
  
       case waitEntry := <-q.waitingForAddCh:  
          if waitEntry.readyAt.After(q.clock.Now()) {  
             insert(waitingForQueue, waitingEntryByData, waitEntry)  
          } else {  
             q.Add(waitEntry.data)  
          }  
  
          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  
             }  
          }  
       }  
    }  
}

我们先看看这个代码片段:

go 复制代码
case waitEntry := <-q.waitingForAddCh:  
          if waitEntry.readyAt.After(q.clock.Now()) {  
             insert(waitingForQueue, waitingEntryByData, waitEntry)  
          } else {  
             q.Add(waitEntry.data)  
          }  
  
          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  
             }  
          }  

这个代码也很好理解,也就是如果当前的 task 还没有超时,那么就按照时间要求将其放入 waitForQueue 中间,否则就赶快放入 queue 中间,让下游去执行;至于其中的 drained 就表示要把当前 chan 中的 task 全部处理完。

那么这个 waitingForQueue 是什么呢?我们接着看代码:

go 复制代码
// waitFor holds the data to add and the time it should be added
type waitFor struct {
	data    t
	readyAt time.Time
	// index in the priority queue (heap)
	index int
}

从注释来看,这里说的是 waitFor 说明的具体需要添加的元素,以及需要添加的具体时间。

然后在下面就又实现了一个优先级队列:

go 复制代码
type waitForPriorityQueue []*waitFor

也就是本质上就是一个按照时间先后实现的优先级队列。我们再来追踪一下这个结构体的数据的来源和去向,其中数据来源就是开始看到的片段,那么数据是如何被消费的呢?

go 复制代码
// Add ready entries
for waitingForQueue.Len() > 0 {
	entry := waitingForQueue.Peek().(*waitFor)
	if entry.readyAt.After(now) {
		break
	}

	entry = heap.Pop(waitingForQueue).(*waitFor)
	q.Add(entry.data)
	delete(waitingEntryByData, entry.data)
}

这里就是在 waitingLoop 中对这个数据的消费情况,其实也非常的简单,就是将 waitFor 中的所有时间到了的元素统统加到 Queue 中,从而让下游去执行。

2 使用场景

我们首先需要明白 DelayingQueue 的功能和场景:

  1. 功能:添加一个 item 并制定其在特定的时间再加入队列中
  2. 场景:在处理某一个 task 的时候发生了意外,因此需要过一段时间再去处理

然后我么来看看官方给出的测试例子:

go 复制代码
func TestSimpleQueue(t *testing.T) {
	fakeClock := testingclock.NewFakeClock(time.Now())
	q := NewDelayingQueueWithConfig(DelayingQueueConfig{Clock: fakeClock})

	first := "foo"

	q.AddAfter(first, 50*time.Millisecond)
	if err := waitForWaitingQueueToFill(q); err != nil {
		t.Fatalf("unexpected err: %v", err)
	}

	if q.Len() != 0 {
		t.Errorf("should not have added")
	}

	fakeClock.Step(60 * time.Millisecond)

	if err := waitForAdded(q, 1); err != nil {
		t.Errorf("should have added")
	} else {
		t.Logf("should have added, len(): %v ", q.Len())
	}
	item, _ := q.Get()
	t.Logf("item: %v", item)
	q.Done(item)

	// step past the next heartbeat
	fakeClock.Step(10 * time.Second)

	err := wait.Poll(1*time.Millisecond, 30*time.Millisecond, func() (done bool, err error) {
		if q.Len() > 0 {
			return false, fmt.Errorf("added to queue")
		}

		return false, nil
	})
	if err != wait.ErrWaitTimeout {
		t.Errorf("expected timeout, got: %v", err)
	}

	if q.Len() != 0 {
		t.Errorf("should not have added")
	}
}

其中需要注意的两个函数:这两个函数第一个是为了检查 waitingForAddCh 是否已经把元素交给了 waitingForQueue(就是那个优先级队列);第二个函数则是为了检查是否已经在时间到了之后,是否从 waitingForQueue 中将数据给到了底层的 queue,为什么要这么写呢,因为在测试中,程序猿使用的是FakeTime而不是真实的time,而且测试时间也很短,在源码中我们知道心跳时间是10s

go 复制代码
func waitForWaitingQueueToFill(q DelayingInterface) error {
	return wait.Poll(1*time.Millisecond, 10*time.Second, func() (done bool, err error) {
		if len(q.(*delayingType).waitingForAddCh) == 0 {
			return true, nil
		}

		return false, nil
	})
}

func waitForAdded(q DelayingInterface, depth int) error {
	return wait.Poll(1*time.Millisecond, 10*time.Second, func() (done bool, err error) {
		if q.Len() == depth {
			return true, nil
		}

		return false, nil
	})
}

然后个人觉得这个 fakeClock 非常的有意思,以后写延迟相关的东西都可以用来用。

所以整个例子就显得非常的明了了。

相关推荐
weixin_453965001 小时前
master节点k8s部署]33.ceph分布式存储(四)
分布式·ceph·kubernetes
是芽芽哩!1 小时前
【Kubernetes】常见面试题汇总(五十八)
云原生·容器·kubernetes
福大大架构师每日一题12 小时前
22.1 k8s不同role级别的服务发现
容器·kubernetes·服务发现
weixin_4539650013 小时前
[单master节点k8s部署]30.ceph分布式存储(一)
分布式·ceph·kubernetes
weixin_4539650013 小时前
[单master节点k8s部署]32.ceph分布式存储(三)
分布式·ceph·kubernetes
tangdou36909865513 小时前
1分钟搞懂K8S中的NodeSelector
云原生·容器·kubernetes
later_rql16 小时前
k8s-集群部署1
云原生·容器·kubernetes
weixin_4539650018 小时前
[单master节点k8s部署]31.ceph分布式存储(二)
分布式·ceph·kubernetes
大G哥21 小时前
记一次K8S 环境应用nginx stable-alpine 解析内部域名失败排查思路
运维·nginx·云原生·容器·kubernetes
妍妍的宝贝21 小时前
k8s 中微服务之 MetailLB 搭配 ingress-nginx 实现七层负载
nginx·微服务·kubernetes