0%

Kubernetes模式:initContainer使用

Init模式

初始化逻辑通常在编程语言中很常见。在面向对象编程语言中,我们有构造函数的概念。构造函数是一个函数(或方法),每当对象被实例化时都会被调用。构造器的目的是“准备”对象以完成它应该做的工作。例如,它设置变量的默认值,创建数据库连接对象,确保对象正确运行所需的先决条件的存在。例如,如果创建了一个user对象,那么它至少需要用户的用户名、名和姓,这样它才能正常工作。不同语言之间的构造函数实现是不同的。但是,所有这些都只被调用一次,并且只在对象实例化时调用。

初始化模式的目的是将对象与其初始化逻辑解耦。因此,如果一个对象需要一些种子数据输入到数据库中,这就属于构造函数逻辑而不是应用程序逻辑。这允许我们更改对象的“启动”方式,而不影响其“工作”方式。

Kubernetes使用相同的模式。虽然对象是面向对象语言的原子单元,但是Kubernetes有Pods。因此,如果我们有一个应用程序在需要一些初始化逻辑的容器上运行,那么将此工作交给另一个容器是一个很好的做法。Kubernetes有一种用于特定作业的容器类型:init containers。

Init Containers

在Kubernetes中,init容器是在同一个Pod中的其他容器之前启动和执行的容器。它的目的是为Pod上托管的主应用程序执行初始化逻辑。例如,创建必要的用户帐户、执行数据库迁移、创建数据库模式等等。

Init Containers设计注意事项

在创建init容器时,我们应该考虑一些注意事项:

  • 它们总是比Pod里的其他容器先执行。因此,它们不应该包含需要很长时间才能完成的复杂逻辑。启动脚本通常很小而且简洁。如果我们发现在init容器中添加了太多的逻辑,那就应该考虑将它的一部分移到应用程序容器本身。
  • Init容器按顺序启动和执行。除非成功完成其前置容器,否则不会调用init容器。因此,如果启动任务很长,可以考虑将其分成若干步骤,每个步骤都由init容器处理,以便知道哪些步骤失败。
  • 如果任何init容器失败,整个Pod将重新启动(除非将restartPolicy设置为Never)。重新启动Pod意味着重新执行所有容器,包括任何init容器。因此,我们可能需要确保启动逻辑能够容忍多次执行而不会导致重复。例如,如果数据库迁移已经完成,那么应该忽略再次执行迁移命令。
  • 在一个或多个依赖项可用之前,init容器是延迟应用程序初始化的一个很好的候选者。例如,如果我们的应用程序依赖于一个施加了API请求速率限制的API,可能需要等待一段时间才能从该API接收响应。在应用程序容器中实现此逻辑可能很复杂;因为它需要与运行状况和准备状态探测相结合。一种更简单的方法是创建一个init容器,该容器等待API准备好后才能成功退出。只有在init容器成功完成其工作之后,应用程序容器才会启动。
  • Init容器不能像应用程序容器那样使用liveness和readiness探针。原因是它们注定要成功启动和退出,就像Jobs和CronJobs的行为一样。
  • 同一个Pod内的所有容器共享相同的卷和网络。我们可以使用此特性在应用程序及其init容器之间共享数据。

Init Containers的“请求”和“限制”行为

正如我们刚刚讨论的,init容器总是在同一个Pod上的其他应用程序容器之前启动。因此,调度程序为init容器的资源和限制提供了更高的优先级。这种行为必须被彻底考虑,因为它可能会导致不期望的结果。例如,如果我们有一个init容器和一个应用程序容器,并且将init容器的资源和限制设置为高于应用程序容器的资源和限制,那么只有在存在满足init容器要求的可用节点时,才会调度整个Pod。换句话说,即使有一个未使用的节点可以运行应用程序容器,如果init容器具有该节点可以处理的更高的资源先决条件,那么Pod也不会部署到该节点。因此,在定义init容器的请求和限制时,应该尽可能严格。作为最佳实践,除非绝对需要,否则不要将这些参数设置为高于应用程序容器的值。

场景01:初始化数据库

在这个场景中,我们为MySQL数据库提供服务。此数据库用于测试应用程序。它不一定要包含真实的数据,但是它必须有足够的数据种子,这样我们就可以测试应用程序的查询速度。我们使用init容器来处理下载SQL转储文件并将其还原到数据库中,该数据库托管在另一个容器中。这种情况可以说明如下:

init

