广告

编写自定义Kubernetes控制器:小心状态漂移

  • 浏览(95)
  • 评论(0)
  • 译者:k8s

Kubernetes已经取得了成功,协调了容器,也影响了云计算。

它的控制平面的设计模式基于一个声明式API(允许用户表达其所需状态)和一组控制循环(也称为控制器),这些控制循环驱动“真实世界”达到用户所需状态,已被证明是足够通用的,可管理的不仅仅是容器。

这带来社区编写的自定义Kubernetes控制器和API的蔓延,用于管理各种资源,如虚拟机、数据服务、软件定义的网络等。

然而,编写一个玩具自定义控制器相对容易,但编写一个生产级控制器有很多挑战。

我们正在基于自定义Kubernetes控制器构建一个完整的控制平面,以完全自动化数据服务的生命周期管理。在这样做的过程中,我们意识到了与生产级控制器相关的许多挑战。本文描述了一个很少讨论的挑战:“真实世界”可以自主地偏离用户期望的状态,因此它必须像Kubernetes API一样由控制器监控。

首先,我们先概述可能的解决方案。

问题

人们通常给出的Kubernetes控制器的定义是:“一个进程,它监控某些(自定义)Kubernetes API对象上的通知,并通过修改对象描述的资源来处理每个此类对象,以使资源与该对象的所需状态相匹配。”使API对象描述的资源与所需状态匹配的行为有时称为协调。

控制器修改哪些资源来协调API对象取决于API对象表示的内容,因此每个控制器都不同。一些控制器通过创建、更新和/或删除其他依赖的API对象来协调API对象。将API对象转换为较低级别的API对象可以进行多个级别的转换,但在某个时候,它会停止,因为控制器通过将副作用应用到非Kubernetes API的对象来协调API对象。

例如,如果你创建一个StatefulSet API对象,它将被转换为一些pod API对象,然后为每个pod启动一个容器;这些容器是Kubernetes API之外的副作用。

根据本节开头的定义,控制器所做的所有工作都是由API对象上的通知驱动的:如果因为API对象的填充没有变化而没有收到新的通知,那么控制器什么也不做。事实上,这就是写了多少控制器。如果控制器只创建/更新/删除API对象,并且不直接修改Kubernetes API之外的任何内容,那么这没有问题。

如果控制器修改Kubernetes API之外的一些资源,那么这样的设计可能有缺陷。原因是它忽略了这样一个事实,即这些资源的状态——“真实”状态可能会自动偏离API对象中描述的所需状态。在这种情况下,控制器将不会收到任何通知,因此无法将资源的状态驱动回所需状态,并且系统无法自我修复。

让我们看一个例子,如图1所示。假设我们有一个控制器,负责直接管理容器化应用程序。(当然,你不想编写这样一个自定义控制器,因为已经有Kubernetes解决了这个问题,但是为了这个示例,请耐心听我们说)。如果创建了一个API对象来描述具有一个副本的新应用,则控制器将生成一个运行该应用的容器。如果容器由于代码中的错误、错误的输入等而崩溃,控制器将永远不会收到关于API对象的通知。然而,它必须了解容器崩溃是为了尽快产生一个替代者来自我修复!

图片

图1:支持API对象的真实状态可以自动偏离所需状态。

因此,对于直接修改Kubernetes API外部资源的自定义Kubernetes控制器,订阅Kubernetes API对象上的通知是不够的。它还必须监控支持这些API对象的资源,即使真实状态消失,也必须触发期望状态和真实状态之间的协调,如图2所示。有一个例外,即只处理API对象的控制器也会遇到相同的问题:当控制器不是直接而是通过Kubernetes作业修改Kubernetes API之外的资源时。

图片

图2:真实状态偏离期望状态,但控制器收到通知并做出反应。

如果你正在构建一个简单的自定义控制器,那么可能只需要将协调的API对象转换为依赖的Kubernetes内置API对象,就不会遇到我们描述的问题。但是,如果您要基于自定义控制器构建一个完整的控制平面,该控制器必须支持复杂的用例,那么您可能需要编写一个控制器来修改Kubernetes API之外的内容,然后我们描述的问题可能会出现。

