自己的 Kubernetes 控制器(1)——工作准备

原文:Your own Kubernetes controller - Laying out the work

作者:Nicolas Fränkel

时至今日,Kubernetes 已经成为容器化应用部署的首选平台,是个难以忽视的存在。

Kubernetes是一个开源系统,用于自动化部署、扩展和管理容器化应用程序。

短短几年里,Kubernetes 在 CNCF 的大旗下高歌猛进,在 DevOps 领域已经深入人心。这其中的原因众说纷纭,其中一个非常有说服力的理由是,用户能够避免被锁定在单一云提供商的 API 上。如果你对 2000 年左右微软的桌面垄断有所了解,你可能会明白我的意思。

Kubernetes 的扩展相对来说比较容易,这是它获得广泛认同的一个重要原因。很多软件供应商在 Docker 镜像之外,还会提供一或多个 Operator。

我假设读者仅对 Kubernetes 有所了解,对控制器一无所知,在这个假设的基础上,我将用三篇连载来讲述如何使用 Go 以外的语言实现自己的控制器。

控制器是什么

配置管理工具可以分为两种:

分类 描述 工具
指令式 指定做事方法,例如启动两个节点 Ansible、SaltStack 等
声明式 指定目标状态,例如总计五个节点 Puppet、Chef 等

声明式的工具通常会周期性的执行以下任务:

  1. 查询当前状态
  2. 评估要从当前状态达到目标状态所需完成的步骤
  3. 执行这些步骤

这个算法描述的是一个控制回路。

Kubernetes 里,已经有了这些控制回路的实现。例如 ReplicaSetDeployment。这两个对象都可以针对特定镜像设置目标 Pod 数量。Kubernetes 会持续生成副本,直到达到预设的实例数量。如果副本数量发生变化,那么就会新建或删除副本,以达到目标副本数量。

现在你可能已经猜到了,控制器就是一个控制循环的实现:检查当前状态,用现有状态计算差异,弥补差异。除了 DeploymentReplicaSet 的控制器之外,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 架构图:

k8s arch

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]

这个通信过程的需求很简单:

  1. 能够处理 HTTP 的请求和响应
  2. 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 通信。在下一篇帖子中,我们将详细介绍并实际开发自己的自定义控制器。

Avatar
崔秀龙

简单,是大师的责任;我们凡夫俗子,能做到清楚就很不容易了。

comments powered by Disqus
下一页
上一页

相关