yaml定义文件可能如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
apiVersion: v1
kind: Pod
metadata:
name: mydb
labels:
app: db
spec:
initContainers:
- name: fetch
image: mwendler/wget
command: ["wget","--no-check-certificate","https://sample-videos.com/sql/Sample-SQL-File-1000rows.sql","-O","/docker-entrypoint-initdb.d/dump.sql"]
volumeMounts:
- mountPath: /docker-entrypoint-initdb.d
name: dump
containers:
- name: mysql
image: mysql
env:
- name: MYSQL_ROOT_PASSWORD
value: "example"
volumeMounts:
- mountPath: /docker-entrypoint-initdb.d
name: dump
volumes:
- emptyDir: {}
name: dump

上面的定义创建了一个Pod,它承载两个容器:init容器和application容器。让我们看看这个定义有趣的方面:

  • init容器负责下载包含数据库转储的SQL文件。我们使用mwendler/wget映像,因为我们只需要wget命令。

  • 下载的SQL的目标目录是MySQL镜像用来执行SQL文件的目录(/docker-entrypoint-initdb.d)。此行为内置到我们在应用程序容器中使用的MySQL镜像中。

  • init容器将/docker-entrypoint-initdb.d挂载到一个emptyDir卷。因为两个容器托管在同一个Pod上,所以它们共享相同的卷。因此,数据库容器可以访问emptyDir卷上的SQL文件。

如果没有Init Containers会发生什么

在这个例子中,我们使用初始化模式作为最佳实践。如果我们在不使用init模式的情况下实现相同的逻辑,那么我们必须基于mysql基本镜像创建一个新映像,安装wget,然后使用它下载SQL文件。这种方法的缺点是:

  • 如果需要对下载逻辑进行任何更改,则需要创建一个新镜像,将其推送到定义文件中并更改其引用。这增加了维护自定义镜像的负担。

  • 它在DB容器及其启动逻辑之间创建了一个紧密耦合的关系,这使得应用程序更难管理,并且增加了引入错误和bug的可能性。

场景02:延迟应用程序启动

init容器的另一个常见用例是当我们需要应用程序等待另一个服务完全运行(响应请求)时。以下定义演示了这种情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: v1
kind: Pod
metadata:
name: myapp-pod
labels:
app: myapp
spec:
initContainers:
- name: init-myservice
image: busybox:1.28
command: ['sh', '-c', 'until nslookup myservice; do echo waiting for myservice; sleep 2; done;']
containers:
- name: myapp-container
image: busybox:1.28
command: ['sh', '-c', 'echo The app is running! && sleep 3600']

所以,假设在myapp容器上运行的应用程序必须依赖myservice正常后才能正常工作。我们需要延迟myapp直到myservice准备好。我们通过使用一个简单的nslookup命令(第11行)来实现这一点,该命令不断检查“myservice”的成功名称解析。如果nslookup能够解析“myservice”,则服务将启动。使用一个成功的退出代码,init容器终止,让位于应用程序容器开始。否则,容器将在重试之前休眠两秒钟,从而延迟应用程序容器的启动。

为了完整起见,这是myservice的定义文件:

1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: Service
metadata:
name: myservice
spec:
ports:
- protocol: TCP
port: 80
targetPort: 9376

写在最后

  • Init模式是设计需要启动逻辑的应用程序时必须遵循的重要实践。
  • Kubernetes提供init容器作为将应用程序逻辑与其启动过程分离的一种方法。
  • 将应用程序初始化逻辑放在init容器中有许多优点:
    • 我们将实施关注点分离原则。应用程序可以有自己的工程师团队,而其初始化逻辑由另一个团队编写。
    • 在授权和访问控制方面,拥有一个独立的团队来处理应用程序的初始化步骤,可以给公司带来更大的灵活性。例如,如果启动应用程序需要使用需要安全许可的资源(例如,修改防火墙规则),则可以由具有适当凭据的人员来完成。应用程序团队不参与操作。
    • 如果涉及太多的初始化步骤,可以将它们分解为多个init容器,然后依次执行。如果一个步骤失败,init容将报告一个错误,这将使我们更好地了解逻辑的哪一部分不成功。
  • 在使用init容器时,应该考虑以下几点:
    • 初始化容器在失败时重新启动。因此,它们的代码必须是幂等的。
    • Init容器的请求和限制会先被调度程序用于调度判断。错误的值可能会对调度器决定将整个Pod(包括应用程序容器)放置在哪里产生负面影响。