不幸的是,不可能有通用的解决方案,因为它在很大程度上取决于自定义API对象所描述的资源的性质。在下一节中,我们将概述一些可以作为构建解决方案基础的想法。

解决方案的想法

完全避免这个问题:

如果自定义控制器只能通过创建/更新/删除依赖的Kubernetes API对象来完成其任务,则采用这种方法;它更简单,完全避免了问题。控制器仍必须监控支持其实现的API对象的状态。尽管如此,由于该状态由其他API对象组成,控制器可以重用其用于通知其实现的API对象的相同Kubernetes机制,因此Kubernetes自动解决了该问题。

以幂等方式定期重新协调API对象:

假设修改Kubernetes API之外的资源的操作是幂等的。在这种情况下,控制器可以定期重新协调每个API对象,即使没有收到新的通知(Kubernetes对此有内置支持),并重新执行幂等操作。如果实际状态偏离所需状态,则在下一次协调时,实际状态是固定的。否则,由于应用的操作是幂等的,所以不会发生任何更改,如图3所示。

图片

图3:通过周期性地对真实状态应用幂等操作来解决真实状态和期望状态之间的不匹配。

这种方法会浪费资源,因为即使没有必要,也会定期进行协调,而且还存在调整协调周期的问题。

轮询真实状态:

如果对支持API对象的资源的真实状态的检查已经是协调的一部分,并且协调不会消耗太多资源,那么控制器可以定期重新协调每个API对象。标准协调逻辑将轮询真实状态,并仅在必要时进行纠正,如图4所示。

图片

图4:作为正常协调的一部分,通过定期轮询真实状态来解决真实状态和期望状态之间的不匹配。

否则,如果一个完整的协调占用了太多的资源,你可以编写只定期轮询每个API对象的真实状态的代码,并在真实状态与所需状态不同时触发协调,如图5所示。此类代码将在控制器进程内运行,但它将与完全协调API对象的主控制循环分开。

图片

图5:通过定期轮询专用逻辑中的真实状态并在检测到不匹配时触发协调,解决了真实状态和期望状态之间的不匹配。

这两种方法的问题是:

资源消耗(可能存在大量不必要的协调/轮询),

设置协调/轮询的合理期限,

控制器开发人员必须编写执行轮询的额外逻辑(对于第二种方法)。

实时监控通知:

如果支持API对象的真实状态内置了对其更改的流式通知的支持(与控制器使用相同的二进制),则可以包括侦听此类通知并在收到此类通知时触发相关API对象协调的逻辑。我们有一个真实的例子,其中每个API对象表示PostgreSQL数据库中的一个角色。由于PostgreSQL支持通知(例如,通过触发器),控制器可以订阅有关角色创建/更新/删除的PostgreSQL通知,并在收到此类通知后触发相关API对象的新协调。这相当于使控制器不仅由Kubernetes API通知驱动,还由“真实状态通知”驱动,如图6所示。

图片

图6:由Kubernetes API通知和真实状态通知驱动的协调。

不幸的是,有些情况下这是不可能的,因为真实状态不支持更改通知。

如果您遇到这种情况,您仍然可以通过轮询从实际状态的更改中合成通知,而代价是编写、维护和操作其他组件。你可以编写并部署一个“轮询器”,它轮询真实状态,并在发现真实状态和所需状态之间的差异时更新相关API对象的状态。此类状态更新将导致控制器收到API对象的通知,控制器将继续再次协调,修复真实状态。

结论

我们描述了一个在编写自定义Kubernetes控制器时经常被忽略的问题:支持API对象的真实状态可能会偏离所需状态,即使在这种情况下,也必须通知控制器协调这两种状态。我们草拟了一些想法,这些想法可以作为构建问题解决方案的起点。我们希望参与编写自定义控制器的任何人都能从我们所描述的问题的意识和如何克服它的想法中获益。

原文链接:

https://thenewstack.io/writing-custom-kubernetes-controllers-beware-of-state-drift/


  • 分享到:
  • icon
  • icon
  • icon
  • icon
箭头