Kubernetes Pod

Pod 是由一个或多个容器构成的集合,被 Kubernetes 作为一个整体进行部署和调度,是 Kubernetes 调度的最小单元。同一个 Pod 内的容器共享 network namespaceUTS nanespace

环境信息

  • Centos7
  • Kubernetes v1.24.7

Pod 创建过程

当用户在 Kubernetes 中创建了一个 Pod 后,CRI 和 CNI 协同创建 Pod 所属容器,并为 Pod 中的容器初始化网络协议栈的具体过程如下

  1. 用户在 Kubernetes 中创建了一个 Pod 后,Kubelet 接收到创建新 Pod 的任务,首先调用 CRI 创建 Pod 内的容器

  2. Pod 中第一个被创建的容器是 pause 容器。pause 容器中运行着一个功能非常简单的 C 程序,具体逻辑是把自己永远阻塞,没有实际的业务逻辑,主要功能是用来占用一个 network namespace

    创建 pause 容器,使用 docker none 网络模式,创建出来的容器除了 lo 回环网卡外没有其他网络设备。

  3. Pod 内的其他用户容器通过加入 pause 容器已占用的 network namespace 的方式共享同一个 network namespace。对应于 docker 的 Container 模式

    其他用户容器都使用 pause 容器的主机名,但并不使用同一个 UTS namespace。

  4. CNI 负责 Pod 中容器的网络初始化工作。主要为 Pod 内的 pause 容器添加 eth0 网卡、分配 IP、配置网关等。

pause 容器

pause 容器是 Pod 中被创建的第一个容器,Pod 中的其他容器通过使用 pause 容器的 network namespace 共享网络协议栈和主机名。它是 Pod 中其他容器的父容器。

1.8 以后版本默认不启用 PID namespace 共享。每个容器拥有独立的 PID namespace。

以下步骤通过 docker 演示 Pod 容器创建的整个过程。首先,创建 pause 容器

docker run --name=test-pause \
--hostname=test-pause \
--network=none \
--workdir=/ \
--log-opt max-size=100m \
--runtime=runc \
--detach=true \
k8s.gcr.io/pause:3.4.1

然后在 Pod 中运行其他容器,本示例启动 2 个应用容器,一个 nginx 容器,里面启动了 nginx 服务,一个自定义的容器,里面包含常用工具

$ docker run --name=pod_nginx_test \
--network=container:0eec8dd9d164 \
--restart=no --log-opt max-size=100m \
--runtime=runc --detach=true \
nginx

$ docker run --name=pod_centos_test \
--network=container:0eec8dd9d164 \
--restart=no --log-opt max-size=100m \
--runtime=runc --detach=true \
centos7:my ping 127.1

$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
54d57add44d2 centos7:my "ping 127.1" 3 seconds ago Up 2 seconds pod_centos_test
cd0c661774e2 nginx "/docker-entrypoint.…" 2 minutes ago Up 2 minutes pod_nginx_test
0eec8dd9d164 k8s.gcr.io/pause:3.4.1 "/pause" 10 minutes ago Up 10 minutes test-pause

进入 pod_centos_test 容器,查看网络、端口、主机名信息

$ hostname
test-pause

$ s -elf
F S UID PID PPID C PRI NI ADDR SZ WCHAN STIME TTY TIME CMD
4 S root 1 0 0 80 0 - 6218 skb_wa 17:01 ? 00:00:00 ping 127.1
4 S root 7 0 0 80 0 - 2959 do_wai 17:02 pts/0 00:00:00 bash
0 R root 30 7 0 80 0 - 12935 - 17:08 pts/0 00:00:00 ps -elf

$ netstat -anutp
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN -
tcp6 0 0 :::80 :::* LISTEN -

$ ip add
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever

