一文搞懂 4 种常用的 Kubernetes 容器

Posted by Mike on 2021-07-30

截止目前 Kubernetes 1.18,Kubernetes 已经支持标准容器,Sidecar 容器,Init 容器,Ephemeral 容器 4 种类型的 Containers。本文我们详细介绍一下这 4 种容器的特性以及使用场景。

标准容器和 Sidecar 容器

在 Kubernetes 1.18 之前,这两种容器从 Kubernetes 管理的角度来看,并没有什么区别。只不过人为从功能上做了区分。

使用 Sidecar 容器(模块化)具有的优点

  • 加速应用程序开发,因为容器可以在团队甚至更大的社区之间重复使用
  • 整理专家知识,因为每个人都在一个容器化的实现上进行协作,该实现反映了最佳实践,而不是无数种功能大致相同的自家生产的不同容器
  • 启用敏捷团队,因为容器边界是自然边界,是团队职责的契约
  • 提供关注点分离,并专注于特定功能,以减少意大利面条的依赖性和不可测的组件

对于 Sidecar 容器一般来说主要体现在以下 4 种角色:

  • 代理

例如现在 Istio 中 的 Envoy。

通过这种 Sidercar 模式,代理可以拦截进出主容器的流量从而 Istio 可以提取有关流量行为的大量信号作为属性。 Istio 可以使用这些属性来执行策略决策,并将其发送到监视系统以提供有关整个网格行为的信息。

Sidecar 代理模型还允许您将 Istio 功能添加到现有部署中,而无需重新构造或重写代码。

  • 适配器

适配器容器对输出进行标准化。考虑监视 N 个不同应用程序的任务。可以使用不同的导出监视数据的方式来构建每个应用程序。(例如 JMX,StatsD,特定于应用程序的统计信息),但每个监控系统都希望其收集的监控数据具有一致且统一的数据模型。

通过使用复合容器的适配器模式,您可以通过创建 Pod 来将来自不同系统的异构监视数据转换为一个统一的表示形式,该 Pod 将应用程序容器与知道如何进行转换的适配器分组在一起。同样,由于这些 Pod 共享名称空间和文件系统,因此这两个容器的协调非常简单明了。

  • 增强主容器功能

Sidecar 容器扩展并增强了 “主” 容器,它们可以使用现有的容器并使它们变得更好。

例如,考虑一个运行 Nginx Web 服务器的容器。添加另一个将文件系统与 Git 存储库同步的容器,在这些容器之间共享文件系统,并且您已经构建了 Git Push-to-deploy。但是您已经以模块化的方式完成了此工作,其中 Git 同步器可以由不同的团队构建,并且可以在许多不同的Web服务器(Apache,Python,Tomcat等)上重复使用。

由于这种模块化,您只需编写和测试 Git 同步器一次,即可在众多应用程序中重复使用它。而且,如果有人编写它,您甚至不需要这样做。

  • 实现辅助功能

这种场景一般出现在 DevOps 中。比如将收集日志的组件以 Sidecar 的方式部署,实现收集日志的用途,或是部署一个 Sidecar 组件从配置中心监听配置变化,实时更新本地配置。

生命周期

Sidecar 容器的所有问题都与容器生命周期相关性有关。由于和 Pod 中的常规容器之间没有区别,因此无法控制哪个容器首先启动或最后终止,但是先正确运行 Sidecar 容器通常是应用程序容器正确运行的要求。

从 1.18 版本开始,K8S 内置的 Sidecar 功能将确保 Sidecar 容器在正常业务流程开始之前就启动并运行,即通过更改 Pod 的启动生命周期,在 Init 容器完成后启动 Sidecar 容器,在 Sidecar 容器就绪后启动业务容器,从启动流程上保证顺序性。

通过更改 Pod 规范中的 container.lifecycle.type 将容器标记为 Sidecar 类型:Sidecar,默认为 Standard,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: v1
kind: Pod
metadata:
name: bookings-v1-b54bc7c9c-v42f6
labels:
app: demoapp
spec:
containers:
- name: bookings
image: banzaicloud/allspark:0.1.1
...
- name: istio-proxy
image: docker.io/istio/proxyv2:1.4.3
lifecycle:
type: Sidecar
...

Init 容器

在 Kubernetes 中,Init 容器是在同一 Pod 中的其他容器之前开始并执行的容器。它旨在为 Pod 上托管的主应用程序执行初始化逻辑。例如,创建必要的用户帐户,执行数据库迁移,创建数据库结构等。

Init 容器与普通的容器非常像,除了如下两点:

  • 它们总是运行到完成。
  • 每个都必须在下一个启动之前成功完成。

与普通容器的不同之处

  • Init 容器支持应用容器的全部字段和特性,包括资源限制、数据卷和安全设置。 然而,Init 容器对资源请求和限制的处理稍有不同。
  • 同时 Init 容器不支持 Readiness Probe,因为它们必须在 Pod 就绪之前运行完成。
  • 如果为一个 Pod 指定了多个 Init 容器,这些容器会按顺序逐个运行。每个 Init 容器必须运行成功,下一个才能够运行。当所有的 Init 容器运行完成时,Kubernetes 才会为 Pod 初始化应用容器并像平常一样运行。

