K8s 设计模式
Kubernetes 是一个具备普遍意义的容器编排工具,它提供了一套基于容器构建分布式系统的根底依赖,其意义等同于 Linux 在操作系统中的位置,能够认为是分布式的操作系统。
自定义资源
K8s 提供了 Pod、Service、Volume 等一系列根底资源定义,为了更好提供扩展性,CRD 性能是在 1.7 版本被引入。
用户能够依据本人的需要增加自定义的 Kubernetes 对象资源(CRD)。值得注意的是,这里用户本人增加的 Kubernetes 对象资源都是 native 的都是一等公民,和 Kubernetes 中自带的、原生的那些 Pod、Deployment 是同样的对象资源。在 Kubernetes 的 API Server 看来,它们都是存在于 etcd 中的一等资源。同时,自定义资源和原生内置的资源一样,都能够用 kubectl 来去创立、查看,也享有 RBAC、平安性能。用户能够开发自定义控制器来感知或者操作自定义资源的变动。
Operator
在自定义资源根底上,如何实现自定义资源创立或更新时的逻辑行为,K8s Operator 提供了相应的开发框架。Operator 通过扩大 Kubernetes 定义 Custom Controller,list/watch 对应的自定义资源,在对应资源发生变化时,触发自定义的逻辑。
Operator 开发者能够像应用原生 API 进行利用治理一样,通过申明式的形式定义一组业务利用的冀望终态,并且依据业务利用的本身特点进行相应控制器逻辑编写,以此实现对利用运行时刻生命周期的治理并继续保护与冀望终态的一致性。
艰深的了解
CRD 是 K8s 标准化的资源扩大能力,以 java 为例,int、long、Map、Object 是 java 内置的类,用户能够自定义 Class 实现类的扩大,CRD 就是 K8s 中的自定义类,CR 就是对应类的一个 instance。
Operator 模式 = 自定义类 + 观察者模式,Operator 模式让大家编写 K8s 的扩大变得非常简单快捷,逐步成为面向 K8s 设计的规范。
Operator 提供了标准化的设计流程:
应用 SDK 创立一个新的 Operator 我的项目
通过增加自定义资源(CRD)定义新的资源 API
指定应用 SDK API 来 watch 的资源
自定义 Controller 实现 K8s 协调(reconcile)逻辑
有了锤子,看到的只有钉子
咱们团队(KubeOne 团队)始终在致力于解决简单中间件利用如何部署到 K8s,天然也是 Operator 模式的践行者。经验了近 2 年的开发,初步解决了中间件在各个环境 K8s 的部署,以后两头也走了很多弯路,踩了很多坑。
KubeOne 内核也经验 3 个大版本的迭代,前 2 次开发过程根本都是 follow Operator 规范开发流程进行开发设计。遵循一个规范的、典型的 Operator 的设计过程,看上去一切都是这么的完满,然而每次设计都十分苦楚,践行 Operator 模式之后,最值得反思和借鉴的就是”有了锤子,看到的只有钉子“,简略总结一下就是 4 个所有:
所有设计皆 yaml
所有皆合一
所有皆终态
所有交互皆 cr
误区 1 所有设计皆 yaml
K8s 的 API 是 yaml 格局,Operator 设计流程也是让大家首先定义 crd,所以团队开始设计时间接采纳了 yaml 格局。
案例
依据标准化流程,团队面向 yaml 设计流程大体如下:
先依据已知的数据初步整顿一个大而全的 yaml,做一下初步的分类,例如利用大略蕴含根底信息,依赖服务,运维逻辑,监控采集等,每个分类做一个子局部
开会讨论具体的内容是否能满足要求,后果每次散会都难以造成共识
因为总是有新的需要满足不了,在探讨 A 时,就有人提到 B、C、D,一直有新的需要
每个局部的属性十分难对立,因为不同的实现属性差别较大
了解不统一,雷同名字但应用时每个人的了解也不同
因为工期很紧,只能长期斗争,做一个两头态,前面再进一步优化
后续优化降级,雷同的流程再来一遍,还是很难造成共识
这是第 2 个版本的设计:
apiVersion: apps.mwops.alibaba-inc.com/v1alpha1
kind: AppDefinition
metadata:
labels:
app: "A"
name: A-1.0 //chart-name+chart-version
namespace: kubeone
spec:
appName: A //chart-name
version: 1.0 //chart-version
type: apps.mwops.alibaba-inc.com/v1alpha1.argo-helm
workloadSettings: // 注 workloadSettings 标识 type 应该应用的属性
- name: "deployToK8SName"
value: ""- name:"deployToNamespace"
value: ${resources:namespace-resource.name}
parameterValues: // 注 parameterValues 标识业务属性
- name: "enableTenant"
value: "1"
- name: "CPU"
value: "1"
- name: "MEM"
value: "2Gi"
- name: "jvm"
value: "flag;gc"
- name: vip.fileserver-edas.ip
value: ${resources:fileserver_edas.ip}
- name: DB_NAME
valueFromConfigMap:
name: ${resources:rds-resource.cm-name}
expr: ${database}
- name: DB_PASSWORD
valueFromSecret:
name: ${instancename}-rds-secret
expr: ${password}
- name: object-storage-endpoint
value: ${resources:object-storage.endpoint}
- name: object-storage-username
valueFromSecret:
name: ${resources:object-storage.secret-name}
expr: ${username}
- name: object-storage-password
valueFromSecret:
name: ${resources:object-storage.secret-name}
expr: ${password}
- name: redis-endpoint
value: ${resources:redis.endpoint}
- name: redis-password
value: ${resources:redis.password}
resources:
- name: tolerations
type: apps.mwops.alibaba-inc.com/tolerations
parameterValues:
- name: key
value: "sigma.ali/is-ecs"
- name: key
value: "sigma.ali/resource-pool"
- name: namespace-resource
type: apps.mwops.alibaba-inc.com/v1alpha1.namespace
parameterValues:
- name: name
value: edas
- name: fileserver-edas
type: apps.mwops.alibaba-inc.com/v1alpha1.database.vip
parameterValues:
- name: port
value: 21,80,8080,5000
- name: src_port
value: 21,80,8080,5000
- name: type
value: ClusterIP
- name: check_type
value: ""
- name: uri
value: ""
- name: ip
value: ""
- name: test-db
type: apps.mwops.alibaba-inc.com/v1alpha1.database.mysqlha
parameterValues:
- name: name
value: test-db
- name: user
value: test-user
- name: password
value: test-passwd
- name: secret
value: test-db-mysqlha-secret
- name: service-slb
type: apps.mwops.alibaba-inc.com/v1alpha1.slb
mode: post-create
parameterValues:
- name: service
value: "serviceA"
- name: annotations
value: "app:a,version:1.0"
- name: external-ip
value:
- name: service-resource2
type: apps.mwops.alibaba-inc.com/v1alpha1.service
parameterValues:
- name: second-domain
value: edas.console
- name: ports
value: "80:80"
- name: selectors
value: "app:a,version:1.0"
- name: type
value: "loadbalance"
- name: service-dns
type: apps.mwops.alibaba-inc.com/v1alpha1.dns
parameterValues:
- name: domain
value: edas.server.${global:domain}
- name: vip
value: ${resources:service-resource2.EXTERNAL-IP}
- name: dns-resource
type: apps.mwops.alibaba-inc.com/v1alpha1.dns
parameterValues:
- name: domain
value: edas.console.${global:domain}
- name: vip
value:“127.0.0.1”- name: cni-resource
type: apps.mwops.alibaba-inc.com/v1alpha1.cni
parameterValues:
- name: count
value: 4
- name: ip_list
value:
- name: object-storage
type: apps.mwops.alibaba-inc.com/v1alpha1.objectStorage.minio
parameterValues:
- name: namespace
value: test-ns
- name: username
value: test-user
- name: password
value: test-password
- name: storage-capacity
value: 20Gi
- name: secret-name
value: minio-my-store-access-keys
- name: endpoint
value: minio-instance-external-service
- name: redis
type: apps.mwops.alibaba-inc.com/v1alpha1.database.redis
parameterValues:
- name: cpu
value: 500m
- name: memory
value: 128Mi
- name: password
value: i_am_a_password
- name: storage-capacity
value: 20Gi
- name: endpoint
value: redis-redis-cluster
- name: accesskey
type: apps.mwops.alibaba-inc.com/v1alpha1.accesskey
parameterValues:
- name: name
value: default
- name: userName
value: ecs_test@aliyun.com
exposes:
- name: dns
value: ${resources:dns-resource.domain}
- name: db-endpoint
valueFromConfigmap:
name: ${resources:rds-resource.cm-name}
expr: ${endpoint}:3306/${database}
- name: ip_list
value: ${resources:cni-resource.ip_list}
- name: object-storage-endpoint
value: ${resources:object-storage.endpoint}.${resource:namespace-resource.name}
- name: object-storage-username
valueFromSecret:
name: ${resources:object-storage.secret-name}
expr: ${username}
- name: object-storage-password
valueFromSecret:
name: ${resources:object-storage.secret-name}
expr: ${password}
- name: redis-endpoint
value: ${resources:redis.endpoint}.${resource:namespace-resource.name}
- name: redis-password
value: ${resources:redis.password}
反思
这样的苦楚难以用语言表达,感觉所有都脱离了掌控,没有对立的判断规范,设计标准,公说公有理婆说婆有理,内容始终加,字段始终改。事不过三,第三次设计时,咱们个体探讨反思为什么这么难造成共识?为什么每个人了解不同?为什么总是在改?
论断很统一,没有面向 yaml 的设计,只有面向对象的设计,设计语言也只有 UML,只有这些历经考验、成熟的设计方法论,才是最简略也是最高效的。
从下面那个一个微小无比的 yaml 大家能够领会咱们设计的简单,然而这还是不是最苦楚的。最苦楚的是大家摈弃了原有的设计流程及设计语言,试图应用一个凋谢的 Map 来形容所有。当设计没有对象,也没有关系,只剩下 Map 里一个个属性,也就无所谓对错,也无所谓优劣。最初争来争去,最初不过是再加一个字段,争了一个寂寞。
适用范围
那 Operator 先设计 CRD,再开发 controller 的形式不正确吗?
答案:局部正确
实用场景
与 Java Class 雷同,简略对象不须要通过简单的设计流程,间接设计 yaml 简略高效。
不实用场景
在设计一个简单的体系时,例如:利用治理,蕴含多个对象且对象之间有简单的关系,有简单的用户故事,UML 和面向对象的设计就显得十分重要。
设计时只思考 UML 和畛域语言,设计实现后,CRD 能够认为是 java 的 Class,或者是数据库的表构造,只是最终要实现时的一种抉择。而且有很多对象不须要长久化,也不须要通过 Operator 机制触发对应的逻辑,就不须要设计 CRD,而是间接实现一个 controller 即可。
yaml 是接口或 Class 申明的一种格式化表白,惯例 yaml 要尽可能小,尽可能职责繁多,尽可能形象。简单的 yaml 是对简略 CRD 资源的一种编排后果,提供相似一站式资源配套方案。
在第 3 个版本及 PaaS-Core 设计时,咱们就采取了如下的流程:
UML 用例图
梳理用户故事
基于用户故事对齐 Domain Object,确定要害的业务对象以及对象间关系
须要 Operator 化的对象,每个对象形容为一个 CRD,当然 CRD 不足接口、继承等面向对象的能力,能够通过其余形式曲线表白
不须要 Operator 化的对象,间接编写 Controller
误区 2 所有皆合一
为了保障一个利用的终态,或者为了应用 gitops 治理一个利用,是否应该把利用相干的内容都放入一个 CRD 或一个 IAC 文件?依据 gitops 设计,每次变更时须要下发整个文件?
案例
案例 1: 利用 WordPress,须要依赖一个 MySQL,终态如何定义?
apiVersion: apps.mwops.alibaba-inc.com/v1alpha1
kind: AppDefinition
metadata:
labels:
app: "WordPress"
name: WordPress-1.0 //chart-name+chart-version
namespace: kubeone
spec:
appName: WordPress //chart-name
version: 1.0 //chart-version
type: apps.mwops.alibaba-inc.com/v1alpha1.argo-helm
parameterValues: // 注 parameterValues 标识业务属性
- name: "enableTenant"
value: "1"
- name: "CPU"
value: "1"
- name: "MEM"
value: "2Gi"
- name: "jvm"
value: "flag;gc"
- name: replicas
value: 3
- name: connectstring
valueFromConfigMap:
name: ${resources:test-db.exposes.connectstring}
expr: ${connectstring}
- name: db_user_name
valueFromSecret:
....
resources:
- name: test-db // 创立一个新的 DB
type: apps.mwops.alibaba-inc.com/v1alpha1.database.mysqlha
parameterValues:
- name: cpu
value: 2
- name: memory
value: 4G
- name: storage
value: 20Gi
- name: username
value: myusername
- name: password
value: i_am_a_password
- name: dbname
value: wordPress
exposes:
- name: connectstring
- name: username
- name: password
exposes:
- name: dns
value: ...
上方的代码是 wordPress 利用的终态吗?这个文件蕴含了利用所须要的 DB 的定义和利用的定义,只有一次下发就能够先创立对应的数据库,再把利用拉起。
案例 2:每次变更时,间接批改整个 yaml 的局部内容,批改后间接下发到 K8s,引起不必要的变更。例如:要从 3 个节点扩容到 5 个节点,批改下面 yaml 文件的 replicas 之后,须要下发整个 yaml。整个下发的 yaml 通过二次解析成底层的 StatefulSet 或 Deployment,解析逻辑降级后,可能会产生不合乎预期的变动,导致所有 pod 重建。
反思
先答复第一个问题,上方 yaml 文件不是利用的终态,而是一个编排,此编排蕴含了 DB 的定义和利用的定义。利用的终态只应该蕴含本人必须的依赖援用,而不蕴含依赖是如何创立的。因为这个依赖援用能够是新创建的,也能够是一个已有的,也能够是手工填写的,依赖如何创立与利用终态无关。
apiVersion: apps.mwops.alibaba-inc.com/v1alpha1
kind: AppDefinition
metadata:
labels:
app: "WordPress"
name: WordPress-1.0 //chart-name+chart-version
namespace: kubeone
spec:
appName: WordPress //chart-name
version: 1.0 //chart-version
name: WordPress-test
type: apps.mwops.alibaba-inc.com/v1alpha1.argo-helm
parameterValues: // 注 parameterValues 标识业务属性
- ....
resources:
- name: test-db-secret
value: "wordPress1Secret" // 援用已有的 secret
exposes:
- name: dns
value: ...
创立一个利用,就不能先创立 db,再创立利用吗?
能够的,多个对象之间依赖是通过编排实现的。编排有单个利用创立的编排,也有一个简单站点创立的编排。以 Argo 为例。
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
generateName: wordPress-
spec:
templates:
-
name: wordPress
steps:
# 创立 db-
-
name: wordpress-db
template: wordpress-db
arguments:parameters: [{name: wordpress-db1}]
-
创立利用
-
- name:
template: wordpress
arguments:
parameters: [{db-sercet: wordpress-db1}]
- name:
-
针对第 2 个案例,是否每次交互都须要下发全副残缺的 yaml?
答案:
编排是一次性的配置,,编排文件下发一次之后,后续操作都是操作单个对象,例如:变更时,只会独自变更 wordPress,或独自变更 wordPressDB,而不会一次性同时变更 2 个对象。
独自变更利用时,是否须要下发整个终态 yaml,这个要依据理论状况进行设计,值得大家思考。前面会提出针对整个利用生命周期状态机的设计,外面有具体的解释。
适用范围
实用场景
CRD 或 Iac 定义时,单个对象的终态只应该蕴含本身及对依赖的援用。与面向对象的设计雷同,咱们不应该把所有类的定义都放到一个 Class 外面。
不实用场景
多个对象要一次性创立,并且须要依照程序创立,存在依赖关系,须要通过编排层实现。
误区 3 所有皆终态
体验了 K8s 的终态化之后,大家在设计时言必称终态,好像不能用上终态设计,不下发一个 yaml 申明对象的终态就是掉队,就是上一代的设计。
案例
案例 1:利用编排 还是以 WordPress 为例,将 WordPressDB 和 WordPress 放在一起进行部署,先部署 DB,再创立利用。示例 yaml 同上。
案例 2:利用公布 利用第一次部署及后续的降级间接下发一个残缺的利用 yaml,零碎会主动帮你达到终态。但为了可能细粒度管制公布的流程,致力在 Deployment 或 StatefulSet 上下功夫,进行 partition 的管制,试图在终态里减少一点点的交互性。
反思
说到终态,必然要提到命令式、申明式编程,终态其实就是申明式最终的执行后果。咱们先回顾一下命令式、终态式编程。
命令式编程
命令式编程的次要思维是关注计算机执行的步骤,即一步一步通知计算机先做什么再做什么。
比方:如果你想在一个数字汇合 collection(变量名) 中筛选大于 5 的数字,你须要这样通知计算机:
- 第一步,创立一个存储后果的汇合变量 results;
- 第二步,遍历这个数字汇合 collection;
- 第三步:一个一个地判断每个数字是不是大于 5,如果是就将这个数字增加到后果汇合变量 results 中。
代码实现如下:
List results = new List();
foreach(var num in collection)
{
if (num > 5)
results.Add(num);
}
很显著,这个样子的代码是很常见的一种,不论你用的是 C, C++ 还是 C#, Java, Javascript, BASIC, Python, Ruby 等等,你都能够以这个形式写。
申明式编程
申明式编程是以数据结构的模式来表白程序执行的逻辑。它的次要思维是通知计算机应该做什么,但不指定具体要怎么做。
SQL 语句就是最显著的一种申明式编程的例子,例如:
SELECT * FROM collection WHERE num > 5
除了 SQL,网页编程中用到的 HTML 和 CSS 也都属于申明式编程。
通过观察申明式编程的代码咱们能够发现它有一个特点是它不须要创立变量用来存储数据。
另一个特点是它不蕴含循环管制的代码如 for,while。
换言之
• 命令式编程:命令“机器”如何去做事件(how),这样不论你想要的是什么(what),它都会依照你的命令实现。
• 申明式编程:通知“机器”你想要的是什么(what),让机器想出如何去做(how)。
当接口越是在表白“要什么”,就是越申明式;越是在表白“要怎么”,就是越命令式。SQL 就是在表白要什么(数据),而不是表白怎么弄出我要的数据,所以它就很“申明式”。
简略的说,接口的表述形式越靠近人类语言——词汇的串行连贯(一个词汇实际上是一个概念)——就越“申明式”;越靠近计算机语言——“程序 + 分支 + 循环”的操作流程——就越“命令式”。
越是申明式,意味着上层要做更多的货色,或者说能力越强,也意味着效率的损失。越是命令式,意味着下层对上层有更多的操作空间,能够依照本人特定的需要要求上层依照某种形式来解决。
简略的讲,Imperative Programming Language (命令式语言)个别都有 control flow, 并且具备能够和其余设施进行交互的能力。而 Declarative Programming language (申明式语言) 个别做不到这些。
基于以上的剖析,编排或工作流实质是一个流程性管制的过程,个别是一次性的过程,无需强行终态化,而且建站编排执行完结后,不能放弃终态,因为后续会依据单个利用进行公布和降级。案例 1 是一个典型的编排,只是一次性的创立了 2 个对象 DB 和利用的终态。
利用公布其实是通过一个公布单或工作流,管制 2 个不同版本的利用节点和流量的终态化的过程,不应该是利用终态的一部分,而是一个独立的管制流程。
适用范围
申明式或终态设计
实用场景
无过多交互,无需关注底层实现的场景,即把申明提供给零碎后,零碎会自动化达到申明所要求的状态,而不须要人为干涉。
不实用场景
一次性的流程编排,有频繁交互的管制流程
命令式和申明式本就是 2 种互补的编程模式,就像有了面向对象之后,有人就鄙视面向过程的编程,当初有了申明式,就开始鄙视命令式编程,那一屋!
误区 4 所有交互皆 cr
因为 K8s 的 API 交互只能通过 yaml,导致大家的设计都以 cr 为核心,所有的交互都设计为下发一个 cr,通过 watch cr 触发对应的逻辑。
案例
调用一个 http 接口或 function,须要下发一个 cr
利用 crud 都下发残缺 cr
反思
案例 1
是否所有的逻辑都须要下发一个 cr?
下发 cr 其实做了比拟多的事件,流程很长,效率并不高,流程如下:
通过 API 传入 cr,cr 保留到 etcd
触发 informer
controller 接管到对应的事件,触发逻辑
更新 cr 状态
清理 cr,否则会占用 etcd 存储
如果须要频繁的调用对应的接口,尽量通过 sdk 间接调用。
案例 2
K8s 对 yaml 操作命令有 create、apply、patch、delete、get 等,但一个利用的生命周期状态机不只是这几个命令能够涵盖,咱们比拟一下利用状态机(上)和 yaml 状态机(下):
不同的有状态利用,在收到不同的指令,须要触发不同的逻辑,例如:MQ 在收到 stop 指令时,须要先停写,检查数据是否生产实现。如果只是通过 yaml 状态机是无奈涵盖利用状态机相干的 event,所以咱们必须突破下发 cr 的模式。对于利用来说,现实的交互方式是通过 event driven 利用状态机的变动,状态产生变换时触发对应的逻辑。
适用范围
实用场景
须要长久化,放弃终态的数据
不实用场景
高频的服务调用,无需长久化的数据
简单状态机的驱动
总结
K8s 给咱们关上了一扇门,带给了咱们很多优良的设计,优良的理念,然而这些设计和理念也是有本人的实用的场景,并不是放之四海而皆准。咱们不应该盲从,试图所有都要 follow k8s 的设计和规定,而摈弃之前的优良设计理念。
软件设计经验了 10 多年的倒退,造成了一套卓有成效的设计方法论,k8s 也是在这些设计方法论的反对下设计进去的。取其精华去其糟粕,是咱们程序员应该做的事件。
原文链接
本文为阿里云原创内容,未经容许不得转载。