controller-runtime 框架如何保证并发性的

背景

相信很多从事云原生的朋友都用过 controller-runtime 框架去实现自定义 controller 逻辑。在使用 controller-runtime 的过程中,通过设置 MaxConcurrentReconciles 参数能够调整 controller 并发数,从而满足高并发的需求。

然而,很多开发者存在个疑惑:在开启并发的情况下,会不会有多个 reconcile 协程同时处理一个 Object 呢。笔者也曾经为了防止此类问题发生,给程序加了一把读写锁。后来从某篇文章上看到,controller-runtime 是可以保证并发性的,笔者提起兴趣读了下代码,今天在这做个分享,希望能够为后来的开发者提供帮助。

考虑到有些读者需要快速得到答案,因此这里先把结论拿出来:

  1. controller-runtime 能够保证同一个时刻同一个 Object 只被一个协程处理,不存在并发性问题;
  2. reconcile 可能会丢弃某些请求,不保证所有的事件都会得到响应;
  3. reconcile 可能会出现某个 Object 等待很久的情况,通过调整并发量可缓解此类问题;

Workqueue

Informer 机制是 controller-runtime 框架的底层依赖,机制中的 workqueue 队列是用户 reconcile 逻辑获取 Object 事件的队列。而 workqueue 有三种实现:

  1. Type:通用队列,在保证 FIFO 特性的基础上,支持去重性;
  2. DelayingInterface:延迟队列,基于 Type 做了一层封装,支持延迟入队;
  3. RateLimitingInterface:限速队列。在 DelayingInterface 的基础上,支持入队速率限制。

ps:controller-runtime 中默认使用 RateLimitingInterface 队列。至于延迟时间与速率有很多种实现,controller-runtime 中默认使用的是令牌法。延迟策略本文不做描述,后续文章补上【又埋坑了】。

在保证并发性这块,通用队列 Type 就能满足,首先来看下结构定义。其中和本文强相关的三个参数 queue、dirty 和 processing 的描述件注释,而 progressing 正对应了上文的结论 1

Golang 复制代码
// staging/src/k8s.io/client-go/util/workqueue/queue.go
type Type struct {
   // slice 结构,只用于保存元素数据,并保证顺序性。所有 queue 的元素都应该出现在 dirty 中。
   queue []t

   // 脏元素集合,用于保存需要被处理的元素,有去重效果,保证同一个元素不会被多个协程处理
   dirty set

   // 正在处理的元素集合,当 reconcile 处理结束后,需要将元素从该集合和 dirty 集合中移除。
   // 因为同一时间不会有相同的元素在 progressing,因为保证了并发性
   processing set

   cond *sync.Cond
   shuttingDown bool
   drain        bool
   metrics queueMetrics
   unfinishedWorkUpdatePeriod time.Duration
   clock                      clock.WithTicker
}

而 Type 也实现了 Interface 中各类方法如下:

Golang 复制代码
// staging/src/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()
   ShutDownWithDrain()
   ShuttingDown() bool
}
  • add 方法:将元素添加到 dirty 和 queue 集合,并做去重操作。若 dirty 集合存在相同的元素,则丢弃;若progressing 集合中存在,则只将元素添加到 dirty,而不添加到 queue。因为 dirty 可能会丢弃元素,所以这里也映射了开头的结论 2
  • Len 方法:queue 集合的长度;
  • Get 方法:从 queue 头部获取一个元素,插入到 progressing 中,并从 dirty 集合中删除;
  • Done 方法:表示任务已处理结束,从 progressing 中删除元素。当元素在 processing 中时,Add 操作只是把元素放到 dirty 集合,并没有放入 queue 中,因此相同的元素处理完成从 processing 中移除后,需要把元素再放入到 queue 中,防止被遗漏。

常见入队流程演示

场景 1:正常流程

此时有 A、B、C 三个 Object 事件:

  1. 通过 Add 方法将其加入到 dirty 和 queue 中;
  2. 调用 Get 方法,将 A 从 dirty 移动到 progressing 中;
  3. 当 A 处理结束后,调用 Done 方法,将 A 从 progressing 中移除。

场景 2:progressing 过程,有相同 Object 的事件进来

接上文,此时又有一个 A E 事件进来:

  1. 第一个 A 还在处理中;
  2. 调用 Add 方法将第二个 A 加入到 dirty 中,因为此时 progressing 已存在第一个 A,所以无法将其加入到 queue 中;
  3. 调用 Add 将 E 加入到 dirty 和 queue 中;
  4. 当第一个 A 处理结束后,调用 Done 方法,第二个 A 被加入到 queue 尾巴。

此时发现,因为第二个 A 是被加入到 queue 队尾,若第一个 A 执行时间较长,会有大量的事件(比如 E)排到第二个 A 之前,导致第二个 A 长时间无法执行,这也印证了上文提到的结论 3。该情况可通过提升并发来解决。

场景 3:dirty 队列丢弃元素

接上文,此时 queue 和 dirty 中有 E A B C 四个元素:

  1. queue 和 dirty 中有 E A B C 四个元素;
  2. 调用 Get 方法将 E 加入 progressing 中;
  3. 此时因为 dirty 中已有 A 元素,第二个 A 元素被迫丢弃(不再进行接下来的判断流程,即无论在不在processing,都不会再入队),这里再次映射了开头的结论 2

结论

这里再说下结论哈:

  1. controller-runtime 能够保证同一个时刻同一个 Object 只被一个协程处理,不存在并发性问题;
  2. reconcile 可能会丢弃某些请求,不保证所有的事件都会得到响应;
  3. reconcile 可能会出现某个 Object 等待很久的情况,通过调整并发量可缓解此类问题;

未完待续

又来给自己埋坑了,有空来看哈。

  1. 延迟队列和限速队列的具体逻辑是什么样的呢;
  2. controller-runtime 框架在 informer 机制的基础上,做了哪些改动呢。
相关推荐
阿里云云原生2 小时前
加工进化论:SPL 一键加速日志转指标
云原生
阿里云云原生3 小时前
破解异构日志清洗五大难题,全面提升运维数据可观测性
云原生
阿里云云原生9 小时前
从 Python 演进探寻 AI 与云对编程语言的推动
云原生
亲爱的非洲野猪9 小时前
关于k8s Kubernetes的10个面试题
云原生·容器·kubernetes
西京刀客10 小时前
k8s之configmap
云原生·容器·kubernetes
阿里云云原生1 天前
Higress MCP 服务管理,助力构建私有 MCP 市场
云原生
zzywxc7871 天前
云原生 Serverless 架构下的智能弹性伸缩与成本优化实践
云原生·架构·serverless
KubeSphere 云原生1 天前
Higress 上架 KubeSphere Marketplace,助力企业构建云原生流量入口
云原生