与 Kustomize 和 Helm 不同的是,Operator不应当被称作是一种工具或者系统,它应该算是一种封装、部署和管理 Kubernetes 应用的方法,尤其是针对最复杂的有状态应用去封装运维能力的解决方案,最早是由 CoreOS 公司(于 2018 年被 RedHat 收购)的华人程序员邓洪超提出的。
简单来说,Operator 是通过 Kubernetes 1.7 开始支持的自定义资源(Custom Resource Definitions,CRD,此前曾经以 TPR,即 Third Party Resource 的形式提供过类似的能力),把应用封装为另一种更高层次的资源,再把 Kubernetes 的控制器模式从面向内置资源,扩展到了面向所有自定义资源,以此来完成对复杂应用的管理。
具体怎么理解呢?我们来看一下 RedHat 官方对 Operator 设计理念的阐述:
Operator 设计理念 Operator是自定义的控制器
Operator 是使用自定义资源(CR,本人注:CR 即 Custom Resource,是 CRD 的实例)管理应用及其组件的自定义 Kubernetes 控制器。高级配置和设置由用户在 CR 中提供。Kubernetes Operator 基于嵌入在 Operator 逻辑中的最佳实践,将高级指令转换为低级操作。Kubernetes Operator 监视 CR 类型并采取特定于应用的操作,确保当前状态与该资源的理想状态相符。—— 什么是 Kubernetes Operator,RedHat
这段文字是直接由 RedHat 官方撰写并翻译成中文的,准确严谨,但比较拗口,对于没接触过 Operator 的人来说并不友好,比如,你可能就会问,什么叫做“高级指令”?什么叫做“低级操作”?它们之间具体如何转换呢?等等。
其实要理解这些问题,你必须先弄清楚有状态和无状态应用的含义及影响,然后再来理解 Operator 所做的工作。在上节课,我给你补充了一个“额外知识”,已经介绍过了二者之间的区别,现在我们再来看看:
有状态应用于无状态应用
有状态应用(Stateful Application)与无状态应用(Stateless Application)说的是应用程序是否要自己持有其运行所需的数据,如果程序每次运行都跟首次运行一样,不依赖之前任何操作所遗留下来的痕迹,那它就是无状态的;反之,如果程序推倒重来之后,用户能察觉到该应用已经发生变化,那它就是有状态的。
无状态应用在分布式系统中具有非常巨大的价值,我们都知道分布式中的 CAP 不兼容原理,如果无状态,那就不必考虑状态一致性,没有了 C,那 A 和 P 就可以兼得。换句话说,只要资源足够,无状态应用天生就是高可用的。但不幸的是,现在的分布式系统中,多数关键的基础服务都是有状态的,比如缓存、数据库、对象存储、消息队列,等等,只有 Web 服务器这类服务属于无状态。
站在 Kubernetes 的角度看,是否有状态的本质差异在于,有状态应用会对某些外部资源有绑定性的直接依赖,比如说,Elasticsearch 在建立实例时,必须依赖特定的存储位置,只有重启后仍然指向同一个数据文件的实例,才能被认为是相同的实例。另外,有状态应用的多个应用实例之间,往往有着特定的拓扑关系与顺序关系,比如 etcd 的节点间选主和投票,节点们都需要得知彼此的存在,明确每一个节点的网络地址和网络拓扑关系。
为了管理好那些与应用实例密切相关的状态信息,Kubernetes 从 1.9 版本开始正式发布了 StatefulSet 及对应的 StatefulSetController。与普通 ReplicaSet 中的 Pod 相比,由 StatefulSet 管理的 Pod 具备几项额外特性。
看到这些特性以后,你可能还会说,我还是不太理解 StatefulSet 的设计意图呀。没关系,我来举个例子,你一看就理解了。
如果把 ReplicaSet 中的 Pod 比喻为养殖场中的“肉猪”,那 StatefulSet 就是被当作宠物圈养的“荷兰猪”,不同的肉猪在食用功能上并没有什么区别,但每只宠物猪都是独一无二的,有专属于自己的名字、习性与记忆。事实上,早期的 StatefulSet 就曾经使用过 PetSet 这个名字。
StatefulSet 出现以后,Kubernetes 就能满足 Pod 重新创建后,仍然保留上一次运行状态的需求了。不过,有状态应用的维护并不仅限于此。比如说,对于一套 Elasticsearch 集群来说,通过 StatefulSet,最多只能做到创建集群、删除集群、扩容缩容等最基本的操作,并不支持常见的运维操作,比如备份和恢复数据、创建和删除索引、调整平衡策略,等等。
我再举个实际例子,来说明一下,Operator 是如何解决那些 StatefulSet 覆盖不到的有状态服务管理需求的。
假设我们要部署一套 Elasticsearch 集群,通常要在 StatefulSet 中定义相当多的细节,比如服务的端口、Elasticsearch 的配置、更新策略、内存大小、虚拟机参数、环境变量、数据文件位置,等等。
这里我直接把满足这个需求的 YAML 全部贴出来,让你对咱们前面反复提到的 Kubernetes 的复杂性有更加直观的理解。
apiVersion: v1
kind: Service
metadata:
name: elasticsearch-cluster
spec:
clusterIP: None
selector:
app: es-cluster
ports:
- name: transport
port: 9300
---
apiVersion: v1
kind: Service
metadata:
name: elasticsearch-loadbalancer
spec:
selector:
app: es-cluster
ports:
- name: http
port: 80
targetPort: 9200
type: LoadBalancer
---
apiVersion: v1
kind: ConfigMap
metadata:
name: es-config
data:
elasticsearch.yml: |
cluster.name: my-elastic-cluster
network.host: "0.0.0.0"
bootstrap.memory_lock: false
discovery.zen.ping.unicast.hosts: elasticsearch-cluster
discovery.zen.minimum_master_nodes: 1
xpack.security.enabled: false
xpack.monitoring.enabled: false
ES_JAVA_OPTS: -Xms512m -Xmx512m
---
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
name: esnode
spec:
serviceName: elasticsearch
replicas: 3
updateStrategy:
type: RollingUpdate
template:
metadata:
labels:
app: es-cluster
spec:
securityContext:
fsGroup: 1000
initContainers:
- name: init-sysctl
image: busybox
imagePullPolicy: IfNotPresent
securityContext:
privileged: true
command: ["sysctl", "-w", "vm.max_map_count=262144"]
containers:
- name: elasticsearch
resources:
requests:
memory: 1Gi
securityContext:
privileged: true
runAsUser: 1000
capabilities:
add:
- IPC_LOCK
- SYS_RESOURCE
image: docker.elastic.co/elasticsearch/elasticsearch:7.9.1
env:
- name: ES_JAVA_OPTS
valueFrom:
configMapKeyRef:
name: es-config
key: ES_JAVA_OPTS
readinessProbe:
httpGet:
scheme: HTTP
path: /_cluster/health?local=true
port: 9200
initialDelaySeconds: 5
ports:
- containerPort: 9200
name: es-http
- containerPort: 9300
name: es-transport
volumeMounts:
- name: es-data
mountPath: /usr/share/elasticsearch/data
- name: elasticsearch-config
mountPath: /usr/share/elasticsearch/config/elasticsearch.yml
subPath: elasticsearch.yml
volumes:
- name: elasticsearch-config
configMap:
name: es-config
items:
- key: elasticsearch.yml
path: elasticsearch.yml
volumeClaimTemplates:
- metadata:
name: es-data
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 5Gi
可以看到,这里面的细节配置非常多。其实,之所以会这样,根本原因在于 Kubernetes 完全不知道 Elasticsearch 是个什么东西。所有 Kubernetes 不知道的信息、不能启发式推断出来的信息,都必须由用户在资源的元数据定义中明确列出,必须一步一步手把手地“教会”Kubernetes 部署 Elasticsearch,这种形式就属于咱们刚刚提到的**“低级操作”**。
如果我们使用Elastic.co 官方提供的 Operator,那就会简单多了。Elasticsearch Operator 提供了一种kind: Elasticsearch的自定义资源,在它的帮助下,只需要十行代码,将用户的意图是“部署三个版本为 7.9.1 的 ES 集群节点”说清楚,就能实现跟前面 StatefulSet 那一大堆配置相同甚至是更强大的效果,如下面代码所示:
ps:我看来有点硬件精简指令集的意思
apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata:
name: elasticsearch-cluster
spec:
version: 7.9.1
nodeSets:
- name: default
count: 3
config:
node.master: true
node.data: true
node.ingest: true
node.store.allow_mmap: false
有了 Elasticsearch Operator 的自定义资源,就相当于 Kubernetes 已经学会怎样操作 Elasticsearch 了。知道了所有它相关的参数含义与默认值,就不需要用户再手把手地教了,这种就是所谓的“高级指令”。
Operator 将简洁的高级指令转化为 Kubernetes 中具体操作的方法,跟 Helm 或 Kustomize 的思路并不一样:
通过程序编码来扩展 Kubernetes,比只通过内置资源来与 Kubernetes 打交道要灵活得多。比如,在需要更新集群中某个 Pod 对象的时候,由 Operator 开发者自己编码实现的控制器,完全可以在原地对 Pod 进行重启,不需要像 Deployment 那样,必须先删除旧 Pod,然后再创建新 Pod。
使用 CRD 定义高层次资源、使用配套的控制器来维护期望状态,带来的好处不仅仅是“高级指令”的便捷,更重要的是,可以在遵循 Kubernetes 的一贯基于资源与控制器的设计原则的同时,又不必受制于 Kubernetes 内置资源的表达能力。只要 Operator 的开发者愿意编写代码,前面提到的那些 StatfulSet 不能支持的能力,如备份恢复数据、创建删除索引、调整平衡策略等操作,都完全可以实现出来。
把运维的操作封装在程序代码上,从表面上看,最大的受益者是运维人员,开发人员要为此付出更多劳动。然而,Operator 并没有被开发者抵制,相反,因为用代码来描述复杂状态往往反而比配置文件更加容易,开发与运维之间的协作成本降低了,还受到了开发者的一致好评。
因为 Operator 的各种优势,它变成了近两、三年容器封装应用的一股新潮流,现在很多复杂分布式系统都有了官方或者第三方提供的 Operator(这里收集了一部分)。RedHat 公司也持续在 Operator 上面进行了大量投入,推出了简化开发人员编写 Operator 的Operator Framework/SDK。
目前看来,应对有状态应用的封装运维,Operator 也许是最有可行性的方案,但这依然不是一项轻松的工作。以etcd 的 Operator为例,etcd 本身不算什么特别复杂的应用,Operator 实现的功能看起来也相当基础,主要有创建集群、删除集群、扩容缩容、故障转移、滚动更新、备份恢复等功能,但是代码就已经超过一万行了。
现在,开发 Operator 的确还是有着相对较高的门槛,通常由专业的平台开发者而非业务开发或者运维人员去完成。但是,Operator 非常符合技术潮流,顺应了软件业界所提倡的 DevOps 一体化理念,等 Operator 的支持和生态进一步成熟之后,开发和运维都能从中受益,未来应该能成长为一种应用封装的主流形式。
我要给你介绍的最后一种应用封装的方案,是阿里云和微软在 2019 年 10 月上海 QCon 大会上联合发布的开放应用模型(Open Application Model,OAM),它不仅是中国云计算企业参与制定,甚至是主导发起的国际技术规范,也是业界首个云原生应用标准定义与架构模型。
OAM 思想的核心是将开发人员、运维人员与平台人员的关注点分离,开发人员关注业务逻辑的实现,运维人员关注程序的平稳运行,平台人员关注基础设施的能力与稳定性。毕竟,长期让几个角色厮混在同一个 All-in-One 资源文件里,并不能擦出什么火花,反而会将配置工作弄得越来越复杂,把“YAML Engineer”弄成容器界的嘲讽梗。
OAM 对云原生应用的定义是:“由一组相互关联但又离散独立的组件构成,这些组件实例化在合适的运行时上,由配置来控制行为并共同协作提供统一的功能”。你可能看不懂是啥意思,没有关系,为了方便跟后面的概念对应,我先把这句话拆解一下:
OAM 定义
OAM 定义的应用一个Application由一组Components构成,每个Component的运行状态由Workload描述,每个Component可以施加Traits来获取额外的运维能力,同时,我们可以使用Application Scopes将Components划分到一或者多个应用边界中,便于统一做配置、限制、管理。把Components、Traits和Scopes组合在一起实例化部署,形成具体的Application Configuration,以便解决应用的多实例部署与升级。
然后,我来具体解释一下上面列出来的核心概念,来帮助你理解 OAM 对应用的定义。在这句话里面,每一个用英文标注出来的技术名词,都是 OAM 在 Kubernetes 的基础上扩展而来的概念,每一个名词都有相对应的自定义资源。换句话说,它们并不是单纯的抽象概念,而是可以被实际使用的自定义资源。
我们来看一下这些概念的具体含义。
OAM 使用咱们刚刚所说的这些自定义资源,对原先 All-in-One 的复杂配置做了一定层次的解耦:
这样,不同角色分工协作,就整体简化了单个角色关注的内容,让不同角色可以更聚焦、更专业地做好本角色的工作,整个过程如下图所示:
事实上,OAM 未来能否成功,很大程度上取决于云计算厂商的支持力度,因为 OAM 的自定义资源一般是由云计算基础设施负责解释和驱动的,比如阿里云的EDAS就已内置了 OAM 的支持。
如果你希望能够应用在私有 Kubernetes 环境中,目前,OAM 的主要参考实现是Rudr(已声明废弃)和Crossplane。Crossplane 是一个仅发起一年多的 CNCF 沙箱项目,主要参与者包括阿里云、微软、Google、RedHat 等工程师。Crossplane 提供了 OAM 中全部的自定义资源和控制器,安装以后,就可以用 OAM 定义的资源来描述应用了。
今天,容器圈的发展是一日千里,各种新规范、新技术层出不穷。在这两节课里,我特意挑选了非常流行,而且有代表性的四种,分别是“无状态应用”的典型代表 Kustomize 和 Helm,和“有状态应用”的典型代表 Operator 和 OAM。
其他我没有提到的应用封装技术,还有CNAB、Armada、Pulumi,等等。这些封装技术会有一定的重叠之处,但并非都是重复的轮子,在实际应用的时候,往往会联合其中多个工具一起使用。而至于怎么封装应用才是最佳的实践,目前也还没有定论,但可以肯定的是,以应用为中心的理念已经成为了明确的共识。