如果你使用 Kubernetes,那么你对kubectl exec -it <pod-name> -- sh这个命令一定不会陌生。它是我们调试容器、查看日志或排查问题的“瑞士军刀”。我们通常的理解是:“哦,它是在容器内部启动了一个 shell”。
但这个理解并不完全准确,甚至有些误导。更精确的说法是:它在宿主机上启动了一个 shell 进程,并巧妙地让这个进程‘加入’了目标容器所在的全部 Linux 命名空间(Namespace)。
今天,我们就来掀开帷幕的一角,看看这个魔法是如何通过 Linux 命名空间实现的。
一、基石:理解 Pod 的共享命名空间
在深入exec之前,必须理解 Pod 的本质。Kubernetes 的一个核心设计是:一个 Pod 内的所有容器共享一组 Linux 命名空间。
这意味着什么呢?这意味着同一个 Pod 里的两个容器:
- 看到的是同样的网络设备(共享
netnamespace) - 拥有同样的主机名(共享
utsnamespace) - 可以通过IPC机制通信(共享
ipcnamespace) - 甚至可以通过
pid命名空间看到彼此的进程(如果配置了共享)
当我们说“进入一个容器”,本质上就是想要进入这组被共享的命名空间集合。
二、kubectl exec 的旅程:从客户端到容器运行时
当你敲下kubectl exec -it my-pod -- sh,背后发生了一系列复杂的交互:
- API 请求:
kubectl并非直接联系你的 Node 节点,而是向 Kubernetes API Server 发送一个请求:“请在my-pod中执行sh命令”。 - 路由与授权:API Server 进行认证和授权后,知道
my-pod运行在哪个节点上,于是将这个请求转发给该节点上的kubelet(节点代理)。 - 调用运行时:
kubelet接收到请求,转而调用本地的容器运行时(如containerd或CRI-O)。
至此,所有流程都是标准的 Kubernetes 控制平面通信。真正的魔法发生在容器运行时接下来做的事情上。
三、核心魔法:nsenter 与 setns()
容器运行时(或其 CRI 插件)需要完成最终的任务:在宿主机上启动一个/bin/sh进程,并让它加入到目标容器的命名空间中。
它是如何做到的呢?
定位目标:运行时首先找到目标 Pod 的“暂停容器”(
pause)或你指定的业务容器在宿主机上的真实进程 ID(PID)。我们称之为<target-pid>。加入命名空间:这是最关键的一步。运行时不会在容器内启动进程,而是在宿主机上,通过类似下面的操作(实际是调用
setns()系统调用)来启动 shell:这是一个概念性类比,实际是代码调用系统调用
nsenter --target <target-pid> \ --mount \ # 加入 Mount NS:看到容器的文件系统 --net \ # 加入 Net NS:看到容器的网络栈 --pid \ # 加入 PID NS:看到容器内的进程 --ipc \ # 加入 IPC NS:可以使用IPC资源 --uts \ # 加入 UTS NS:看到容器的主机名 --cgroup \ # 加入 Cgroup NS:继承容器的资源限制 /bin/sh # 最后,在这个新上下文中执行 shell* `nsenter`(namespace enter)是一个 Linux 命令行工具,其功能就是让进程加入已有的命名空间。 * `--target` 指定了我们要“附身”的目标进程。 * 后面的一系列 `--<namespace>` 参数指明了我们要加入哪些类型的命名空间。- 建立连接:
-it参数要求交互式终端。运行时会建立到新sh进程的标准输入(stdin)、输出(stdout)和错误(stderr)的流连接,使得你可以像在本地一样与这个“容器内”的 shell 交互。
四、直观验证:看看宿主机上的进程树
让我们用一个具体的例子来巩固理解。假设一个简单的 Pod,其容器的主进程是/my-app,在宿主机上的 PID 是5678。
当你执行kubectl exec后,在宿主机上使用pstree -p 5678查看,你可能会看到这样的结构:
containerd(1234)───my-app(5678) # 容器原本的主进程
└─sh(7788) # kubectl exec 创建的进程!
看到了吗?sh(7788)这个进程本身就运行在宿主机上,是containerd的子进程。但它通过setns()系统调用,加入了my-app(5678)进程的所有命名空间。
因此,在这个sh进程里:
ps aux看到的是容器内部的进程列表(因为加入了pidnamespace)。ip addr看到的是容器的网络接口(因为加入了netnamespace)。ls /看到的是容器的根文件系统(因为加入了mountnamespace)。hostname看到的是容器的主机名(因为加入了utsnamespace)。
它完美地“扮演”了一个容器内部的进程。
结论
所以,下次当你使用kubectl exec时,可以这样理解:
你不是在启动一个“容器内的进程”,而是在启动一个“拥有容器视角的宿主机进程”。Kubernetes 和容器运行时通过 Linux 命名空间这面“镜子”,让这个外部进程看到了一个完全不同的、属于容器内部的世界。
这种基于命名空间的“附身”能力,正是容器技术轻量、高效和可调试性的完美体现。它模糊了容器内外的边界,让我们能从一个更高的维度去观察和管理这些被隔离的环境。