Kubernetes in Action 笔记 —— 深入了解 Pod 概念

下图展示了 Kubernetes 如何通过 Deployment、Pod、Service 三种对象部署一个最小化应用。
Three basic objects comprising a deployed application

其中的 Pod 对象是 Kubernetes 中最重要的一个核心概念,它代表着一个处于运行状态的应用实例。

理解 Pods

Pod 可以包含一个或多个有协作关系的容器,是 Kubernetes 中最基本的构造单位。

  • 容器只能借助 Pod 进行部署,无法独立运行
  • 一个 Pod 可以包含多个容器,但只包含一个容器的 Pod 也是很常见的
  • Pod 中的多个容器只允许在同一个工作节点上运行,即单个 Pod 不能跨节点部署

Pods

Pod 机制的优势

为什么要在一个 Pod 中运行多个容器,而不是在一个容器中运行多个进程。

一个容器不应该包含多个进程
容器就像是一台隔离的虚拟机,因此是可以同时运行多个进程的。但这样会使得容器难于管理。
容器被设计成只会运行一个进程(子进程不计算在内),大多数容器管理工具也都是基于这个原则去设计的。
比如容器中运行的进程有时候会向标准输出打印其日志信息,Kubernetes 中查看日志的命令就只会显示从这个输出中捕获到的内容。如果容器中同时运行多个进程,都向外输出日志信息,Kubernetes 捕获到的日志内容就会变得错综复杂。

另一个原因在于,容器运行时只会在容器的根进程挂掉时重启该容器。
为了充分利用容器运行时提供的特性,应该在每个容器中只运行一个进程。

Pod 如何组合多个容器
不应该在同一个容器中运行多个进程。与此同时,将分散在多个容器中相互关联的进程结合成一个单位,统一进行管理也是很有必要的。
因此引入了 Pod 机制。

Pod 可以同时运行多个关系紧密的进程(容器),给它们(几乎)相同的运行环境,使得它们就像是运行在同一个容器中那样。
这些进程是相互独立、隔离的,但也会共享某些资源。因而既可以使用容器提供的各种隔离特性,又能够促使多个进程相互协作。
Pod share network interface

如上图所示,Pod 中的多个容器共享同一个 Network 命名空间,因而共享该命名空间的网络接口、IP 地址、端口空间等。这些容器能够使用 loopback 设备进行通信,但不能绑定同一个网络端口。

同一个 Pod 中的多个容器还会共享相同的 UTS 命名空间,因而能看到同一个主机名;共享相同的 IPC 命名空间,因而不同容器中的多个进程之间可以通过 IPC 进行通信;还可以配置成共享同一个 PID 命名空间,使得这些容器中的进程使用同一个进程树。
与前面的内容相反,每个 Pod 总是有自己的 Mount 命名空间,这意味着每个 pod 都有自己的文件系统。当然,假如 Pod 中有两个容器需要共享部分文件系统,也可以向该 Pod 添加 Volume,作为共享存储挂载到每一个容器上。

单容器 or 多容器

可以将 Pod 看作一台独立的计算机。不同于虚拟机通常需要同时运行多个应用,每个 Pod 中一般只运行一个应用。 Pod 几乎没有额外的资源开销,因而在同一个 Pod 中运行多个应用并不是必须的。

假如一个系统的前端和后端运行在同一个 Pod 中,而单个 Pod 不能跨节点存在(即单个 Pod 只能部署在某一个节点上)。如果你有一个双节点的集群且只创建了一个 Pod,则该 Pod 只会运行在其中一个节点上,造成 CPU、内存、存储和带宽等资源的闲置。
将前后端分别部署到不同的 Pod 中则能够提高硬件的利用率。

另一个不将多个应用部署到同一个 Pod 中的原因与横向扩展有关。Pod 不仅仅是部署的基本单位,也是扩展的基本单位
当通过修改 Deployment 对象扩展应用时,Kubernetes 并不会复制 Pod 中的容器,而是直接创建新的 Pod 实例(应用副本)。
前端和后端组件通常有着不同的扩展需求,基本上都会独立地进行扩展。而同一个 Pod 中不同组件的扩展是同步的。如果一个容器中的某个组件相对于其他组件需要独立地进行扩展,该应用就必须部署在另一个 Pod 中。
Splitting application

将多个容器放置在同一个 Pod 中的唯一场景,就是某个应用包含一个基础进程以及一个或者多个对基础进程有补充作用的附加进程。运行附加进程称为 sidecar container

