初始化容器(Init Container)通过为初始化相关的任务提供不同于主应用容器的独立生命周期,实现了关注点的分离。在本章中,我们将仔细研究这个基本的 Kubernetes 概念,当需要初始化逻辑时,这个概念被用于许多其他模式中。

问题描述

初始化是许多编程语言中普遍关注的问题。有些语言把它作为语言的一部分,有些语言则使用命名惯例和模式来表示一个构造作为初始化器。例如,在 Java 程序语言中,为了实例化一个需要一些设置的对象,我们使用构造函数(Constructor)(或者对于更高级的使用情况,使用静态块)。构造函数被保证作为对象中的第一件事来运行,并且它们被管理运行时保证只运行一次(这只是一个例子,我们在这里不详细介绍不同的语言和角落情况)。此外,我们还可以使用构造函数来验证前提条件,比如强制参数。我们还可以使用构造函数用传入的参数或默认值来初始化实例字段。

:::info 初始化容器也是类似的,但在 Pod 级别而不是类级别。因此,如果你在 Pod 中拥有一个或多个代表你的主应用程序的容器,这些容器在启动前可能有先决条件。这些可能包括在文件系统上设置特殊的任务,数据库模式设置,或应用程序种子数据安装。此外,这种初始化逻辑可能需要不能包含在应用程序镜像中的工具和库。出于安全原因,应用程序镜像可能没有执行初始化活动的权限。另外,您可能希望推迟应用程序的启动,直到满足外部依赖性。对于所有这些类型的用例,Kubernetes 使用 Init 容器作为实现。 :::

这种模式,允许将初始化活动从主要的应用职责中分离出来。

解决方案

Kubernetes 中的 Init 容器是 Pod 定义的一部分,它们将一个 Pod 中的所有容器分成两组:Init 容器和应用容器。所有的 Init 容器都是按顺序一个个执行的,在应用容器启动之前,所有的 Init 容器都必须成功终止。从这个意义上说,Init 容器就像Java类中的构造函数指令,帮助对象初始化。另一方面,应用容器是并行运行的,启动顺序是任意的。执行流程如图 14-1 所示。
image.png
图 14-1 Pod 中的 Init 容器合应用容器

:::tips 通常情况下,Init 容器应该是小的,快速运行,并且完全成功,除非当一个 Init 容器被用来延迟 Pod 的启动,同时等待一个依赖关系,在这种情况下,它可能不会终止,直到依赖关系得到满足。如果一个 Init 容器失败了,整个 Pod 将被重新启动(除非它被标记为 RestartNever),导致所有 Init 容器再次运行。因此,为了防止任何副作用,使 Init 容器成为幂等是一个很好的做法。 :::

一方面,Init 容器具有与应用容器相同的所有功能:所有的容器都是同一个 Pod 的一部分,所以它们共享资源限制、存储卷和安全设置,并最终放在同一个节点上。另一方面,它们的健康检查和资源处理语义略有不同。对于 Init 容器没有就绪检查,因为所有 Init 容器必须在 Pod 启动进程可以继续应用容器之前成功地完全终止。

Init 容器也会影响 Pod 资源需求的计算方式,用于调度、自动缩放和配额管理。考虑到 Pod 中所有容器的执行顺序(首先,Init 容器按顺序运行,然后所有应用容器并行运行),有效的 Pod 级请求和限制值成为以下两组中的最高值:

  • 最高的 Init 容器请求/限制值
  • 请求/限制的所有应用容器值的总和

这种行为的后果是,如果你有高资源需求的 Init 容器和低资源需求的应用容器,影响调度的 Pod 级请求和限制值将基于 Init 容器的高值。这种设置并不节约资源。即使 Init 容器运行时间很短,大部分时间节点上有可用容量,其他 Pod 也无法使用。

此外,Init 容器可以实现关注点的分离,并允许保持容器的单一用途。应用容器可以由应用工程师创建,只关注应用逻辑。部署工程师可以编写一个 Init 容器,只关注配置和初始化任务。我们在例 14-1 中演示了这一点,它有一个基于 HTTP 服务器的应用容器,该容器为文件服务。

该容器提供了一个通用的 HTTP 服务功能,并且不对不同用例的服务文件可能来自哪里做任何假设。在同一个 Pod 中,一个 Init 容器提供了 Git 客户端功能,它的唯一目的是克隆一个 Git 仓库。由于两个容器都是同一个 Pod 的一部分,它们可以访问同一个卷来共享数据。我们使用同样的机制将克隆的文件从 Init 容器共享到应用容器。

