§ 0x01 起因
开发控制器时,团队内一直在讨论是否需要为单个控制器对象添加并发控制(即加锁),最终把 controller-runtime 框架中并发数改为1,同时启用了 k8s 的 leader election机制保证只有单实例来规避并发的可能。
这种做法其实是有问题的,没有搞清楚 controller-runtime 框架本身是什么样的行为,强行把并发限制为1,可能导致性能上不去。
刚好在使用 cluster-api 过程中又遇到另外一个问题,某个 cluster 对象的 Reconcile 过程死锁阻塞了,导致这个对象后续都不再有 Reconcie 日志产生,而且注意到 cluster-api 的默认并发数是10。这两个问题的解答都需要对 controller-runtime 的行为进行梳理。
一般情况下直接看 controller-runtime 的文档就能明白了,不过在看过 https://github.com/kubernetes-sigs/controller-runtime/blob/main/pkg/reconcile/reconcile.go
中的文档,对 Reconcile 的解释,并没有强调同一个对象的并发 Reconcie 行为:是不会并发,还是会有并发?没有体现。没办法只能看代码了。
§ 0x02 无奈地去看源码
最终的关键逻辑在 k8s.io/client-go/util/workqueue/queue.go
中。
Type 对象中有3个关键数据结构。
- queue 队列,用来添加新对象。
- dirty hashset 记录 dirty 的对象集合。一个对象被取出处理时,如果又收到新的对象时,它就是 dirty 的,需要两次处理。 Add 时加入, Done 时取出,重新放回 queue 中。
- processing hashset 正在处理的对象集合。 Get 获取对象时放入,Done 调用时取出。
对应的数据流转图如下:
以上 hashset 的定义如下:
type empty struct{}
type t interface{}
type set map[t]empty
它是一个以泛型为 key 的map 。结合 controller-runtime,它存放的对象类型是 Request,定义如下:
type Request struct {
// NamespacedName is the name and namespace of the object to reconcile.
types.NamespacedName
}
而 types.NamespacedName 是个包含 Namepsace 和 Name 的 struct 类型。
通过分析 Type 类型的 Add 方法,可以解释一个对象 A 正在被 Reconcile 过程中,又有一个事件触发时, controller-runtime 的行为。
Add 上述场景会把对象放在 dirty 集合中,判断已在 processing 集群中则返回。所以解释这个问题的关键在于,set
类型中是是否存在某个元素是如何判断的,即 Request
对象对应的 struct 类型是如何在 map 中取 hash 的。
这种验证比较简单,直说结论:struct 类型是逐个对象迭代计算出的 hash 值,所以同一个对象转换得到的 Request
对象取值是一样的,最终对应的 hash 值 也是一样的。
§ 0x03 结论
即便控制器的并发数不为1,同一个进程中,不会有多个协程同时处理一个对象。
详细如下:
- 正在处于中的对象,Add 调用不会入队,只记录在 dirty 中。
- 对象处于完成后,在 Done 调用时检查,如在 dirty 中,再次入队,开始下一轮的处理。保证不丢事件。
这种设计核心思想是,用 map 对事件进行合并;使用队列保证顺序。
标签:Reconcile,对象,并发,controller,dirty,runtime,struct From: https://www.cnblogs.com/linlinsite/p/17983814