引言
Kubernetes出现的报错如下:
Failed to create pod sandbox: rpc error: code = Unknown desc = failed to get sandbox image "k8s.gcr.io/pause:3.5": failed to pull image "k8s.gcr.io/pause:3.5": failed to pull and unpack image "k8s.gcr.io/pause:3.5": failed to resolve reference "k8s.gcr.io/pause:3.5": failed to do request: Head "https://k8s.gcr.io/v2/pause/manifests/3.5": x509: certificate signed by unknown authority
k8s.gcr.io 这个地址是需要连外网才可以拉取到,导致 pause 镜像拉不下来,Pod无法启动。以前都没关注过 pause 这个容器,它是啥,做什么用的,怎么在 Pod 里没看到过他,本文将带你了解 pause 容器。
Pause容器是个啥
在Kubernetes中,Pod是最小的调度单元,但它的内部结构却充满了许多复杂的机制,其中之一就是Pause容器。尽管Pause容器看似不起眼,但它在整个Kubernetes集群中发挥了至关重要的作用。我们在 kubernetes 的 node 节点,执行 docker ps,可以发现每个 node 上都运行了一个 pause进程的容器,具体如下:
[root@localhost ~]# docker ps |grep traefik
66032431a20e 2ae1addee1b2 "/entrypoint.sh --gl…" 30 hours ago Up 30 hours k8s_traefik_traefik-68b9ccfc77-x8sqg_traefik_aa5b97bf-3db8-4b92-89a7-1fe551645e6a_0
10d393461904 registry.aliyuncs.com/google_containers/pause:3.5 "/pause" 30 hours ago Up 30 hours k8s_POD_traefik-68b9ccfc77-x8sqg_traefik_aa5b97bf-3db8-4b92-89a7-1fe551645e6a_0
会发现有很多 pause 容器运行于服务器上面,容器命名也很规范,然后每次启动一个容器,都会伴随一个pause这样的容器启动。那它究竟是干啥子的?它就是 Pause 容器,又叫 Infra 容器。我们在部署完 kubernetes 集群后,查看 kubelet 进程,可以看到配置中有这样一个参数:
[root@localhost ~]# ps -ef|grep kubelet
root 8675 1 10 Sep18 ? 03:15:07 /usr/bin/kubelet --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubernetes/kubelet.conf --config=/var/lib/kubelet/config.yaml --network-plugin=cni --pod-infra-container-image=registry.aliyuncs.com/google_containers/pause:3.5
pause 容器使用的镜像为 registry.aliyuncs.com/google_containers/pause:3.5 该镜像非常小,只有 683kB,由于它总是处于 Pause (暂时)状态,所以取名叫 pause
[root@localhost ~]# docker images|grep pause
registry.aliyuncs.com/google_containers/pause 3.5 ed210e3e4a5b 2 years ago 683kB
想了解该 pause 容器的构成(代码是用 C 语言写的)的可以去官方仓库上一看究竟:https://github.com/kubernetes/kubernetes/tree/master/build/pause
Pause容器的作用
- 网络命名空间隔离:Pod是Kubernetes中最小的调度单元,可以包含一个或多个容器。为了实现容器之间的网络隔离,每个Pod都有自己独立的网络命名空间。Pause容器负责创建并维护这个网络命名空间,其他容器共享这个网络命名空间,使它们能够相互通信,而不会与其他Pod中的容器发生冲突。
- 进程隔离:Pause容器保持一个轻量级的进程运行,即使Pod中的其他容器都停止了。这个进程实际上不执行任何有用的工作,但它的存在确保了Pod不会在没有容器运行的情况下被删除。当其他容器停止时,Pause容器仍在运行,以维持Pod的生命周期。
- 资源隔离:尽管Pause容器通常不分配大量的CPU和内存资源,但它可以配置以使用一些资源。这有助于确保即使Pod中没有其他容器运行时,Kubernetes仍然可以监控和管理Pod的资源使用情况。这也有助于防止Pod被其他具有相同资源要求的Pod占用。
- IP地址维护:Pause容器负责维护Pod的IP地址。Pod的IP地址通常是动态分配的,但由于Pause容器一直在运行,它可以维护Pod的IP地址,以便其他容器可以通过该地址进行通信。这有助于确保Pod的IP地址在整个Pod的生命周期内保持一致。
- 生命周期管理:Pause容器的生命周期与Pod的生命周期相同。当Pod创建时,Pause容器被创建;当Pod删除时,Pause容器也会被删除。这确保了Pod的整个生命周期都由Kubernetes进行管理,包括创建、扩展、缩放和删除。
Pause容器工作原理
一个 Pod 可以由一组容器组成的,这些容器之间共享存储和网络资源,那么网络资源是如何共享的呢?下面是个例子:
图片
比如说现在有一个 Pod,其中包含了一个容器 A 和一个容器 B,它们两个就要共享 Network Namespace。在 Kubernetes 里的解法是这样的:它会在每个 Pod 里,额外起一个 Infra container 小容器来共享整个 Pod 的 Network Namespace。Infra container 是一个非常小的镜像,大概 683kB,是一个C语言写的、永远处于“暂停”状态的容器。由于有了这样一个 Infra container 之后,其他所有容器都会通过 Join Namespace 的方式加入到 Infra container 的 Network Namespace 中。所以说一个 Pod 里面的所有容器,它们看到的网络视图可以说是完全一样的。即:它们看到的网络设备、IP地址、Mac地址等等,跟网络相关的信息,其实全是一份,这一份都来自于 Pod 第一次创建的这个 Infra container。这就是 Pod 解决网络共享的一个解法。在 Pod 里面,一定有一个 IP 地址,是这个 Pod 的 Network Namespace 对应的地址,也是这个 Infra container 的 IP 地址。所以大家看到的都是一份,而其他所有网络资源,都是一个 Pod 一份,并且被 Pod 中的所有容器共享。这就是 Pod 的网络实现方式。由于需要有一个相当于说中间的容器存在,所以整个 Pod 里面,必然是 Infra container 第一个启动。并且整个 Pod 的生命周期是等同于 Infra container 的生命周期的,与容器 A 和 B 是无关的。这是非常重要的一个设计。kubernetes的pause容器主要为每个业务容器提供两个核心功能:
- 第一,它提供整个pod的Linux命名空间的基础。
- 第二,启用PID命名空间,它在每个pod中都作为PID为1的进程,并回收僵尸进程。
手工模拟Pod
我们已经知道,一个 Pod 从表面上来看至少由一个容器组成,而实际上一个 Pod 至少要有包含两个容器,一个是应用容器,一个是 pause 容器。运行一个pause容器:
[root@localhost ~]# docker run -d --name pause -p 8080:80 registry.aliyuncs.com/google_containers/pause:3.5
fd315974f5d1a5f52ca47c5dc31aea3774cebf90c88ce065cc9e9ea2f52c103c
- --name:指定 pause 容器的名字,pause
- -p 8080:80:将宿主机的 8080 端口映射到容器的 80 端口
运行一个nginx容器,代理 127.0.0.1:8888 springboot应用程序
# 准备nginx配置文件
[root@k8s001 ~]# cat <<EOF >> nginx.conf
error_log stderr;
events { worker_connections 1024; }
http {
server {
listen 80 default_server;
server_name www.kubesre.com;
location / {
proxy_pass http://127.0.0.1:8888;
}
}
}
EOF
# 创建nginx容器
[root@localhost ~]# docker run -d --name nginx -v `pwd`/nginx.conf:/etc/nginx/nginx.conf --net=container:pause --ipc=container:pause --pid=container:pause --ipc=shareable nginx
fa9f858adae826ad536178747e00fffc829c7baf98c3bc29e945230abbf2a5cb
- --net=container:pause:用于与另一个容器共享网络命名空间。在这种情况下,容器 "nginx" 会与名为 "pause" 的容器共享网络命名空间,它们可以使用相同的网络配置和接口。
- --ipc=container:pause:用于与另一个容器共享 IPC 命名空间。IPC 命名空间允许容器之间进行进程间通信(Inter-Process Communication),在这里,容器 "nginx" 与名为 "pause" 的容器共享 IPC 命名空间。
- --pid=container:pause:用于与另一个容器共享 PID 命名空间。PID 命名空间允许容器查看和管理其他容器的进程。
- --ipc=shareable:指示 IPC 命名空间是可共享的,以便其他容器也可以加入到这个共享命名空间中。
创建一个应用容器 springboot
[root@localhost ~]# docker run -d --name springboot --net=container:pause --ipc=container:pause --pid=container:pause --ipc=shareable registry.cn-shanghai.aliyuncs.com/kubesre02/springboot
e33cfa3cebd5aafa714ca6ef0f6a16be52a282c64b8d24b2d98890ccf02c436a
到这里,我们就纯手工模拟出了一个符合 K8S Pod 模型的 “Pod” ,只是它并不由 K8S 进行管理。验证,查看运行的容器
[root@localhost ]~# docker ps | grep -E "pause|nginx|springboot"
4f877cdcba5d registry.cn-shanghai.aliyuncs.com/kubesre02/springboot "java -jar /app.jar" 3 seconds ago Up 2 seconds springboot
e541dc010fb3 nginx "/docker-entrypoint.…" 19 hours ago Up 19 hours nginx
09f94a052d50 registry.aliyuncs.com/google_containers/pause:3.5 "/pause" 19 hours ago Up 19 hours 0.0.0.0:8080->80/tcp, :::8080->80/tcp pause
通过浏览器访问 http://ip:8080 端口
[root@localhost ~]# curl http://localhost:8080
Hello Docker World
从上面的步骤可见:
- pause容器将内部80端口映射到宿主机8080端口。
- pause容器在宿主机上设置好网络namespace后,nginx容器加入到该网络的namespace中。
- nginx容器启动的时候指定了-net=container:pause。
- springboot 容器启动时,同样方式加入到该网络的namespace中。
- 这样三个容器共享了网络,互相之间就可以使用localhost直接通信。
- --ipc=container:pause,--pid=container:pause就是三个容器的ipc和pid处于同一个namespace中,init进程为pause。
这里,我们进入springboot 容器内部查看:
[root@localhost ~]# /tmp/test# docker exec -it springboot sh
/ # ps aux
PID USER TIME COMMAND
1 65535 0:00 /pause
205 root 0:22 java -jar /app.jar
240 root 0:00 nginx: master process nginx -g daemon off;
261 101 0:00 nginx: worker process
263 root 0:00 sh
269 root 0:00 ps aux
在springboot 容器中可以看到pause和nginx容器的进程,并且pause容器的PID为1,而在kubernetes中容器的PID=1的进程则为容器本身的业务进程。
如果没有 K8S 的 Pod ,启动一个 业务容器,你需要手动创建三个容器,当你想销毁这个服务时,同样需要删除三个容器。而有了 K8S 的 Pod,这三个容器在逻辑上就是一个整体,创建 Pod 就会自动创建三个容器,删除 Pod 就会删除三个容器,从管理上来讲,方便了不少。
这正是 Pod 存在的一个根本意义所在。
如何回收僵尸进程
在Linux中,PID命名空间中的进程是一个树型结构,每个进程有一个父进程。在树的根上只有一个进程没有真正的父进程。这是init进程,其PID为1。
僵尸进程是指已经停止运行但它们的进程表条目仍然存在的进程,在UNIX系统中,一个子进程结束了,但是它的父进程没有等待(调用wait/waitpid)它,那么它将变成一个僵尸进程。
僵尸进程是怎么产生的?
出现僵尸进程的一种情况是:父进程编写得很糟糕,省略了wait调用,或者父进程意外崩溃在子进程之前死亡,而新的父进程没有调用wait。当一个进程的父进程在子进程之前死亡时,操作系统将该子进程分配给init进程或PID 1的进程。即init进程接纳子进程并成为其父进程。这意味着,现在当子进程退出时,新的父进程(init)必须调用wait来获取它的退出码,否则它的进程表条目将永远保留下来,成为僵死进程。
在Kubernetes pod中,容器的运行方式与上述基本相同,但是为每个pod创建了一个特殊的pause容器。
这个pause容器运行了一个非常简单的进程,它不执行任何函数,本质上永远休眠,其源码实现:
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
static void sigdown(int signo) {
psignal(signo, "Shutting down, got signal");
exit(0);
}
static void sigreap(int signo) {
while (waitpid(-1, NULL, WNOHANG) > 0);
}
int main() {
if (getpid() != 1)
/* Not an error because pause sees use outside of infra containers. */
fprintf(stderr, "Warning: pause should be the first process\n");
if (sigaction(SIGINT, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0)
return 1;
if (sigaction(SIGTERM, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0)
return 2;
if (sigaction(SIGCHLD, &(struct sigaction){.sa_handler = sigreap,
.sa_flags = SA_NOCLDSTOP},
NULL) < 0)
return 3;
for (;;)
pause();
fprintf(stderr, "Error: infinite loop terminated\n");
return 42;
}
从上述代码种我们可以发现,pause容器不仅仅调用pause()使进程休眠,还拥有另外一个重要的功能:
它假定自己为PID 1的角色,当僵尸进程被其父进程孤立时,会被pause容器进行收养,通过调用wait来获取僵尸进程。这样一来就不会在Kubernetes pod的PID命名空间中堆积僵尸进程了。
那为啥使用 kubectl create 或 kubectl apply 等命令创建Pod时,通常不会显式地看到Pause容器。这是因为Pause容器是由Kubernetes自动创建和管理的,通常不需要用户手动操作或关注。它是Pod的一个隐式组成部分,用于维护Pod的基础设施和容器之间的网络隔离。
不难想到,这其中的过程是非常复杂的。而且我们还没有深入探讨如何去监控和管理这些容器的生命周期。但是不用担心,我们不需要这么复杂的去管理我们的容器,因为kubernetes已经都为我们做好了。