k8s client-go中的队列workqueue设计笔记

本文详细剖析了Golang工作队列workqueue的实现原理,包括如何利用Set实现去重,FIFO队列、延迟队列和限速队列的实战应用,以及脏数据处理策略。重点讲解了workqueue在避免并发冲突和高效工作的核心机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在开发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()
    }
 }

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值