深入浅出 Kubernetes Pause 容器:Pod 背后那个默默无闻的“沙箱”
在 Kubernetes 的世界里,我们每天都在跟 Pod 打交道。你可能已经知道,Pod 是 K8s 的最小调度单元,它由一个或多个紧密关联的业务容器组成。
但如果你登录到一个 K8s 节点,通过 docker ps 或 crictl ps 查看底层的容器,你会发现一些奇特的存在:
$ docker ps | grep pause
k8s.gcr.io/pause:3.6 "/pause" ... k8s_POD_nginx-pod_default_...
每个 Pod 启动时,都会先于你的业务容器运行一个名为 Pause(也常被称为 Infrastructure Container,基础设施容器)的容器。
这个只有几百 KB、几乎不消耗 CPU 和内存的镜像,为什么会成为每个 Pod 的“标配”?如果没了它,K8s 的 Pod 还能正常运转吗?本文将从 Linux 内核底层机制入手,为你彻底揭开 Pause 容器的神秘面纱。
一、 问题的根源:多容器协作的“纳秒级”痛点
在理解 Pause 容器之前,我们先思考一个最基础的问题:什么叫“Pod 里的容器共享网络和存储”?
在 Linux 中,容器的本质是“受限的进程”。隔离是通过 Namespace(命名空间) 实现的,而资源限制是通过 Cgroups 实现的。
如果我们要让 Container A 和 Container B 共享网络命名空间(Net Namespace),在原生的 Docker 中,我们可以这样操作:
- 启动 Container A:
docker run -d --name container_A nginx - 启动 Container B,并指定加入 Container A 的网络:
docker run -d --name container_B --net=container:container_A alpine sh
这时候,Container B 就会直接使用 Container A 的网络协议栈、网卡和 IP 地址。
但是,这种设计在分布式调度系统(如 K8s)中存在一个致命的缺陷:容器的对等性与生命周期耦合。
- 谁先启动? 如果 Container A 因为某种原因崩溃重启,Container B 的网络通道就会瞬间中断。
- 生命周期谁说了算? A 和 B 是对等的业务容器,谁也不应该成为谁的“宿主”。如果把网络“锚定”在 A 身上,A 一死,整个 Pod 事实上就瓦解了,即使 B 还在正常运行。
为了解决这个“鸡生蛋、蛋生鸡”的生命周期耦合问题,Kubernetes 引入了 Pause 容器。
二、 核心解法:引入一个永远不死的“上帝进程”
K8s 的解法非常巧妙:既然业务容器谁当“带头大哥”都不合适,那就引入一个专门用来占坑的、生命周期极长的、功能极度单一的容器作为整个 Pod 的基石。
这个容器就是 Pause 容器。
当我们创建一个 Pod 时,K8s 节点的 Kubelet 调用 CRI 运行时的步骤如下:
+--------------------------------------------------+
| Pod SandBox |
| |
| +---------------------+ |
| | Pause Container | |
| | (Holds Namespaces) | |
| +---------+-----------+ |
| | |
| +-------------+-------------+ |
| | | |
| v v |
| +------------+ +------------+ |
| | App A | | App B | |
| | (Business) | | (Business) | |
| +------------+ +------------+ |
+--------------------------------------------------+
- 第一步:先启动 Pause 容器。它会初始化好 Net、IPC、UTS、PID(如果开启了共享)等 Namespace。
- 第二步:配置好 Pod 的 IP 地址、网卡等网络基础设施。
- 第三步:依次启动业务容器(如 App A、App B),并将它们的 Namespace 挂载到 Pause 容器的 Namespace 之下。
因为 Pause 容器内部除了挂起进程外什么都不做,它几乎永远不会崩溃。只要 Pause 容器不死,Pod 的网络、存储等共享资源就一直存在。即使业务容器 A 和 B 重启了一万次,它们重新加入后,依然能拿到相同的 IP 并在同一个 Localhost 下通信。
三、 Pause 容器的两大核心职责
Pause 容器绝对不是“混吃等死”的闲职,它在内核层面承担了两个极其关键的底层职责。
1. 充当 Namespace 共享的“锚点”
这是它最基本的功能。在 K8s 中,通过将业务容器的 Namespace 绑定到 Pause 容器上,实现了以下共享:
- Net Namespace:Pod 内所有容器看到的是同一张网卡、同一个 IP,通过
localhost即可互相访问。 - IPC Namespace:允许 Pod 内的容器通过 System V IPC 或 POSIX 消息队列进行高性能的进程间通信。
- PID Namespace(可选):如果配置了
shareProcessNamespace: true,Pod 内的不同容器可以看见彼此的进程。
2. 扮演 PID 1 角色,收割“孤儿/僵尸进程”
这是很多人容易忽略、但对系统稳定性至关重要的一点。
在 Linux 中,进程的组织是一棵树。PID 为 1 的进程(通常是 systemd 或 init)有一个神圣的职责:收割僵尸进程(Zombie Processes)。
当一个子进程退出时,它的父进程必须调用 wait() 或 waitpid() 系统调用来读取子进程的退出状态,否则这个子进程就会变成“僵尸进程”,继续占用系统的进程表项(PID 资源)。
如果父进程在子进程退出之前就先死了,这个子进程就会变成孤儿进程。在 Linux 机制中,孤儿进程会被托管给系统的 PID 1 进程。PID 1 必须负责在这些孤儿进程结束时,“收割”它们。
在 K8s 中,如果开启了 shareProcessNamespace,业务容器中的进程可能会将 Pause 容器作为其父进程或祖先进程。如果某个业务进程生了子进程后异常退出,那些残留的子进程就会被 Pause 容器(PID 1)接管。
如果 Pause 容器不具备收割僵尸进程的能力,系统的 PID 资源很快就会被耗尽,导致节点无法创建新进程,整台机器陷入瘫痪。
四、 Pause 容器的源码有多简单?
Pause 容器的源码是用 C 语言写的,极其精简,完美诠释了 Unix 的“单一职责原则”。我们来看看它的核心逻辑(简化版):
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
// 信号处理函数:当收到子进程退出的信号(SIGCHLD)时,调用 waitpid 收割它
static void sigchld_handler(int sig) {
while (waitpid(-1, NULL, WNOHANG) > 0);
}
int main(int argc, char **argv) {
// 1. 注册信号处理,专门用来清理僵尸进程
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = sigchld_handler;
sa.sa_flags = SA_NOCLDSTOP;
if (sigaction(SIGCHLD, &sa, NULL) < 0) {
perror("sigaction");
return 1;
}
// 2. 忽略其他的控制信号,防止自己意外退出
signal(SIGINT, SIG_IGN);
signal(SIGTERM, SIG_IGN);
// 3. 进入无限挂起状态(调用 pause 内核系统调用)
for (;;) {
pause();
}
return 0;
}
这段代码的核心就两件事:
- 监听
SIGCHLD信号:一旦有子进程退出,立即调用waitpid进行回收,防止产生僵尸进程。 - 死循环调用
pause():这是一个 Linux 系统调用,使调用进程进入睡眠状态,直到收到信号。这意味着它不消耗任何 CPU 资源。
五、 动手验证:Pause 容器如何影响网络
我们可以通过一个简单的实验,直观地感受 Pause 容器的存在。
- 编写一个简单的 Pod 定义文件
two-containers.yaml,包含两个容器:一个 Nginx,一个 Busybox:
apiVersion: v1
kind: Pod
metadata:
name: share-net-pod
spec:
containers:
- name: nginx-container
image: nginx:alpine
- name: busybox-container
image: busybox
command: ["sh", "-c", "sleep 3600"]
- 部署该 Pod 并定位到它所在的 Worker 节点,通过
crictl(或docker)查看:
# 找到该 Pod 对应的容器
$ crictl ps | grep share-net-pod
你会发现,除了 nginx-container 和 busybox-container 之外,还有一个 Sandbox 容器,它就是 Pause。
- 进入 Busybox 容器,尝试访问
localhost:80(Nginx 默认端口):
$ kubectl exec -it share-net-pod -c busybox-container -- wget -O- http://localhost:80
你会发现能够成功拿到 Nginx 的欢迎页面!
这证明了 Busybox 和 Nginx 共享了同一个网络命名空间,而这个命名空间的“房东”正是那个只调用 pause() 系统调用的基础设施容器。
总结
Pause 容器是 Kubernetes 设计艺术的缩影。它通过解耦容器的物理实体与逻辑边界(Namespace),优雅地解决了分布式系统中多容器协作的生命周期管理难题。
- 它充当 Namespace 的锚点,保证了业务容器重启时网络和资源的连续性;
- 它作为 PID 1 进程,默默地在底层收割着孤儿进程,维护着节点操作系统的稳定。
下一次当你执行 kubectl get pods 时,别忘了那个在后台默默无闻、体量微小却无比关键的“沙箱”守护者。