workqueue
workqueue 提供了实现 RateLimitingInterface 接口的 Queue,它支持如下特性:
- 顺序性(Fair):item 处理的顺序和添加它的顺序一致;
- 单实例(Stingy):一个 item 只会被一个 worker 处理,如果在被处理前添加了多次,则只会被处理一次;
- 在处理过程中,可以将 item 重新入队列,后续可以重新处理;
- 关闭通知;
在实现 K8S Controller 时,写入 workqueue 的 item 是资源对象的标识 Key,而非资源对象本身(因为对象本身一般都在变化)。
Interface 接口定义了 queue 接口。
RateLimiter 接口的 When(item interface{}) time.Duration 方法,返回对某个对象进行操作的延迟。
DelayingInterface 接口包含 Interface 接口,但是提供了额外的 AddAfter(item interface{}, duration time.Duration) 方法,其中的 duration 一般来源于 RateLimiter.When() 方法。
RateLimitingInterface 接口包含 DelayingInterface 接口,但是提供了额外的、操作 RateLimiter 的 AddRateLimited(item interface{})、Forget(item interface{})、NumRequeues(item interface{}) int 方法。
函数 NewRateLimitingQueue() 返回实现 RateLimitingInterface 接口的对象,它的参数是实现 RateLimter 接口的对象。
Interface 接口
Interface 接口定义了 queue 接口。
// 来源于: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
}
函数 New()、NewNamed()、newQueue() 均返回实现 Interface 接口的 Type 类型对象:
// 来源于:k8s.io/client-go/util/workqueue/queue.go
func New() *Type {
return NewNamed("")
}
func NewNamed(name string) *Type {
rc := clock.RealClock{}
return newQueue(
rc,
globalMetricsFactory.newQueueMetrics(name, rc),
defaultUnfinishedWorkUpdatePeriod,
)
}
func newQueue(c clock.Clock, metrics queueMetrics, updatePeriod time.Duration) *Type {
t := &Type{
clock: c,
dirty: set{},
processing: set{},
cond: sync.NewCond(&sync.Mutex{}),
metrics: metrics,
unfinishedWorkUpdatePeriod: updatePeriod,
}
go t.updateUnfinishedWorkLoop()
return t
}
实现 Interface 接口的 Type 类型
Type 类型实现了 Interface 接口,即实现了 work queue。
// 来源于:k8s.io/client-go/util/workqueue/queue.go
type Type struct {
// queue 是一个将要处理的 item 有序列表,item 顺序和加入的顺序一致;
// queue 中的原生也位于 dirty 中,但是不能位于 processing 中。
queue []t
// dirty 包含所有正在处理的、即将被处理的 item
dirty set
// processing 包含正在处理的 item,该 item 可能同时位于 dirty 中
// 当处理 item 结束时,如果发现 dirty 中有该 item,如果有的话,将它加到 queue 中
processing set
cond *sync.Cond
shuttingDown bool
metrics queueMetrics
unfinishedWorkUpdatePeriod time.Duration
clock clock.Clock
}
q.dirty 缓存了所有 item,q.processing 缓存了正在处理的 item,而 q.queue 缓存了即将被处理的 item。
Add() 方法
Add() 方法用于向 workqueue 中添加 item,但是在添加前,它会检查 queue 中是否有该 item,或 item 是否在处理过程中:
- 如果 q.dirty 中有该 item,则直接返回;
- 否则将 item 添加到 q.dirty 中;
- 如果 item 位于 q.processing 中,则表示正在处理,则直接返回;
- 否则,将 item 添加到 q.queue 中;
// 来源于:k8s.io/client-go/util/workqueue/queue.go
func (q *Type) Add(item interface{}) {
q.cond.L.Lock()
defer q.cond.L.Unlock()
if q.shuttingDown {
return
}
if q.dirty.has(item) {
return
}
q.metrics.add(item)
q.dirty.insert(item)
if q.processing.has(item) {
return
}
q.queue = append(q.queue, item)
q.cond.Signal()
}
Get() 方法
Get() 方法从 queue 中返回一个 item:
- 从 q.queue 中返回第一个 item;
- 将该 item 插入到 q.processing 中,表示正在处理;
- 将该 item 从 q.dirty 删除;
Get() 方法将 item 从 q.queue 中弹出,插入 q.processing,从 q.dirty 中删除。
// 来源于:k8s.io/client-go/util/workqueue/queue.go
func (q *Type) Get() (item interface{}, shutdown bool) {
q.cond.L.Lock()
defer q.cond.L.Unlock()
for len(q.queue) == 0 && !q.shuttingDown {
q.cond.Wait()
}
if len(q.queue) == 0 {
// We must be shutting down.
return nil, true
}
item, q.queue = q.queue[0], q.queue[1:]
q.metrics.get(item)
q.processing.insert(item)
q.dirty.delete(item)
return item, false
}
Done() 方法
Done() 方法表示对 item 的处理结束,将它从 q.processing 移除。同时查看 q.dirty 中是否有该 item,如果有的话,将它加入 q.queue,供后续 Get() 返回处理:
注意:处理结束后必须调用 Done() 方法,否则对象一致处于 q.processing 中,后续 Add() 该 item 可能会失效(只会成功一次,添加到 q.dirty 中,后续都失效)。
// 来源于:k8s.io/client-go/util/workqueue/queue.go
func (q *Type) Done(item interface{}) {
q.cond.L.Lock()
defer q.cond.L.Unlock()
q.metrics.done(item)
q.processing.delete(item)
if q.dirty.has(item) {
q.queue = append(q.queue, item)
q.cond.Signal()
}
}
向 workqueue 添加 item 的 4 种情况
第一次添加 item:
- 添加到 q.dirty 中;
- q.processing 中没有该 item,故添加 q.queue 中;
如果该 item 没有被 Get() 返回,也没有正在被处理,再次添加 item:
- q.dirty 中有该对象,直接返回;(无效添加)
如果该 item 被 Get() 返回,但是在处理过程中,再次添加 item:
- q.dirty 中没有该 item,故添加到 q.dirty 中;
- q.processing 中有该 item,故直接返回(不添加到 q.queue 中);
如果该 item 被 Get() 返回,且处理结束(调用 Done 方法),再次添加 item:
- 和第一次添加该 time 的效果一致。
对于第三种情况,即处理过程中再次添加该 item,在处理结束调用 Done() 方法的过程中,会将 itme 从 q.dirty 中移除,添加到 q.queue 中,这样后续可以 Get() 处理。
Type 类型的 workqueue 一般不会单独使用,而是内嵌到 deleyQueue 中,和 rateLimiter 联合使用。
RateLimiter 接口
RateLimiter 接口的 When() 方法,返回了下一次添加 item 需要等待的时间。
// 来源于:k8s.io/client-go/util/workqueue/default_rate_limiters.go
type RateLimiter interface {
// When gets an item and gets to decide how long that item should wait
When(item interface{}) time.Duration
// Forget indicates that an item is finished being retried. Doesn't matter whether its for perm failing
// or for success, we'll stop tracking it
Forget(item interface{})
// NumRequeues returns back how many failures the item has had
NumRequeues(item interface{}) int
}
函数 DefaultControllerRateLimiter()
返回实现该接口的 NewMaxOfRateLimiter
类型对象。
// 来源于:k8s.io/client-go/util/workqueue/default_rate_limiters.go
func DefaultControllerRateLimiter() RateLimiter {
return NewMaxOfRateLimiter(
NewItemExponentialFailureRateLimiter(5*time.Millisecond, 1000*time.Second),
// 10 qps, 100 bucket size. This is only for retry speed and its only the overall factor (not per item)
&BucketRateLimiter{Limiter: rate.NewLimiter(rate.Limit(10), 100)},
)
}
在开发自定义 Controller 时,经常使用该函数返回的 RateLimiter 来创建 workqueue:
- BucketRateLimiter 实现 overall rate limiting,NewItemExponentialFailureRateLimiter 实现 per-item 的 rate limiting;
- overall 是 token bucket,per-item 是 exponential;
实现 RateLimter 接口的 BucketRateLimiter 类型
BucketRateLimiter 是基于标准的 token bucket 实现的限速器 :
// 来源于:k8s.io/client-go/util/workqueue/default_rate_limiters.go
type BucketRateLimiter struct {
*rate.Limiter
}
var _ RateLimiter = &BucketRateLimiter{}
func (r *BucketRateLimiter) When(item interface{}) time.Duration {
return r.Limiter.Reserve().Delay()
}
func (r *BucketRateLimiter) NumRequeues(item interface{}) int {
return 0
}
func (r *BucketRateLimiter) Forget(item interface{}) {
}
创建 BucketRateLimiter(参考上面的 DefaultControllerRateLimiter()
函数):
BucketRateLimiter{Limiter: rate.NewLimiter(rate.Limit(10), 100)}
这会创建一个 10qps、100 bucket size 的 token bucket 限速器。该类型的限速器,如果 bucket size 有富余,则允许 burst 大于 10qps。
实现 RateLimter 接口的 ItemExponentialFailureRateLimiter 类型
ItemExponentialFailureRateLimiter 实现了根据失败次数对延迟进行指数型增长的限速器,延迟时间为 baseDelay*2^,调用者可以执行 maxDelay 来限制最大延迟时间:
// 来源于:k8s.io/client-go/util/workqueue/default_rate_limiters.go
type ItemExponentialFailureRateLimiter struct {
failuresLock sync.Mutex
failures map[interface{}]int
baseDelay time.Duration
maxDelay time.Duration
}
函数 NewItemExponentialFailureRateLimiter() 和 DefaultItemBasedRateLimiter() 方法返回创建该类型的实例:
// 来源于:k8s.io/client-go/util/workqueue/default_rate_limiters.go
func NewItemExponentialFailureRateLimiter(baseDelay time.Duration, maxDelay time.Duration) RateLimiter {
return &ItemExponentialFailureRateLimiter{
failures: map[interface{}]int{},
baseDelay: baseDelay,
maxDelay: maxDelay,
}
}
func DefaultItemBasedRateLimiter() RateLimiter {
return NewItemExponentialFailureRateLimiter(time.Millisecond, 1000*time.Second)
}
创建 ItemExponentialFailureRateLimiter(参考上面的 DefaultControllerRateLimiter()
函数):
NewItemExponentialFailureRateLimiter(5*time.Millisecond, 1000*time.Second),
}
其它方法:
- 每次调用 When() 方法时,对应 item 失败次数加 1。
- NumRequests() 方法返回对象失败的次数;
- Forget()方法将对象从限速器移除,相当于将失败次数置 0,这样下次延迟从 baseDelay 开始。
实现 RateLimter 接口的 ItemFastSlowRateLimiter 类型
实现 RateLimter 接口的 MaxOfRateLimiter 类型
DelayingInterface 接口
RateLimitingInterface 接口
workqueue 的使用场景
// 来源于:https://github.com/kubernetes/sample-controller/blob/master/controller.go
type Controller struct {
...
workqueue workqueue.RateLimitingInterface
...
}
func NewController(
kubeclientset kubernetes.Interface,
sampleclientset clientset.Interface,
deploymentInformer appsinformers.DeploymentInformer,
fooInformer informers.FooInformer) *Controller {
...
controller := &Controller{
...
workqueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "Foos"),
...
}
...
}
func (c *Controller) enqueueFoo(obj interface{}) {
var key string
var err error
if key, err = cache.MetaNamespaceKeyFunc(obj); err != nil {
utilruntime.HandleError(err)
return
}
c.workqueue.AddRateLimited(key)
}
func (c *Controller) processNextWorkItem() bool {
...
err := func(obj interface{}) error {
defer c.workqueue.Done(obj)
var key string
var ok bool
if key, ok = obj.(string); !ok {
c.workqueue.Forget(obj)
utilruntime.HandleError(fmt.Errorf("expected string in workqueue but got %#v", obj))
return nil
}
if err := c.syncHandler(key); err != nil {
c.workqueue.AddRateLimited(key)
return fmt.Errorf("error syncing '%s': %s, requeuing", key, err.Error())
}
c.workqueue.Forget(obj)
klog.Infof("Successfully synced '%s'", key)
return nil
}(obj)
...
}