Kubernetes 资源编排系列之四: CRD+Operator 篇
作者 炯思(钟炯恩) 雪尧(郭耀星)
这是我们的《Kubernetes 资源编排系列》的第四篇——CRD+Operator 篇。在前面的文章中,常常会提到 CRD 和 k8s operator,但并没有对此进行深入的探讨。作为 k8s 中的一大亮点,在本篇文章中,我们会详细展开讲讲。
1. 什么是 CRD
如果 K8S 中的自带资源类型不足以满足业务需求,需要定制开发资源怎么办?自定义资源(Custom Resource)由此产生。那么,如何让 Kubernetes 认识这些自定义的资源呢?CRD(Custom Resource Definition)就承担了一个说明书的角色,让 Kubernetes 来认识这个自定义资源 CR。
那么 CRD 是怎么来的呢?最早是谷歌提出 Third Party Resource 的概念,希望开发者以插件化形式扩展 K8s API 对象模型,以增强整个 k8s 的生态。基于 Third Party Resource 这一概念,Kubernetes 社区在 1.7 版本中提出了 CRD 的概念。
随便打开一个 CRD 的 YAML 可以看到,其主体部分是使用 OpenAPI v3 schema 来描述 CR 的字段结构,类似编程语言中的强类型声明。
有了 CRD 之后,我们可以自由地增加各种与 Pod 平级的资源,很多之前需要落在 CMDB 中的数据,也可以被放在 k8s 集群中。这极大地拓宽了我们的想象力,什么交换机、作业、路由等各种关联的资源都一股脑地放进集群里面去。
在各种自定义资源被放进去之后,就会有人问,这放进去是挺方便的,但是放进去就会生效吗?是的,资源的生效就是 Operator 的功劳。下面我们就开始介绍 Operator。
2. 什么是 Operator
首先随便翻看一本词典看一下 operator 这个词的定义:操作员/运算符,是个名词。那么,operator 描述的应该是一个围绕"操作、控制"概念的东西。为了让大家有个更直观的认识,我们来举一个例子,比如 1 + 2 = 3,这个 "+" 就是一个 operator(运算符),这个 "+" 让两个数字发生了一些互动(相加)。
有了词典里的概念铺垫后,我们继续往下分析,既然是一种操作或运算,那么在 k8s 中,是谁来操作?而被操作的对象又是什么呢?让我们来看一下 OperatorFramework 官网上对于 Operator 的解释:
WHAT IS AN OPERATOR AFTER ALL?
An Operator represents human operational knowledge in software, to reliably manage an application. They are methods of packaging, deploying, and managing a Kubernetes application.
从这个定义中,我们可以看到,这个 operator 是指由人发出的,对 k8s 应用(Kubernetes application)展开的操作。一般围绕应用的操作有哪些?部署、升级、扩缩容、卸载等等。我们可以先这样理解,operator 应该就是一个类似控制器的东西,里面含有一些运维操作(后面会继续展开,其实不仅仅是这些)。
较真一点的读者可能会问,既然这样,这东西叫 controller 是不是会更贴切一点呢?事实上,问出这个问题的读者,和真相很接近了,每个 operator 基本都会有个控制器,但又不仅仅只有一个控制器,还会有前面提到过的资源定义: CRD (CustomResourceDefinition) 。每种自定义资源背后都会有一个或多个控制器,让这些资源看起来像活的一样,如下面的 YAML 样例:
这个叫 Bob 的人,生日和性别是不可变属性,无法修改;而位置是可以修改的,可以从家改到公司,但是改完之后会有一段时间处于不 Ready 的状态,因为他正在去上班的路上。去哪个公司上班呢,他在 helloworld 公司工作,所以他是去这家公司上班。
通过上面这段 YAML 我们可以发现,当我们关注于对象终态的时候,我们就不太关注这个控制过程,这个 Bob 怎么去上班的,是开车还是地铁去的,其实我们并不关心。如果类比到普通的日常实践,也是这样:做一个应用的存储位置迁移,我们只需要设置这个应用新的存储位置,至于怎么迁移过去,是用网络命令传输过去的还是物理上用硬盘拷贝过去我们不关心,迁移过程中数据一致性我们也不关心,只知道会有 operator 把这些都给搞定。
所以,operator 其实是一种架构理念,它区别于常见的 shell 等运维脚本方案:operator 希望应用能够自己管理自己,而不是由运维人员写一堆脚本从外围来控制他们。不过,如果仅仅是这样,可能 operator 也只能叫 controller 了,只是一些自控制的逻辑而已。从最前面提到的 operator 的概念可以看出,operator 能够让两种以上的资源产生一些互动关系,那么这是如何实现的呢?
我们继续用上面 Human 的例子再加个 YAML:
这个 helloworld 公司也可以用一个资源对象来描述。如果我们把这个公司的 isOpen 改成 true,这个 company 会有个控制器来遍历所有的 Human 资源,把 spec.company="helloworld"的人(比如 Bob)的 location 全改成公司,这样就会让每个公司的人都动起来,想各种办法来公司上班。
![](https://static001.geekbang.org/infoq/90/904f89c3ccb50ea699a618925881aa74.png)
从上面的例子可以看出,每个控制器只负责自己的那部分,但从顶层往下看,已经实现了级联控制,能够实现牵一发而动全身的效果。这个就是上面所提到的 operator 的更深一层的机制:能够像运算符一样,让几种资源产生某种互动关系,一起协作完成一些复杂的工程动作。
3. 如何实现 K8S Operator
不管是原生 YAML / Helm 还是 Kustomize,都是通过配置来搞定各类事情。然而 CRD + Operator 就不一样了,它们让你直接接入 apiserver,作为 K8S 的一部分监听所有你关心的对象,并通过代码进行状态维持及管理。因为 CRD 的开发是非常复杂的,除了业务逻辑之外,还需要做很多基础的工作,非常不便,所以有了 Operator 的开发框架(常见的有 KubeBuilder 和 Operator-SDK),让开发人员专注于 CRD 的业务代码开发。
我们可以来看一下 operator 的架构实现,这个有助于我们理解 operator 的工作原理:
![](https://static001.geekbang.org/infoq/77/77e8c548f40e93421b79d3acf7146ad5.png)
虽然有了 KubeBuilder 或 Operator-SDK 开发框架,但 Operator 的开发在当前所有的几类组件托管方案当中仍然是最为复杂的。前前后后需要 CRD 设计及安装,编译 Operator 及部署到集群,最后再下发 CR,外围为了配套这些内容可能还需要上面 Helm 或 Kustomize 的协助,配合对应的 CICD 流程及工具。
Spark Operator
Spark Operator 是大数据分布式系统在 k8s 场景一次经典的实践。原本 Spark 的作业提交是需要通过 spark-submit 命令,但有了 Spark Operator 之后,我们可以直接向 k8s 提交作业 YAML,然后 Spark Operator 监听 CR,将这一作业提交给控制器。实现了我们前文提到的,将作业资源放在 k8s 集群进行管理这一目标。
敏
![](https://static001.geekbang.org/infoq/67/67b86daef849ad0f2264758d079e4130.png)
4. 大数据通用 Operator 设计与实践
上文讲述了 operator 实现的复杂性。不过,我们发现,越是这样复杂的应用,越是会有一些共通性:因为这些复杂应用基本都是分布式应用,只是在某些状态或部署顺序上的有些特殊需求。于是,我们针对这个现状,开发了一款通用的大数据 Operator。
这个通用 Operator 的架构设计如下:
![](https://static001.geekbang.org/infoq/98/9830831a665a920f1477332336e61c97.png)
与市面上常见的 golang 编写的 operator 不同的是,我们鼓励用户不编写代码,而是直接用 yaml 来描述控制逻辑,按照感知/决策/执行 三大环节来进行控制器的逻辑分解和编排设计。同时,因为有这几个环节抽象的辅助,用户在设计 operator 的时候能够更有目的性,对于复杂场景,不引入过多的复杂逻辑流,尽量用无状态的方式解决问题。
同时,我们还借鉴了前端框架 React 中的 VirtualDOM 的设计,在云原生场景下,引入了 VirtualResource 这样的一个概念。VirtualResource 能够将云原生对象资源映射进行 Operator 的内存数据库中,让控制器能够用 SQL 语法快速查询和操作这些资源对象,简化 Reconcile(调和)场景的逻辑复杂性。对照 React 框架中生命周期的概念,VirtualResource 也存在生命周期的概念,用户能够控制在资源变化的不同阶段,追加一些自定义的运维描述动作。
我们在大量使用 helm 的情况下,发现 golang template 语法在进行模板渲染的时候,还是不够灵活。于是我们把整体架构栈切换到 python,采用 jinja2 进行控制器的语法渲染,同时我们也保留 helm 在渲染框架中,用户能够无缝切换两种渲染引擎。
这个通用 Operator 的控制器将原本需要 golang 编写的控制层逻辑,简化成使用 cmd(指令) + yaml(资源) 的方式进行描述。控制器的描述示例如下:通过 helm 将 vvp 这个应用的所有 yaml 下发,监听 service 的状态变化,同步更新 ingress 资源的状态。
5. 总结
对于承载组件 (Component)这个概念而言,CRD+Operator 可以说是最为复杂的,但是又是最万能的,如果 Helm 或者 Kustomize 无法满足需求,Operator 基本上是唯一的选择。另一方面来说,CRD+Operator 一般又会和 Helm / Kustomize 相辅相成一起出现,最难搞的事情通过 Operator 与 apiserver 交互解决,剩下的胶水粘合,各种 YAML 拼接之类的交给 Helm / Kustomize 搞定。
同时,我们也可以看出,CRD+Operator 是云原生演进时期的方案,特别适合原本非 k8s 的软件架构上 k8s。那些原本就在 k8s 架构下出现的软件方案,会逐渐淡化 Operator 这个概念: 所有的工作负载都有能力和 k8s apiserver 交互。
对于承载 SREWorks 中的应用 (Application) 这个概念而言,Operator 是不合适的,无他,太复杂了。一般来说,Operator 只要管好自己这个独立功能在 K8S 中的生命周期就已经足够了。从目前的社区方向来看,Operator 不会作为一整个业务场景应用解决方案去裸提供,而是与 Helm / Kustomize / KubeVela / AppManager 等集成并作为一个整体 (组件 or 应用) 对外发布。
后续文章我们会分享更多的 Kubernetes 组件和应用管理工具,均会发布在我们的公众号“阿里智能运维”上,请大家持续关注~也欢迎大家在公众号后台留言想了解的内容和感兴趣的相关话题,与 SREWorks 团队进行交流。
若有收获,就点个赞吧
评论