WEBKT

深入浅出 Kubernetes Pause 容器:Pod 背后那个默默无闻的“沙箱”

4 0 0 0

在 Kubernetes 的世界里,我们每天都在跟 Pod 打交道。你可能已经知道,Pod 是 K8s 的最小调度单元,它由一个或多个紧密关联的业务容器组成。

但如果你登录到一个 K8s 节点,通过 docker pscrictl 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 中,我们可以这样操作:

  1. 启动 Container A:
    docker run -d --name container_A nginx
    
  2. 启动 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) |      |
   |  +------------+              +------------+      |
   +--------------------------------------------------+
  1. 第一步:先启动 Pause 容器。它会初始化好 Net、IPC、UTS、PID(如果开启了共享)等 Namespace。
  2. 第二步:配置好 Pod 的 IP 地址、网卡等网络基础设施。
  3. 第三步:依次启动业务容器(如 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 的进程(通常是 systemdinit)有一个神圣的职责:收割僵尸进程(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;
}

这段代码的核心就两件事:

  1. 监听 SIGCHLD 信号:一旦有子进程退出,立即调用 waitpid 进行回收,防止产生僵尸进程。
  2. 死循环调用 pause():这是一个 Linux 系统调用,使调用进程进入睡眠状态,直到收到信号。这意味着它不消耗任何 CPU 资源。

五、 动手验证:Pause 容器如何影响网络

我们可以通过一个简单的实验,直观地感受 Pause 容器的存在。

  1. 编写一个简单的 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"]
  1. 部署该 Pod 并定位到它所在的 Worker 节点,通过 crictl(或 docker)查看:
# 找到该 Pod 对应的容器
$ crictl ps | grep share-net-pod

你会发现,除了 nginx-containerbusybox-container 之外,还有一个 Sandbox 容器,它就是 Pause。

  1. 进入 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 时,别忘了那个在后台默默无闻、体量微小却无比关键的“沙箱”守护者。

云原生探路者 KubernetesPause 容器容器网络

评论点评