$ curl localhost:80
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>
  • Pod 中的容器主机名相同,都使用 pause 容器的主机名,但实际并未共享 UTS namespace。

    可以通过找到对应 docker 容器的进程 PID,找到对应进程的 namespace 信息对比确认

    $ ps -elf | grep ping
    4 S root 35333 35311 0 80 0 - 6218 skb_wa 01:01 ? 00:00:00 ping 127.1

    $ ls -l /proc/35333/ns/
    total 0
    lrwxrwxrwx 1 root root 0 Apr 8 01:13 cgroup -> cgroup:[4026531835]
    lrwxrwxrwx 1 root root 0 Apr 8 01:02 ipc -> ipc:[4026532664]
    lrwxrwxrwx 1 root root 0 Apr 8 01:02 mnt -> mnt:[4026532662]
    lrwxrwxrwx 1 root root 0 Apr 8 01:02 net -> net:[4026532588]
    lrwxrwxrwx 1 root root 0 Apr 8 01:02 pid -> pid:[4026532665]
    lrwxrwxrwx 1 root root 0 Apr 8 01:13 pid_for_children -> pid:[4026532665]
    lrwxrwxrwx 1 root root 0 Apr 8 01:13 user -> user:[4026531837]
    lrwxrwxrwx 1 root root 0 Apr 8 01:02 uts -> uts:[4026532663]

    $ ps -elf | grep nginx
    4 S root 34909 34887 0 80 0 - 2233 sigsus 00:59 ? 00:00:00 nginx: master process nginx -g daemon off;

    $ ls -l /proc/34909/ns/
    total 0
    lrwxrwxrwx 1 root root 0 Apr 8 01:14 cgroup -> cgroup:[4026531835]
    lrwxrwxrwx 1 root root 0 Apr 8 01:00 ipc -> ipc:[4026532660]
    lrwxrwxrwx 1 root root 0 Apr 8 01:00 mnt -> mnt:[4026532658]
    lrwxrwxrwx 1 root root 0 Apr 8 01:00 net -> net:[4026532588]
    lrwxrwxrwx 1 root root 0 Apr 8 01:00 pid -> pid:[4026532661]
    lrwxrwxrwx 1 root root 0 Apr 8 01:14 pid_for_children -> pid:[4026532661]
    lrwxrwxrwx 1 root root 0 Apr 8 01:14 user -> user:[4026531837]
    lrwxrwxrwx 1 root root 0 Apr 8 01:00 uts -> uts:[4026532659]

    $ ps -elf | grep pause
    4 S root 34239 34221 0 80 0 - 241 ia32_s 00:51 ? 00:00:00 /pause

    $ ls -l /proc/34239/ns/
    total 0
    lrwxrwxrwx 1 root root 0 Apr 8 01:15 cgroup -> cgroup:[4026531835]
    lrwxrwxrwx 1 root root 0 Apr 8 00:51 ipc -> ipc:[4026532585]
    lrwxrwxrwx 1 root root 0 Apr 8 00:51 mnt -> mnt:[4026532583]
    lrwxrwxrwx 1 root root 0 Apr 8 00:51 net -> net:[4026532588]
    lrwxrwxrwx 1 root root 0 Apr 8 00:51 pid -> pid:[4026532586]
    lrwxrwxrwx 1 root root 0 Apr 8 01:15 pid_for_children -> pid:[4026532586]
    lrwxrwxrwx 1 root root 0 Apr 8 01:15 user -> user:[4026531837]
    lrwxrwxrwx 1 root root 0 Apr 8 00:51 uts -> uts:[4026532584]

    以上观察可以看到,Pod 中的容器共享了 network namespace,未共享 UTS namespace。Kubernetes 中 Pod 中的容器同理。

  • Pod 中容器的 PID namespace 进行了隔离,各个容器的 PID 进行了隔离,PID namespace 不同。
  • Pod 中的容器共享了 network namespace,具有相同的网络信息(网卡、IP、端口资源)
  • Pod 中的容器直接可以通过 localhost 互相访问

Pod 和 Kubernetes API 服务交互

