本文翻译自:https://openkruise.io/blog/learning-concurrent-reconciling/,如果你对并发reconciling也存在一致性担忧,请继续阅读本文。
Controller模式是Kubernetes成功的重要因素之一,它是保证Kubernetes API处于目标状态的核心机制。通过CRD、Controller以及Operator,其他系统可以很方便的集成进Kubernetes。
众多开发者使用Controller运行时库和对应的Controller工具--KubeBuilder来构建他们自己的Kubernetes Controller。在OpenKruise项目中,我们同样使用了KubeBuilder生成脚手架代码进而实现reconciling逻辑。在这篇博客中,我将会分享一些从Kruise Controller开发中学习到的东西,尤其是并发reconciling。
肯定有人已经注意到:controller runtime是支持并发reconciling的。下面是创建controller时的Options参数结构体[source code]:
typeOptionsstruct { // MaxConcurrentReconciles is the maximum number of concurrent Reconciles which can be run. Defaults to 1.MaxConcurrentReconcilesint// Reconciler reconciles an objectReconcilerreconcile.Reconciler}
当Controller watch的对象十分频繁的发生变更,reconcile队列中就会堆积大量的reocncile请求。此时“并发reconciling”就显得尤为重要。和默认的单个reconcile循环相比,多个reconcile循环可以更快速的处理reconcile队列中的请求。并发reconciling的确有很大的性能提升,但是如果不深入研究相关的代码,开发人员很容易会担心一个问题:并发reconciling会引入一致性问题吗?比如:有没有可能两个reconcile循环同时处理相同的对象呢?
如你期望的那样,答案是否定的。两次reconcile不会同时处理相同的对象。这个“魔法”是由Kubernetes client-go中实现的工作队列保证的,controller runtime的reconcile队列使用的正是这个队列。这个工作队列[source code]的算法如下图所示:
为了实现多个reconcile循环不会同时处理同一个对象,这个工作队列除了一个队列(queue)以外,还用到了两个集合(set)。
- 1. Figure1(a)代表了一个空队列的初始状态,假设将会有4个reconcile请求到达,其中两个请求关联的对象都是object A。
- 2. 当一个请求到达,与之关联的目标对象首先会被添加到dirty set中,如果这个对象已经在dirty set中,则这个对象会被丢掉(译者注:这里的“丢掉”指的是不再进行接下来的判断流程,即:无论在不在processing set中,都不会入队);然后,如果这个对象不在processing set中,则将这个对象入队。Figure1(b)表示连续添加了3个对象进入队列。
- 3. 当一个reconcile循环准备好处理一个请求的时候,它就会从队首取出一个对象,同时将这个对象加入processing set,并且从dirty set中移除(如Figure1(c)所示)。
- 4. 此时,如果有一个正在处理中的对象对应的请求到达,这个对象只会被添加到dirty set中,并不会入队(如Figure1(d)所示)。这保证了一个对象只可能被一个reconcile循环处理。
- 5. 当这个reconcile结束,这个对象会从processing set中被移除。如果这个对象还存在于dirty set中,则这个对象会被加入到队尾(如Figure1(e)所示)。
上述算法会有如下影响:
- * 它避免了多个reconcile循环同时处理相同的对象。
- * 对象的处理顺序可能会和相应请求的到达顺序不同,即便只有一个reconcile线程。因为Controller一定会将集群渲染到最终目标状态,所以这两个顺序不同通常不会造成错误。然而,乱序reconcile可能会导致一个请求被明显的延迟。
以Figure2为例:
- 1. 假设当前只有一个reconcile线程,并且有两个处理object A的请求到达。
- 2. 它们其中一个会被处理,一个会加入到dirty set中(如Figure2(b)所示)。
- 3. 如果当前的reconciling持续了很久,在此期间有很多新的reconcile请求到达,这些新请求都会进入队列(如Figure2(c)所示)。
- 4. 当这次reconciling结束,object A会被加入队尾(如Figure2(d)所示)。因此直到队列里的前面对象都被处理完,A才会被再次处理。很明显第二个处理A的请求是在这些请求之前到达的,但是实际的处理却在这些请求之后,因此造成了不能忽视的延迟。
一个简单的缓解这类延迟的方法--并发reconciling。因为一个go routine的消耗通常很小,所以即使当前Controller是空闲的,多个reconcile线程也不会占用太多资源。所以MaxConcurrentReconciles通常会被设置为大于1的整数。
最后要说的是:reconcile请求是可能会被丢弃的(如果目标目标对象已经在dirty set中)。这意味着我们不能假定Controller可以追踪到某个对象的所有状态更改事件。回顾Tim Hockin的演讲,Kubernetes的Controller是水平触发的,而不是边缘触发。It reconciles for state, not for events.