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 机制的基础上,做了哪些改动呢。
相关推荐
分布式存储与RustFS11 小时前
RustFS 多协议接入全景:S3 之外,Swift/Keystone 与 SFTP/FTPS 怎么选
云原生·开源·对象存储·分布式存储·s3·rustfs·性能基准
分布式存储与RustFS15 小时前
在 Kubernetes 上用 RustFS Operator 给 Tenant 开 KMS 加密:spec.encryption 实战
云原生·开源·对象存储·分布式存储·s3·rustfs·性能基准
Seoyoneh18 小时前
呼叫中心系统云原生架构演进:传统自建与云端部署的技术实现对比分析
云原生·架构
2601_9622184718 小时前
万象生鲜系统订单全生命周期追踪实现生鲜企业订单业务数字化可视
大数据·运维·微服务·云原生·架构
分布式存储与RustFS1 天前
RustFS 1.0 GA 前的生产验收清单:从 rc.1 到 GA 该准备什么
云原生·开源·对象存储·分布式存储·s3·rustfs·性能基准
Henry-SAP1 天前
SAP PP 反冲机制业务解析
人工智能·云原生·sap·erp
A-刘晨阳2 天前
K8s集群中的数据库新范式:KES-Operator解锁部署、扩缩容、备份全链路自动化
运维·数据库·云原生·kubernetes·自动化
江畔柳前堤2 天前
云原生 × AI 全景图谱:从 Kubernetes 到 Agent 基础设施的一次认知跃迁
人工智能·分布式·gpt·云原生·kubernetes·原型模式·agi
天机玄正2 天前
云运维高阶二:CLI 调用 API (白名单追随)
windows·云原生
Ningcode_cloud2 天前
Kubernetes 弹性伸缩实验手册:从集群搭建到 HPA / VPA 自动扩缩容
云原生·k8s·prometheus