比如某个 Pod 包含一个运行 Node.js 应用的容器,而该 Node.js 应用只支持 HTTP 协议。为了令其支持 HTTPS,可以对 JavaScript 代码做一些小的改动。其实也可以在不改动应用代码的情况下完成此需求。
只需要向 Pod 中再添加一个反向代理容器,将 HTTPS 流量转发成 HTTP 传给 Node.js 容器。
A sidecar container that converts HTTPS traffic to HTTP

另一个例子如下图所示,基础容器运行一个 Web 服务,其资源文件存放在挂载的 Volume 上。Pod 中的另一个容器则挂载了同一个 Volume,作为 Agent 定期从外部拉取新的资源文件,存储在 Volume 供 Web 服务使用。
A sidecar container that delivers content to the web server container via a volume

从 YAML 文件创建 Pod

可以使用 kubectl create 命令创建 Pod 对象(Kubernetes in Action 笔记 —— 部署第一个应用),但更常见的方式是创建一个 JSON 或 YAML 格式的清单文件,描述整个应用的架构,再将其发送给 Kubernetes API 来生成 Pod 等对象。

比如下面的 kubia.yml 文件:

1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: Pod
metadata:
name: kubia
spec:
containers:
- name: kubia
images: luksa/kubia:1.0
ports:
- containerPort: 8080

创建 Pod 对象
可以使用 kubectl apply -f xxx.yml 命令将清单文件发送给 API 并应用到 Kubernetes 集群:

1
2
$ kubectl apply -f kubia.yml
pod/kubia created

kubectl apply 命令不仅仅用于创建对象,同时也可以对现有的对象进行修改。
比如 Pod 对象创建之后需要对其做一些改动,可以直接编辑 yml 文件,再运行一遍 kubectl apply 命令即可。但有些描述 Pod 的字段是不可变的,因而更新操作有可能会失败,此时就可以先删除后再重建。

检查新创建的 Pod
使用 kubectl get pod 命令获取某个 Pod 的汇总信息:

1
2
3
$ kubectl get pod kubia
NAME READY STATUS RESTARTS AGE
kubia 1/1 Running 0 6m37s

想获取更详细一点的信息可以加上 -o wide 选项:

1
2
3
$ kubectl get pod kubia -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
kubia 1/1 Running 0 8m3s 172.17.0.3 minikube <none> <none>

或者使用 kubectl describe pod 命令:

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
$ kubectl describe pod kubia
Name: kubia
Namespace: default
Priority: 0
Node: minikube/192.168.49.2
Start Time: Fri, 24 Dec 2021 15:08:57 +0800
Labels: <none>
Annotations: <none>
Status: Running
IP: 172.17.0.3
IPs:
IP: 172.17.0.3
Containers:
kubia:
Container ID: docker://aa8778766a69122de8c8b401922d2318b15b8aaf220945d747e494de2fda9199
Image: luksa/kubia:1.0
Image ID: docker-pullable://luksa/kubia@sha256:a961dc8f377916936fa963508726d77cf77dcead5c97de7e5361f0875ba3bef7
Port: 8080/TCP
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 9m28s default-scheduler Successfully assigned default/kubia to minikube
Normal Pulled 9m27s kubelet Container image "luksa/kubia:1.0" already present on machine
Normal Created 9m27s kubelet Created container kubia
Normal Started 9m27s kubelet Started container kubia

与 Pod 进行交互

向 Pod 中的应用发起请求

可以使用 kubectl expose 命令创建一个 Service 对象,给 Pod 分配一个负载均衡器,从而可以从外部访问 Pod 中运行的应用。但是对于开发、测试和调试等目的,有可能需要直接与 Pod 中的应用进行交互。
每个 Pod 都会自动绑定一个 IP 地址,从而可以被集群中的其他 Pod 访问,但该 IP 地址是仅限于集群内部的。

获取 Pod 的 IP 地址

1
2
3
$ kubectl get pod kubia -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
kubia 1/1 Running 0 58m 172.17.0.3 minikube <none> <none>

从工作节点连接 Pod
Kubernetes 的网络模型支持从任意节点直接访问集群中所有节点上的任何 Pod。因而可以先登录某一个工作节点,再从该节点访问集群中的 Pod。
对于 Minikube 而言,可以使用 minikube ssh 命令登录工作节点。

1
2
$ minikube ssh
docker@minikube:~$

登录成功后,即可使用内部 IP 地址访问 kubia Pod:

1
2
docker@minikube:~$ curl 172.17.0.3:8080
Hey there, this is kubia. Your IP is ::ffff:172.17.0.1.

One-off 客户端 Pod

第二种测试应用连通性的方法是创建一个临时的 Pod 并运行 curl 命令。

