聊一聊控制器模式

开头

容器的出现和k8s的普及已经改变了我们传统的运维方式,这些技术给我们带来了更多的资源利用率和更强的容错能力。

这其中控制器模式必然占据了举足轻重的作用。这种设计模式是开发和扩展Kubernetes的核心,在Kubernetes的代码仓库中能到处看到它的身影。

声明式or命令式

当然,当提到控制器模式时候,你也一定会看到声明式API这种东西,这两种一定是同时搭配使用的。

再之,提到“声明式”,你也必然会在大量的文章中看到“命令式”与“声明式”的比较。这些文章一般会说:

  1. 与命令性API相比,声明性API的使用更加简洁,并且提供了更好的抽象性。
  2. “命令式”强调的是“how”,你必须step-by-step告诉计算机如何完成一项工作(类似自己做菜)。“声明式”只需要告诉计算机你想要什么,声明你的”what”,计算机会为你完成具体的工作(类似于去饭店点菜吃饭)

还有的教程上会说:

“声明式 API”允许有多个 API 写端,以 PATCH 的方式对 API 对象进行修改,而无需关心本地原始 YAML 文件的内容。

在我看来,声明式能够提供所谓的更简洁的API,且有Patch的能力,这些并不是我们常用的命令式API所不能做到的

如果你觉得API不够简洁抽象,那你应该去考虑你设计上是否存在问题,或者没按照规范来?

想要命令式API具有Patch的能力,那可以把资源的属性全部传过去,让你的接口具有类似compare and update的能力不就行了?

所以,不管声明式还是命令式,我觉得核心还是在背后处理API的方式上的不同,这种处理方式通常叫做”控制器模式”。

云平台的困境

在之前的工作中,我们基于IaaS平台来实现业务。与web服务不同的是,IaaS平台存在更多的耗时请求,且请求过程中出现报错、超时等错误更是家常便饭。

不管是自研IaaS还是二次开发,针对一个多阶段的耗时请求,为了保证发生异常时不存在垃圾资源,通常会实现与正向操作相反的方法,我们称之为rollback方法。

就拿创建虚拟机实例的例子来说,一个createVmInstance操作通常会有很多阶段(正向):

[AllocateVolume -> AllocateNic -> CreateOnHypervisor…]

这里的每一步都会报错,那么为了报错之后保持环境干净,你必须提供相应的回滚操作(反向):

[DeleteOnHypervisor -> DeleteNic -> DeleteVolume]

这里通常会设计成一个Stack,当执行一个正向操作时,将相应的反向操作压栈;当发生错误时对栈进行Pop,依次执行回滚操作。

有点像Saga的分布式事务

但是这里会有个几个问题:

  1. 我在执行回滚函数时又发生了报错该怎么办?这时候可能就需要定时GC相关的逻辑来处理垃圾资源。然后,用户通过重试请求createVmInstance的方式再次创建,循环往复。

  2. 假如vm创建成功,其下一个子资源被人误删(虽然很多时候会报device is busy删除失败),那这个vm只能一直处于错误状态了。

控制器模式

控制器模式可以很好地解决上面解决上面这两个问题。

下面以k8s中的控制器来说明。

Kubernetes控制器会追踪一个或多个资源,并且将资源描述成如下结构:

1
2
3
4
5
6
type Object struct {
metav1.TypeMeta
metav1.ObjectMeta
Spec ObjectSpec
Status ObjectStatus
}

除了一些必要的元数据外,还有两个字段: Spec(期望)和Status(当前状态)。控制器不断监控该资源,比较Spec的定义和当前的实际环境,在一个调谐循环中尝试将当前实际环境变换成期望的状态。伪代码如下:

1
2
3
4
5
for {
desired := getDesiredState()
current := getCurrentState()
makeChanges(desired, current)
}

这也就是我们常说的最终一致性,当整个系统恢复正常时,总能给你一个你想要的状态的资源,哪怕你中间误删了一些子资源。

在Kubernetes中,主要通过Informer机制来实现,它作为客户端被使用在每一个Kubernetes的控制器逻辑中。
其中,为了保证事件不丢失,实现了list-watch机制, cache机制减少了对apiserver的请求压力,还有限速队列等保护机制等,这里不再展开叙述。

现实中的例子

控制器模式让我想起了我大学时的自动化专业,在自动控制领域中,闭环的负反馈控制系统如下所示:

可以看到有类似的地方:通过不断地比较输入信号和反馈信号,再经过相应的算法(如经典的PID算法),以达到输出信号和输入信号趋于一致的状态。

这些控制器的例子在生活中比比皆是:空调温湿度调节,电机转速等。

结语

在分布式环境下,错误永远不能避免,如果你想让你的软件达到Always On的效果,可能就需要引入控制器模式。当然,基于Kubernetes之上运行的大多数软件,已经不需要再考虑这些问题了。