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 非常的有意思,以后写延迟相关的东西都可以用来用。

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

相关推荐
木鱼时刻14 小时前
容器与 Kubernetes 基本概念与架构
容器·架构·kubernetes
chuanauc1 天前
Kubernets K8s 学习
java·学习·kubernetes
庸子2 天前
基于Jenkins和Kubernetes构建DevOps自动化运维管理平台
运维·kubernetes·jenkins
李白你好2 天前
高级运维!Kubernetes(K8S)常用命令的整理集合
运维·容器·kubernetes
Connie14512 天前
k8s多集群管理中的联邦和舰队如何理解?
云原生·容器·kubernetes
伤不起bb2 天前
Kubernetes 服务发布基础
云原生·容器·kubernetes
别骂我h2 天前
Kubernetes服务发布基础
云原生·容器·kubernetes
weixin_399380692 天前
k8s一键部署tongweb企业版7049m6(by why+lqw)
java·linux·运维·服务器·云原生·容器·kubernetes
斯普信专业组3 天前
K8s环境下基于Nginx WebDAV与TLS/SSL的文件上传下载部署指南
nginx·kubernetes·ssl
&如歌的行板&3 天前
如何在postman中动态请求k8s中的pod ip(基于nacos)
云原生·容器·kubernetes