例 14-1 展示了一个将数据复制到空卷的 Init 容器。

  1. # 例 14-1 初始化容器示例
  2. ---
  3. apiVersion: v1
  4. kind: Pod
  5. metadata:
  6. name: www
  7. labels:
  8. app: www
  9. spec:
  10. initContainers:
  11. - name: download
  12. image: axeclbr/git
  13. # 将外部的 Git 仓库克隆到挂载的目录中
  14. command:
  15. - git
  16. - clone
  17. - https://github.com/mdn/beginner-html-site-scripted
  18. - /var/lib/data
  19. # Init 容器和应用程序容器使用的共享卷
  20. volumeMounts:
  21. - mountPath: /var/lib/data
  22. name: source
  23. containers:
  24. - name: run
  25. image: docker.io/centos/httpd
  26. ports:
  27. - containerPort: 80
  28. volumeMounts:
  29. - mountPath: /var/www/html
  30. name: source
  31. volumes:
  32. # 节点上用于共享数据的空目录
  33. - emptyDir: {}
  34. name: source

我们本可以通过使用 ConfigMap 或 PersistentVolumes 达到同样的效果,但想在这里演示一下 Init 容器是如何工作的。这个例子说明了 Init 容器与主容器共享一个卷的典型使用模式。

:::success 对于调试 Init 容器的结果,如果将应用程序容器的命令暂时替换为一个虚拟的睡眠命令,这样你就有时间检查情况。如果你的 Init 容器无法启动,而你的应用程序又因为配置缺失或损坏而无法启动,这个技巧就特别有用。在 Pod 声明中的以下命令给了你一个小时的时间来调试: command: ["/bin/sh", "-c", "sleep 3600"] 。 :::

接下来第 15 章介绍的使用 Sidecar 也可以达到类似的效果,即 HTTP 服务器容器和 Git 容器作为应用容器并排运行。但是采用 Sidecar 的方式,无法知道哪个容器会先运行,Sidecar 的目的是在容器运行时使用并肩连续使用(就像例 15-1 中,Git 同步器容器持续更新本地文件夹一样)。如果既需要保证初始化,又需要持续更新数据,我们也可以同时使用 Sidecar 和 Init 容器。

更多的初始化技巧

正如你所看到的,Init 容器是一个 Pod 级别的构造,在一个 Pod 被启动后被激活。其他一些用于初始化 Kubernetes 资源的相关技术与 Init 容器不同,为了完整起见,值得在此列出。

  • 准入控制器:这些是一组插件,它们在对象持久化之前拦截每一个向 Kubernetes API Server 发出的请求,并可以对其进行转换或验证。有许多控制器用于应用检查、执行限制和设置默认值,但它们都被编译到 kube-apiserver 二进制中,并在 API Server 启动时由集群管理员配置。这种插件系统并不是很灵活,这也是 Kubernetes 中加入接纳 Webhook 的原因。
  • 准入 Webhook:这些组件是外部准入控制器,它们对任何匹配的请求执行 HTTP 回调。有两种类型的准入 Webhook:转换型(可以改变资源以执行自定义默认值)和验证型(可以拒绝资源以执行自定义准入策略)。这种外部控制器的概念允许在 Kubernetes 之外开发准入 Webhook,并在运行时进行配置。
  • Pod Presets:PodPreset 由另一个准入控制器评估,它有助于在创建时将匹配 PodPreset 中指定的字段注入 Pod。这些字段可以包括卷、卷挂载或环境变量。因此,PodPreset 在创建时使用标签选择器将额外的运行时要求注入到 Pod 中,以指定给定 PodPreset 适用的 Pod PodPreset 允许 Pod 模板作者自动添加多个 Pod 所需的重复信息。

:::info 有许多技术用于初始化 Kubernetes 资源。然而,这些技术不同于准入 Webhook,因为它们在创建时验证和突变资源。例如,你可以使用这些技术,将一个 Init 容器注入任何还没有的 Pod 中。相比之下,本章讨论的 Init 容器模式是在 Pod 的启动过程中激活并执行其职责的东西。最后,最显著的区别是,Init 容器是针对部署在 Kubernetes 上的开发者的,而这里描述的技术则帮助管理员控制和管理容器初始化过程。 :::

一些讨论

那么为什么要把 Pod 中的容器分成两组呢?如果需要的话,为什么不直接使用 Pod 中的应用容器进行初始化呢?答案是这两组容器有不同的生命周期、目的,甚至在某些情况下还有作者。

:::tips 让 Init 容器先于应用容器运行,更重要的是,让 Init 容器分阶段运行,只有当当前 Init 容器成功完成时,才会有进展,这意味着你可以在初始化的每一步都确定前一步已经成功完成,你可以进入下一阶段。而应用容器则是并行运行的,并没有提供和 Init 容器一样的保障。有了这种区别,我们可以创建专注于单一初始化或专注于应用的任务的容器,并将它们组织在 Pod 中。 :::

参考资料