Init 容器作用

因为 Init 容器具有与应用容器分离的单独镜像,其启动相关代码具有如下优势:

  • Init 容器可以包含一些安装过程中应用容器中不存在的实用工具或个性化代码。例如,没有必要仅为了在安装过程中使用类似 sed、 awk、 python 或 dig 这样的工具而去FROM 一个镜像来生成一个新的镜像。
  • Init 容器可以安全地运行这些工具,避免这些工具导致应用镜像的安全性降低。
  • 应用镜像的创建者和部署者可以各自独立工作,而没有必要联合构建一个单独的应用镜像。
  • Init 容器能以不同于Pod内应用容器的文件系统视图运行。因此,Init容器可具有访问 Secrets 的权限,而应用容器不能够访问。
  • 由于 Init 容器必须在应用容器启动之前运行完成,因此 Init 容器提供了一种机制来阻塞或延迟应用容器的启动,直到满足了一组先决条件。一旦前置条件满足,Pod内的所有的应用容器会并行启动。

创建 InitContainer 时应考虑一些注意事项:

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

正如我们刚刚讨论的那样,Init 容器总是比同一个 Pod 上的其他应用程序容器先启动。结果,调度程序对 Init 容器的资源和限制赋予了更高的优先级。必须仔细考虑这种行为,因为这可能会导致不良后果。例如,如果您有一个初始化容器和一个应用程序容器,并且将初始化容器的资源和限制设置为高于应用程序容器的资源和限制,那么只有在有一个可用节点满足初始化的情况下,才调度整个 Pod 容器要求。换句话说,即使有一个未使用的节点可以在其中运行应用程序容器,但如果初始化容器具有该节点可以处理的更高资源先决条件,则 Pod 也不会部署到该节点。因此,在定义初始化容器的请求和限制时,您应尽可能严格。最佳做法是,除非绝对必要,否则请勿将这些参数设置为高于应用程序容器的值。

使用 Init 容器

下面的例子定义了一个具有 2 个 Init 容器的简单 Pod。 第一个等待 myservice 启动,第二个等待 mydb 启动。 一旦这两个 Init容器 都启动完成,Pod 将启动 spec 区域中的应用容器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: v1
kind: Pod
metadata:
name: myapp-pod
labels:
app: myapp
spec:
containers:
- name: myapp-container
image: busybox:1.28
command: ['sh', '-c', 'echo The app is running! && sleep 3600']
initContainers:
- name: init-myservice
image: busybox:1.28
command: ['sh', '-c', "until nslookup myservice.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for myservice; sleep 2; done"]
- name: init-mydb
image: busybox:1.28
command: ['sh', '-c', "until nslookup mydb.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for mydb; sleep 2; done"]

这是 Kubernetes 1.6 版本的新语法,尽管老的 annotation 语法仍然可以使用。我们已经把 Init 容器的声明移到 spec 中:

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

1.5 版本的语法在 1.6 版本仍然可以使用,但是我们推荐使用 1.6 版本的新语法。 在 Kubernetes 1.6 版本中,Init 容器在 API 中新建了一个字段。 虽然期望使用 beta 版本的 annotation,但在未来发行版将会被废弃掉。

在所有的 Init 容器没有成功之前,Pod 将不会变成 Ready 状态。 Init 容器的端口将不会在 Service 中进行聚集。 正在初始化中的 Pod 处于 Pending 状态,但应该会将条件 Initializing 设置为 true。

如果 Pod 重启,所有 Init 容器必须重新执行。

对 Init 容器 spec 的修改,被限制在容器 image 字段中。 更改 Init 容器的 image 字段,等价于重启该 Pod。

Ephemeral 容器

临时容器与其他容器的不同之处在于,它们缺少对资源或执行的保证,并且永远不会自动重启,因此不适用于构建应用程序。临时容器使用与常规容器相同的 ContainerSpec 段进行描述,但许多字段是不相容且不允许的。

  • 临时容器没有端口配置,因此像 portslivenessProbereadinessProbe 这样的字段是不允许的。
  • Pod 资源分配是不可变的,因此 resources 配置是不允许的。
  • 有关允许字段的完整列表,请参见临时容器参考文档

临时容器是使用 API 中的一种特殊的 ephemeralcontainers 处理器进行创建的,而不是直接添加到 pod.spec 段,因此无法使用 kubectl edit 来添加一个临时容器。

与常规容器一样,将临时容器添加到 Pod 后,将不能更改或删除临时容器。

为什么我们需要 Ephemeral 容器?