1
2
3
$ kubectl run --image=curlimages/curl -it --restart=Never --rm client-pod curl 172.17.0.2:8080
Hey there, this is kubia. Your IP is ::ffff:172.17.0.4.
pod "client-pod" deleted

上面的命令会从 curlimages/curl 镜像创建一个容器,执行 curl 172.17.0.2:8080 命令。
其中 -it 选项会将当前的终端与容器的标准输入输出绑定,--restart=Never 选项确保 curl 命令执行完后容器直接停掉,--rm 选项负责在最后删除 Pod。

这种方式在测试 Pod 与 Pod 之间连通性的时候非常有用。

通过 Kubectl 端口转发访问 Pod
在开发过程中,最简单的访问容器中应用的方法是使用 kubectl port-forward 命令。该命令可以通过绑定到本地机器端口上的代理来访问特定的 Pod。
此方式甚至不需要知道 Pod 的 IP 地址,只需要指定 Pod 的名字和端口号即可:

1
2
3
$ kubectl port-forward kubia 8080
Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080

上述命令会在本地机器的 8080 端口开启一个代理,指向 kubia 容器的 8080 端口。
此时打开另一个命令行窗口,运行 curl localhost:8080 命令即等同于访问 kubia 容器的 8080 端口:

1
2
$ curl localhost:8080
Hey there, this is kubia. Your IP is ::ffff:127.0.0.1.

kubectl port-forward 的底层机制

查看应用日志

不同于将日志写入到文件中,容器化应用通常会将日志输出到标准输出(stdout)和标准错误输出(stderr)。这使得容器运行时能够拦截应用的日志输出,将其转存在固定的位置(通常是 /var/log/containers),而不需要知道容器中日志文件的保存位置。
当使用 Kubernetes 运行应用时,可以登录到工作节点通过 docker logs 命令查看应用的日志,但更简单的方式是直接在本地使用 kubectl logs 命令。

获取某个 Pod 的日志

1
2
3
4
5
6
7
8
9
$ kubectl logs kubia
Kubia server starting...
Local hostname is kubia
Listening on port 8080
Received request for / from ::ffff:172.17.0.1
Received request for / from ::ffff:172.17.0.1
Received request for / from ::ffff:172.17.0.4
Received request for / from ::ffff:172.17.0.4
Received request for / from ::ffff:127.0.0.1

加上 -f (--follow) 选项可以实时显示日志输出。

在日志输出中显示时间戳

1
2
3
4
5
6
7
8
9
$ kubectl logs kubia --timestamps=true
2021-12-27T13:40:21.606420000Z Kubia server starting...
2021-12-27T13:40:21.606992200Z Local hostname is kubia
2021-12-27T13:40:21.607003000Z Listening on port 8080
2021-12-27T13:43:11.228907300Z Received request for / from ::ffff:172.17.0.1
2021-12-27T13:47:45.648686300Z Received request for / from ::ffff:172.17.0.1
2021-12-27T13:59:16.819436200Z Received request for / from ::ffff:172.17.0.4
2021-12-27T14:01:19.101277000Z Received request for / from ::ffff:172.17.0.4
2021-12-27T14:34:40.145715500Z Received request for / from ::ffff:127.0.0.1

此外还可以根据时间筛选日志输出,比如只显示最近一段时间内的日志。
kubectl logs kubia --since=2h 显示最近 2 小时内输出的日志内容。

也可以通过 --since-time 选项筛选特定时间后输出的日志:
kubectl logs kubia –-since-time=2020-02-01T09:50:00Z

或者直接使用 --tail=n 选项显示最近的 n 条日志。

PS
Kubernetes 会为每一个容器都保留一个独立的日志文件。它们通常保存在容器运行节点的 /var/log/containers 路径下,容器重启后其日志会写入到一个新的文件中。

如果某些容器应用将其日志写入到文件中而不是 stdout,理想情况下,应该配置一个中心化的日志系统定期收集这些日志。
如果只是想简单地手动访问容器中的日志文件,可以参考后面的内容。

容器的文件传输

有些时候需要向运行的容器中添加文件,或者从容器中获取文件。虽然修改容器中的文件的场景并不多见(尤其在生产环境中),但在开发过程中还是有一定用处的。

Kubernetes 提供了 cp 命令,能够从本地磁盘复制文件或目录到任意 Pod,或者相反方向。
kubectl cp kubia:/etc/hosts /tmp/kubia-hosts 将 kubia 容器中的 /etc/hosts 文件复制到本地 /tmp 路径下。