downwardAPI 提供了一种简单的方式,可以将 Pod 的 Metadata 和状态数据通过环境变量或者 Volume 传递(挂载)到 Pod 中,允许内部进程使用。但这种方式仅仅可以暴露一个 Pod 自身的 Metadata ,而且只能暴露一部分数据,某些情况下,应用可能需要知道其他 Pod 的信息,甚至集群中其他资源的信息,这种情况下,downwardAPI 的方式就无能为力了。

Pod 启动时,系统会主动将部分信息以环境变量的形式注入到每个 Pod 中,比如自定义的 Service 的部分信息,Kubernetes API Service 的信息等,可以登陆 Pod,检查系统环境变量

$ printenv | grep "SERVICE"
KUBERNETES_SERVICE_PORT_HTTPS=443
TEST_API_SERVICE_PORT=tcp://10.106.126.96:10302
KUBERNETES_SERVICE_PORT=443
TEST_API_SERVICE_PORT_10302_TCP_PORT=10302
TEST_API_SERVICE_PORT_10302_TCP_PROTO=tcp
TEST_API_SERVICE_SERVICE_HOST=10.106.126.96
TEST_API_SERVICE_SERVICE_PORT=10302
TEST_API_SERVICE_PORT_10302_TCP=tcp://10.106.126.96:10302
TEST_API_SERVICE_SERVICE_PORT_TEST_API_PORD=10302
TEST_API_SERVICE_PORT_10302_TCP_ADDR=10.106.126.96
KUBERNETES_SERVICE_HOST=10.96.0.1

以上示例中的 Pod ,在启动时,系统主动注入了相同 namespace 中的 Service 的信息,包括 Service IP 地址、端口等信息,以及 Kubernetes API Service 的 IP 及端口信息。

Pod 中要使用 Service 的环境变量,要确保 Service 在 Pod 启动之前被创建,否则 Pod 中不存在相关环境变量。