我们知道容器的优点是它们通过使用不变方法提供所有必需的依赖项来运行隔离的进程。通过仅将所需的依赖项添加到镜像中,容器可以降低攻击面并提供更快的启动和部署。使用 “distroless” 方法构建容器镜像(基于 Scratch ),通过仅包含已编译的应用程序二进制文件,将容器镜像提升到了一个新的水平。与普通的容器镜像不同,它们不基于任何种类的 Linux 发行版,因此不包含任何其他可通过 kubectl exec 执行以进行故障排除的二进制文件和工具。这就决定了该容器有助于提供安全可靠的运行时环境,但也很难在问题发生时进行调试。

在这种情况下,临时容器发挥作用。它们实现了调试容器附加到主进程的功能,然后你可以用于调试任何类型的问题。调试容器可以基于任何镜像,因此可以根据您的需求进行定制。您可以构建自己的调试镜像,其中包含特殊的调试二进制文件或仅包含 curl,OpenSSL 和 MongoDB客户端之类的工具。但是,您也可以选择Linux发行版(如Ubuntu)或仅运行Busybox镜像,这两个镜像都已经包含了许多有用的工具。

如何使用临时容器?

临时容器是alpha功能,因此默认情况下处于禁用状态。您将需要激活以下功能门才能使用它们:

  • 临时容器
  • PodShareProcessNamespace(v1.16中的beta版,因此默认情况下已启用)

本节中的示例演示了临时容器如何出现在 API 中。 通常,您可以使用 kubectl 插件进行故障排查,从而自动化执行这些步骤。

临时容器是使用 Pod 的 ephemeralcontainers 子资源创建的,可以使用 kubectl --raw 命令进行显示。首先描述临时容器被添加为一个 EphemeralContainers 列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"apiVersion": "v1",
"kind": "EphemeralContainers",
"metadata": {
"name": "example-pod"
},
"ephemeralContainers": [{
"command": [
"sh"
],
"image": "busybox",
"imagePullPolicy": "IfNotPresent",
"name": "debugger",
"stdin": true,
"tty": true,
"terminationMessagePolicy": "File"
}]
}

使用如下命令更新已运行的临时容器 example-pod

1
$ kubectl replace --raw /api/v1/namespaces/default/pods/example-pod/ephemeralcontainers  -f ec.json

这将返回临时容器的新列表:

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
27
28
{
"kind":"EphemeralContainers",
"apiVersion":"v1",
"metadata":{
"name":"example-pod",
"namespace":"default",
"selfLink":"/api/v1/namespaces/default/pods/example-pod/ephemeralcontainers",
"uid":"a14a6d9b-62f2-4119-9d8e-e2ed6bc3a47c",
"resourceVersion":"15886",
"creationTimestamp":"2019-08-29T06:41:42Z"
},
"ephemeralContainers":[
{
"name":"debugger",
"image":"busybox",
"command":[
"sh"
],
"resources":{

},
"terminationMessagePolicy":"File",
"imagePullPolicy":"IfNotPresent",
"stdin":true,
"tty":true
}
]
}

可以使用以下命令查看新创建的临时容器的状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ kubectl describe pod example-pod
...
Ephemeral Containers:
debugger:
Container ID: docker://cf81908f149e7e9213d3c3644eda55c72efaff67652a2685c1146f0ce151e80f
Image: busybox
Image ID: docker-pullable://busybox@sha256:9f1003c480699be56815db0f8146ad2e22efea85129b5b5983d0e0fb52d9ab70
Port: <none>
Host Port: <none>
Command:
sh
State: Running
Started: Thu, 29 Aug 2019 06:42:21 +0000
Ready: False
Restart Count: 0
Environment: <none>
Mounts: <none>
...

可以使用以下命令连接到新的临时容器:

1
$ kubectl attach -it example-pod -c debugger

如果启用了进程命名空间共享,则可以查看该 Pod 所有容器中的进程。 例如,运行上述 attach 操作后,在调试器容器中运行 ps 操作:

1
2
# 在 "debugger" 临时容器内中运行此 shell 命令
$ ps auxww

运行命令后,输出类似于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
PID   USER     TIME  COMMAND
1 root 0:00 /pause
6 root 0:00 nginx: master process nginx -g daemon off;
11 101 0:00 nginx: worker process
12 101 0:00 nginx: worker process
13 101 0:00 nginx: worker process
14 101 0:00 nginx: worker process
15 101 0:00 nginx: worker process
16 101 0:00 nginx: worker process
17 101 0:00 nginx: worker process
18 101 0:00 nginx: worker process
19 root 0:00 /pause
24 root 0:00 sh
29 root 0:00 ps auxww

总结

本文简单介绍了标准容器,Sidecar 容器,Init 容器,Ephemeral 容器 4 种类型的 Containers。随着 Kubernetes 日益普及,我们需要充分掌握这几种类型容器原理和使用方法,才能更好地服务业务。

此外 Sidecar 容器将会成为未来软件交付的一种新的方式,参照 Dapr 等,不同的团队提供自己的功能容器,然后选择性注入 Sidecar 到主业务容器,实现解耦。

参考文档

  1. https://www.google.com
  2. https://zhuanlan.zhihu.com/p/145233597
  3. https://cloud.tencent.com/developer/article/1645954