自己的 Kubernetes 控制器(1)——工作准备
原文:Your own Kubernetes controller - Laying out the work
时至今日,Kubernetes 已经成为容器化应用部署的首选平台,是个难以忽视的存在。
Kubernetes是一个开源系统,用于自动化部署、扩展和管理容器化应用程序。
短短几年里,Kubernetes 在 CNCF 的大旗下高歌猛进,在 DevOps 领域已经深入人心。这其中的原因众说纷纭,其中一个非常有说服力的理由是,用户能够避免被锁定在单一云提供商的 API 上。如果你对 2000 年左右微软的桌面垄断有所了解,你可能会明白我的意思。
Kubernetes 的扩展相对来说比较容易,这是它获得广泛认同的一个重要原因。很多软件供应商在 Docker 镜像之外,还会提供一或多个 Operator。
我假设读者仅对 Kubernetes 有所了解,对控制器一无所知,在这个假设的基础上,我将用三篇连载来讲述如何使用 Go 以外的语言实现自己的控制器。
控制器是什么
配置管理工具可以分为两种:
分类 | 描述 | 工具 |
---|---|---|
指令式 | 指定做事方法,例如启动两个节点 | Ansible、SaltStack 等 |
声明式 | 指定目标状态,例如总计五个节点 | Puppet、Chef 等 |
声明式的工具通常会周期性的执行以下任务:
- 查询当前状态
- 评估要从当前状态达到目标状态所需完成的步骤
- 执行这些步骤
这个算法描述的是一个控制回路。
Kubernetes 里,已经有了这些控制回路的实现。例如 ReplicaSet
和 Deployment
。这两个对象都可以针对特定镜像设置目标 Pod 数量。Kubernetes 会持续生成副本,直到达到预设的实例数量。如果副本数量发生变化,那么就会新建或删除副本,以达到目标副本数量。
现在你可能已经猜到了,控制器就是一个控制循环的实现:检查当前状态,用现有状态计算差异,弥补差异。除了 Deployment
和 ReplicaSet
的控制器之外,Kubernetes 还提供了很多开箱即用的控制器。
- Service
- DeamonSet
- PersistentVolume
- Job
- …
其实大多数的 Kubernetes 资源都是由控制器管理的。
初识 Operator
对控制器感兴趣的读者,可能已经在搜索过程中偶然发现了 Operator 这个名词。如果你的时间非常有限,我建议你跳过这一部分,将这两个术语视为近义词即可。
前面说到 Kubernetes 的扩展性。其中一个扩展方法就是创建控制器,这也是本文的的重点内容。另一个方式就是对 Kubernetes 模型本身进行扩展:在开箱即用的 Pod、Job 等内置资源以外,还可以使用 CRD 来提供额外的资源类型。
例如下面的代码定义了一个叫做 Hazelcast
的资源:
hazelcast-crd.yml
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: hazelcasts.hazelcast.com
spec:
group: hazelcast.com
names:
kind: Hazelcast
listKind: HazelcastList
plural: hazelcasts
singular: hazelcast
scope: Namespaced
subresources:
status: {}
versions:
- name: v1alpha1
served: true
storage: true
把文件提交给 API Server,让 Kubernetes 注册这个新的 Hazelcast
CRD。
kubectl apply -f hazelcast-crd.yml
这个动作完成之后,就可以像其他内置资源一样进行常用操作了:
kubectl get hazelcasts
Operator
就是一个用于某种 CRD 的控制器。如果知道怎么实现控制器,也就能够创建 Operator 了。
控制器的需求
现在我们看看 Kubernetes 控制器的需求。
控制器的部署位置
下图是一个简化的 Kubernetes 架构图:
Kubernetes 的内置控制器是其控制平面的组成部分。然而自定义控制器是不会出现在这里(Controller Manager)的。控制器没什么限制,它可以在集群内部以 Pod 的形式运行,也可以作为独立的外部进程。
当然 Pod 形式会享受各种 Kubernetes 上运行容器化应用的福利,例如自愈等。
和 Kubernetes 的通信
在 Kubernetes 中,API Server 是一个通信组件。客户端发送 HTTP 请求,API Server 处理请求后发回响应。给 kubectl
加上参数就能观察到这一过程:
$ kubectl get pods --v=8
I0209 12:36:31.330067 13717 round_trippers.go:420] GET https://192.168.99.103:8443/api/v1/namespaces/default/pods?limit=500
I0209 12:36:31.330078 13717 round_trippers.go:427] Request Headers:
I0209 12:36:31.330081 13717 round_trippers.go:431] Accept: application/json;as=Table;v=v1beta1;g=meta.k8s.io, application/json
I0209 12:36:31.330085 13717 round_trippers.go:431] User-Agent: kubectl/v1.17.2 (darwin/amd64) kubernetes/59603c6
I0209 12:36:31.339770 13717 round_trippers.go:446] Response Status: 200 OK in 9 milliseconds
I0209 12:36:31.339780 13717 round_trippers.go:449] Response Headers:
I0209 12:36:31.339798 13717 round_trippers.go:452] Content-Length: 2933
I0209 12:36:31.339804 13717 round_trippers.go:452] Date: Sun, 09 Feb 2020 11:36:31 GMT
I0209 12:36:31.339822 13717 round_trippers.go:452] Content-Type: application/json
I0209 12:36:31.340084 13717 request.go:1017] Response Body:
{ "kind":"Table",
"apiVersion":"meta.k8s.io/v1beta1",
"metadata":{
"selfLink":"/api/v1/namespaces/default/pods",
"resourceVersion":"2387836" },
"columnDefinitions":[
{ "name":"Name",
"type":"string",
"format":"name",
"description":"Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: http://kubernetes.io/docs/user-guide/identifiers#names",
"priority":0 },
{ "name":"Ready",
"type":"string",
"format":"",
"description":"The aggregate readiness state of this pod for accepting traffic.",
"priority":0 },
{ "name":"Status",
"type":"string",
"format":"",
"description":"The aggregate status of the containers in this pod.",
"priority":0 },
{ "name":"Restarts",
"type":"integer",
"format":"",
"description":"The number of times the containers in this pod have been restarted.",
"priority":0 },
{ "name":"Age",
"type":"stri
[truncated 1909 chars]
这个通信过程的需求很简单:
- 能够处理 HTTP 的请求和响应
- JSON 解析(或者说序列化和反序列化)
是的,有 JSON 和 HTTP 的处理能力就够了,所以要编写一个控制器,并不一定必须使用特定语言(例如 Go),理论上用单纯的 Shell 也是可以实现的。
Go 的定位
在进入实现细节之前,首先要看看 Kubernetes 的生态。
历史上好像 Kubernetes 的祖先是用 Java 开发的,后来被移植到了 Go 上。这可能是部分代码不符合 Go 语言风格的原因。尽管 Go 具有垃圾收集功能,但它还是被称为一种低级语言,很适合运行接近于裸机的软件。这种说法是否成立,远远超出了本文的范围,也超出了我的能力。
然而 Kubernetes 生态中大量软件是使用 Go 语言编写的,我想是有其原因的。
如果你已经对 Go 相当了解,那么继续使用是个很好的选择——改弦易辙需要勇气。这并不只是一个语言的问题,除了语法之外,还有很多其他内容:
要多久才能用新语言写出地道的代码
我记得我在学习 Java 的时候,读过 C 语言开发者写的代码。虽然语法是 Java,但是却写出了 C 语言的风格,例如在方法结束之前释放本地变量的引用。
多久才能搞清楚在什么条件下使用什么库
我不了解 Go,但是我知道 Java。Java 生态的丰富是人所皆知的。例如测试的场景,就有 JUnit 4、JUnit 5 以及 TestNG 可以选择,另外需要加入断言库么?这还只是测试呢。
选择正确的工具链要多久
如果已经在使用 JetBrains 的产品,那么从 JetBrains IDE 之间跳转是比较容易的,例如 IDEA 和 GoLand。但是 IDE 市场非常混乱,例如微软正在推广的包含丰富插件的 VS Code。而 Java 世界中,Eclipse 仍然占据客观的市场份额。各种产品都有自己的优劣,自己的拥趸。工具的选择可能在组织内部引发圣战。
新工具形成生产力要多久
各种 IDE 都有各自的玩法。例如我从 Eclipse 切换到 IntelliJ 的过程中,几个星期后才停掉了频繁保存文件的习惯。除了 IDE 之外,还有除错工具等。新的语言能怎么除错?有什么先决条件么?
另外前面说的几个点只是开发,如果考虑到相关的构建、集成和投产环境,其投入可能又会有数倍的增长。
我希望上面几点能够让读者意识到,语言的切换事关重大。在很多情况下,沿用原有的语言可能是个更好的选择。
结论
本文的第一部分,大概了解了一下 Kubernetes 控制器的基础内容。我们详细介绍了什么是控制器,以及开发控制器的需要:即能够与 HTTP/JSON 通信。在下一篇帖子中,我们将详细介绍并实际开发自己的自定义控制器。