kubectl cp /path/to/local/file kubia:path/in/container 将本地文件复制到 kubia 容器。

在运行的容器中执行命令

运行单个命令
可以使用 kubernetes exec 命令运行容器文件系统中的某个可执行文件。
用户远程执行命令,无需登录到对应的工作节点上。

1
2
3
4
$ kubectl exec kubia -- ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.2 564436 30216 ? Ssl 13:40 0:00 node app.js
root 13 5.0 0.0 36632 2660 ? Rs 15:54 0:00 ps aux

前面的章节中曾经通过一个 one-off 客户端容器运行 curl 命令,向应用发送请求。实际上也可以直接在应用容器中运行 curl 命令:

1
2
$ kubectl exec kubia -- curl -s localhost:8080
Hey there, this is kubia. Your IP is ::ffff:127.0.0.1.

开启一个交互式 Shell
如果想要交互式地在容器中运行多条命令,可以开启一个 Shell:

1
2
$ kubectl exec -it kubia -- bash
root@kubia:/#

之后就可以像在本地运行 Linux 命令一样在容器中执行命令了。

需要注意的是,为了保证容器的镜像足够小同时提高安全性,生产环境中使用的容器通常只包含运行应用所需要的可执行文件。这会大大减少潜在的攻击目标,同时也意味着用户无法使用 Shell 或其他 Linux 工具。

在 Pod 中运行多个容器

前面的 kubia 容器只支持 HTTP 协议,可以为其添加 TLS 支持。其实有一种简单的不需要修改现有代码的方案,就是在现有的 Node.js 容器旁边添加一个提供反向代理功能的 sidecar 容器。

用 Envoy 代理扩展 kubia 应用
简单来说,Pod 中包含两个容器,其中 Node.js 容器仍然负责处理 HTTP 请求,而新加的 Envoy 容器负责转发 HTTPS 请求。
架构示意图

创建如下清单文件 kubia-ssl.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: v1
kind: Pod
metadata:
name: kubia-ssl
spec:
containers:
- name: kubia
image: luksa/kubia:1.0
ports:
- name: http
containerPort: 8080
- name: envoy
image: luksa/kubia-ssl-proxy:1.0
ports:
- name: https
containerPort: 8443
- name: admin
containerPort: 9901

运行 kubectl apply -f kubia-ssl.yml 命令应用清单文件创建 Pod,之后等待创建完成即可。
可以运行 kubectl describe pod kubia-ssl 命令查看创建的进度。

与两个容器的 Pod 进行交互
使用 kubectl port-forward 命令启用端口转发:

1
2
3
4
5
6
7
$ kubectl port-forward kubia-ssl 8080 8443 9901
Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080
Forwarding from 127.0.0.1:8443 -> 8443
Forwarding from [::1]:8443 -> 8443
Forwarding from 127.0.0.1:9901 -> 9901
Forwarding from [::1]:9901 -> 9901

打开一个新的命令行窗口,使用 curl 命令测试与 kubia-ssl 应用的连通性:

1
2
3
4
$ curl localhost:8080
Hey there, this is kubia-ssl. Your IP is ::ffff:127.0.0.1.
$ curl https://localhost:8443 --insecure
Hey there, this is kubia-ssl. Your IP is ::ffff:127.0.0.1.

查看日志
由于 kubia-ssl 包含两个容器,因此查看日志时需要使用 --container-c 选项指定容器的名称。

1
2
3
4
5
6
7
8
9
10
$ kubectl logs kubia-ssl -c kubia
Kubia server starting...
Local hostname is kubia-ssl
Listening on port 8080
Received request for / from ::ffff:127.0.0.1
Received request for / from ::ffff:127.0.0.1
$ kubectl logs kubia-ssl -c envoy
[2021-12-28 15:08:51.671][1][info][main] [source/server/server.cc:255] initializing epoch 0 (hot restart version=11.104)
[2021-12-28 15:08:51.671][1][info][main] [source/server/server.cc:257] statically linked extensions:
...

或者使用 kubectl logs kubia-ssl --all-containers 命令显示所有容器的日志

删除 Pod 和其他对象

删除 kubia Pod:

1
2
$ kubectl delete pod kubia
pod "kubia" deleted

kubectl delete 命令默认会等待删除操作彻底完成后才退出,可以加上 --wait=false 异步执行此命令。

删除清单文件中定义的对象:

1
2
$ kubectl delete -f kubia-ssl.yml
pod "kubia-ssl" deleted

删除所有 Pod:kubectl delete po --all
删除所有对象:kubectl delete all --all

参考资料

Kubernetes in Action, Second Edition