WorkQueue
WorkQueue称为工作队列,Kubernetes的WorkQueue队列与普通的FIFO对比,实现略显复杂,它的主要功能在于标记和去重,并支持以下特性:
有序:按照添加顺序处理元素(item)
去重:相同元素在同一时间不会被重复处理,例如一个元素在处理前被添加了多次,它只会被处理一次。
并发性:多生产者和多消费者
标记机制:支持标记功能,标记一个元素是否被处理,也允许元素在处理时重新排队。
通知机制:ShutDown方法通过信号量通知队列不再接收新的元素,并通知metric goroutine退出。
延迟:支持延迟队列,延迟一段时间后再将元素存入队列。
限速:支持限速队列,元素存入队列时进行速率限制。限制一个元素被重新排队(Reenqueued)的次数。
Metric:支持metric监控指标,可用于Prometheus监控。
WorkQueue支持3种队列,并提供3种接口,不同队列实现可应对不同的使用场景,分别如下:
Interface:FIFO队列接口,先进先出队列,并支持去重机制。
DelayingInterface:延迟队列接口,基于Interface接口封装,延迟一段时间后再将元素存入队列。
RateLimitingInterface:限速队列接口,基于DelayingInterface接口封装,支持元素存入队列时进行速率限制。
1. FIFO队列
FIFO队列是最基本的队列方法,例如增删改查,获取长度等。WorkQueue中的延迟队列和限速队列都是基于Interface接口实现的,Interface定义如下:
源码路径:k8s.io\client-go\util\workqueue\queue.go
FIFO队列的数据结构为:
例如上图所示为FIFO的存储过程,通过Add方法向FIFO队列中分别插入1,2,3这3个元素,此时队列中的queue和dirty字段分别存有1,2,3元素,processing字段为空。
然后通过Get方法获取最先进入的元素(元素1),此时队列中的queue和dirty字段分别存有2,3;元素1被放入processing字段中,说明它正在被处理。最后处理完元素1时,通过Done方法将其标记为处理完成,此时队列中的processing字段中的1元素被删除。
但是再并发存储下,如何保证处理一个元素之前哪怕被添加多次,也只是处理一次,下图为FIFO并发存储的过程。
在并发场景下,goroutine A通过Get方法获取元素1,元素1被添加到processing字段中,同一时间,goroutine B通过Add方法插入另一个1元素,此时在processing字段中已经存在相同的元素,所以后面后面的元素1不会被直接插入到queue字段中,而是存入dirty字段中;在goroutine A通过Done方法标记处理完元素1后,如果dirty字段中存有元素1,则将其追加到queue字段的尾部,dirty和processing字段都是HashMap数据结构实现的,不考虑无序,只考虑去重。
下面来看源码,
源码路径:k8s.io\client-go\util\workqueue\queue.go
2. 延迟队列
延迟队列是基于FIFO队列接口封装的,在原有功能上增加了AddAfter方法,其原理是延迟一段时间后再将元素插入FIFO队列,延迟队列的数据结构如下:
延迟队列运行原理如图所示:
将元素1放入waitingForAddCh字段中,通过waitingLoop函数消费元素数据,当元素的延迟时间不大于当前时间时,说明还需要延迟将元素插入FIFO队列的时间,此时将该元素放入优先队列(waitForPriorityQueue)中。当元素的延迟时间大于当前时间时,则将该元素插入FIFO队列中。同时,也会遍历waitForPriorityQueue中的元素,按照上述逻辑验证时间。
看下源码:
源码路径:k8s.io\client-go\util\workqueue\delaying_queue.go
3. 限速队列
限速对列是基于延迟队列和FIFO队列接口封装,限速队列接口(RateLimitingInterface)在原有功能上增加了AddRateLimited、Forget、NumRequeues方法。限速队列的重点不在于RateLimitingInterface接口,而在于它提供的四种限速算法接口,其原理是:限速队列利用延迟队列的特性,延迟某个元素的插入时间,达到限速的目的。数据结构为:
源码路径:k8s.io\client-go\util\workqueue\default_rate_limiters.go
When:获取指定元素应该等待的时间
Forget:释放指定元素,清空该元素的排队数
NumRequeues:获取指定元素的排队数
注意:限速周期——很重要的一个概念。
一个限速周期是指从执行AddRateLimited方法到执行完Forget方法之间的时间。如果该元素被Forget方法处理完,则清空排队数。
3.1 令牌桶算法(BucketRateLimiter)
令牌桶算法是通过Go的第三方库golang.org/x/time/rate实现。内部实现了一个存放token(令牌)的”桶“,初始时”桶“是空的,token会以固定速率往”桶“里填充,直到将其填满为止,多余的token会被丢弃。每个元素都会从令牌桶得到一个token,直到得到token的元素才允许通过(accept),而没有得到token的元素处于等待状态。令牌桶算法通过控制token发放来达到限速的目的。原理图如下:
WorkQueue在默认情况下会实例化令牌桶,代码如下:
在实例化rate.NewLimiter后,传入两个参数r和b,其中r参数表示每秒往”桶“里填充的token的数量,b参数表示令牌桶的大小(即令牌桶最多存放token的数量)。默认r=10,b=100;假设在一个限速周期内插入1000个元素,通过r.Limiter.Reserve().Delay函数返回指定元素应该等待的时间,那么前b(即100)个元素会被立刻处理,而后面元素的延迟时间分别为item100/100ms、item101/200ms、item102/300ms、item103/400ms,以此类推。
3.2 排队指数算法(ItemExponentialFailureLimiter)
排队指数算法将相同元素的排队数作为指数,排队数增大,速率限制呈指数级增长,但其最大值不会超过maxDelay。元素的排队数统计是有限速周期的。一个限速周期是从执行AddRateLimited方法到执行完Forget方法之间的时间。如果该元素被Forget方法处理完,则清空排队数,核心代码实现为:
源码路径:k8s.io\client-go\util\workqueue\default_rate_limiters.go
在同一限速周期内,如果不存在相同元素,那么所有元素的延迟时间为baseDelay;而在同一限速周期内,如果存在相同元素,那么相同元素的延迟时间呈指数级增长,最长时间不会超过maxDelay。
3.3 计数器算法(ItemFastSlowRateLimiter)
计算器算法是限速算法中最简单的一种,其原理是:限制一段时间内允许通过的元素数量,例如在1分钟内只允许通过100个元素,每插入一个元素,计数器自增1,当计数器到100的阈值且还在限速周期内时,则不允许元素再通过。WorkQueue在此基础上扩展了fast和slow速率。数据结构为:
通过源码看到,当排队数超过maxFastAttempts字段限制的数量时,则返回slow速率,即假如maxFastAttempts=3,当第4个元素到达时,返回slow速率;则前三个元素使用fast速率,而第四个使用slow速率。
3.4 混合模式(MaxOfRateLimiter)
混合模式是将多种限速算法混合使用,即多种限速算法同时生效。下面为默认使用排队指数算法和令牌桶算法:
Last updated
Was this helpful?