Pod 启动时,系统除了主动注入环境变量,还通过创建 projected 的 Volume 的方式给每个 Pod 挂载了以下信息

  • 所属 namespace 的默认 ServiceAccount 的 token(serviceAccountToken
  • 集群的 CA 证书(configMap
  • Pod 的所属 namespace 信息(downwardAPI

可以通过以下命令验证上述信息。其中的 Volume kube-api-access-8ch7r 为系统自动生成并挂载到了 Pod 中的容器的 /var/run/secrets/kubernetes.io/serviceaccount 目录

$ kubectl edit pod -n ops ops-centos7-bfc4d75b5-kjcfs
apiVersion: v1
kind: Pod
metadata:
labels:
app: ops-centos7
name: ops-centos7-bfc4d75b5-kjcfs
namespace: ops
spec:
containers:
- command:
- ping
- 127.0.0.1
image: centos:centos7.9.2009
name: ops-centos7

volumeMounts:
- mountPath: /var/run/secrets/kubernetes.io/serviceaccount
name: kube-api-access-8ch7r
readOnly: true

volumes:
- name: kube-api-access-8ch7r
projected:
defaultMode: 420
sources:
- serviceAccountToken:
expirationSeconds: 3607
path: token
- configMap:
items:
- key: ca.crt
path: ca.crt
name: kube-root-ca.crt
- downwardAPI:
items:
- fieldRef:
apiVersion: v1
fieldPath: metadata.namespace
path: namespace

kube-api-access-8ch7r 主要包含 3 部分内容

  • serviceAccountToken 每个 namespace 都有个默认的 ServiceAccount,在没有为 Pod 配置 ServiceAccount 的情况下,系统会自动将默认的 ServiceAccount 挂载到 Pod 中的每个容器,作为运行在 Pod 中的进程和 API Server 交互时认证授权的身份(主体)。ServiceAccount 使用说明参考
  • configMap - 以 configMap 的方式为 Pod 中的容器挂载了集群的 CA 证书
  • downwardAPI - 以 downwardAPI 的方式,让 Pod 中的容器可以获取到 Pod 所在的 namespace 信息。

以上内容被挂载到了容器的 /var/run/secrets/kubernetes.io/serviceaccount 目录中,可以在容器中检查此目录

$ ls /var/run/secrets/kubernetes.io/serviceaccount/
ca.crt namespace token

在 Pod 中通过 curl 命令请求 Kubernetes API

默认情况下,Kubernetes API Service 的 Service 名称为 kubernetes,因此在集群内部,可以通过 FQDN kubernetes.default.svc.cluster.local 访问 Kubernetes API Service

$ kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 156d

在 Pod 中尝试访问 Kubernetes API Service,

$ curl https://kubernetes.default.svc.cluster.local
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.haxx.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.

以上报错,是因为通过 https 访问 Kubernetes API Service,但是无法验证证书。最简单的规避此问题的方法是使用 curl 命令的 -k, --insecure 选项忽略验证证书,但这是存在安全隐患的,不建议在生产环境使用。

Pod 启动时,系统已经将集群的 CA 证书注入到了每个容器,因此可以使用 CA 证书来验证服务器证书。执行以下命令,使用 CA 证书重新访问 Kubernetes API Service

$ curl --cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt https://kubernetes.default.svc.cluster.local
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {},
"status": "Failure",
"message": "forbidden: User \"system:anonymous\" cannot get path \"/\"",
"reason": "Forbidden",
"details": {},
"code": 403

可以看到,使用 CA 证书后,HTTPS 连接成功,Kubernetes API 返回了响应。以上 Kubernetes API 输出中,表面此次请求使用的是匿名用户,不具有 API 服务器的授权,因此拒绝访问。

默认情况下,Pod 使用的是 namespace 中的默认的 ServiceAccount token,默认不具有任何权限。

参考 RBAC 配置认证鉴权使 Pod 的容器可以和 Kubernetes API Server 通信

Pod 计算资源限制

创建 Pod 时,可以指定容器对 CPU 和 Memory 的资源请求量(requests)和资源限制量(’limits`),他们不是针对 Pod,而是针对 Pod 中的容器。Pod 对资源的请求量和限制量是它所包含的所有容器的请求量和限制量之和。

apiVersion: v1
kind: Pod
metadata:
name: ${NAME}
namespace: ${NAMESPACE}
spec:
containers:
- name: ${NAME}
image: nginx:1.14.2
ports:
- containerPort: 80
name: http-web

resources:
requests:
cpu: 200m
memory: 10Mi
limits:
cpu: 200m
memory: 10Mi

如果为容器的资源申请指定了 limits,但未指定 requests,并且没有应用准入时机制为该资源设置默认请求, 然后 Kubernetes 将复制你所指定的限制值,将其用作资源的请求值。

资源类型

CPU 和 内存 都是 资源类型。每种资源类型具有其基本单位。 CPU 表达的是计算处理能力,其单位是 Kubernetes CPU。内存的单位是字节。[2]

CPU 资源单位

CPU 资源的限制和请求以 “cpu” 为单位。 在 Kubernetes 中,一个 CPU 等于 1 个物理 CPU 核 或者 1 个虚拟核, 取决于节点是一台物理主机还是运行在某物理主机上的虚拟机。

1 CPU = 1000m 的 CPU 资源

Memory 资源单位

memory 的限制和请求以字节为单位。 你可以使用普通的整数,或者带有以下 数量后缀 的定点数字来表示内存:E、P、T、G、M、k。 你也可以使用对应的 2 的幂数:Ei、Pi、Ti、Gi、Mi、Ki。

查看节点资源总量

通过以下命令输出,可以看到节点的资源总量和可分配资源量

$ kubectl get nodes
...
Capacity:
cpu: 16
ephemeral-storage: 524276716Ki
hugepages-1Gi: 0
hugepages-2Mi: 0
memory: 32068748Ki
pods: 110
Allocatable:
cpu: 16
ephemeral-storage: 483173420666
hugepages-1Gi: 0
hugepages-2Mi: 0
memory: 31966348Ki
pods: 110

脚注