LabHub
学习 学习路径 课程

容器内部原理

用眼睛确认命名空间

在 LabHub 中继续学习

本实验在真正的 VM 中运行

这个环境不是 Pod,而是由 KubeVirt 启动的虚拟机。Linux 内核独立运行,systemd 真正管理服务,docker 也不是模拟工具,而是真正的 Docker 引擎。通过 docker run 启动的容器会成为实际进程,docker execdocker logs 也都能照常工作。

过去,这个实验在 Pod 中运行。由于该环境放弃了所有内核权限,启动容器的步骤无法进行,因此只能学习直接解开镜像归档的变通方法。现在不再需要绕路了。

有两点需要了解。

目标

亲自测量七类命名空间中的 pid、uts、mnt、net、user 五类,并通过 inode 编号证明,容器不是“箱子”,而是视野受限的普通进程。最后,只使用 Docker 重现两个容器共享网络命名空间的 Kubernetes Pod 结构。

为什么重要

容器故障排查之所以卡住,大多是因为不知道“这个资源是否被隔离”。端口为何冲突、容器内看到的文件在主机上的什么位置、Sidecar 为何通过 localhost 连接应用——答案都归结为一句话:“该资源属于哪个命名空间?”命名空间具有 inode 编号,并可通过 /proc/<pid>/ns/* 读取,因此这个问题可以通过测量而非猜测来回答。编号相同就是同一个命名空间,编号不同就是不同的命名空间,仅此而已。

步骤

  1. 创建 /root/int1 目录,在主机上将 /proc/self/ns/pid 链接所指向的值保存到 /root/int1/host-pid-ns.txt。文件内容应只有一行 pid:[숫자]
  2. 使用 alpine:3.20 镜像创建名为 dk-ns 的容器,添加 --hostname labhub-uts,以 sleep infinity 在后台运行;将该容器内部读取的 /proc/self/ns/pid 值保存到 /root/int1/ctr-pid-ns.txt。该值必须与第 1 步不同。
  3. dk-ns 内部以显示 PID 列的格式导出进程列表,保存到 /root/int1/pid1.txtdocker exec dk-ns ps -o pid,comm)。PID 1 应为 sleep——即启动容器时给出的命令。重点在于它既不是 systemd,也不是 init。直接在这台 VM 中运行 ps -e 时,PID 1 是 systemd,可用于对照。
  4. dk-ns 的主机名保存到 /root/int1/uts.txt。内容应只有一行 labhub-uts
  5. 将此环境的 /etc/os-release 和 alpine 镜像中的 /etc/os-release 追加到同一个文件 /root/int1/mnt-proof.txt 中。文件内必须同时出现主机侧的 ubuntu 和容器侧的 alpine。 将 docker run --rm alpine:3.20 cat /etc/os-release 的结果与这台 VM 中同一文件的内容追加到一个文件即可。也请一并输出 uname -r——**内核相同,只有文件树不同。**挂载命名空间所做的,归根结底只是把进程的 / 换成另一棵树,这两行便体现了这一事实。
  6. 将在 dk-ns 内读取的 /proc/net/dev 保存到 /root/int1/ctr-net.txt。其中必须有 lo: 行,而且内容必须与主机上的同一文件不同——接口配置和收发统计在每个命名空间中分别管理。
  7. 在后台启动一个加入 dk-ns 网络命名空间的 dk-sidecar 容器;将在 dk-ns 内读取的 /proc/self/ns/net 保存到 /root/int1/ns-a.txt,将在 dk-sidecar 内读取的同一值保存到 /root/int1/ns-b.txt。两个值都必须采用 net:[숫자] 格式,且彼此相同。
  8. 将主机的 /proc/self/uid_map 保存到 /root/int1/host-uidmap.txt,将在 dk-ns 内的 /proc/self/uid_map 保存到 /root/int1/ctr-uidmap.txt。两份映射内容必须不同。

参考

读取主机的 PID 命名空间标识符

创建 /root/int1 目录,在主机上将 /proc/self/ns/pid 链接所指向的值保存到 /root/int1/host-pid-ns.txt。文件内容应只有一行 pid:[숫자]

命名空间标识符通过 /proc/self/ns/ 下的符号链接公开。链接指向的字符串本身就是答案,因此不能用 cat,而要使用跟随链接的命令。文件中只能留下一行 pid:[숫자]

与容器的 PID 命名空间比较

使用 alpine:3.20 镜像创建名为 dk-ns 的容器,添加 --hostname labhub-uts,以 sleep infinity 在后台运行;将该容器内部读取的 /proc/self/ns/pid 值保存到 /root/int1/ctr-pid-ns.txt。该值必须与第 1 步不同。

必须在容器内部读取同一路径,才能得到不同的编号。如果在主机 Shell 中读取,就会得到与第 1 步完全相同的值并导致失败。后续步骤还要继续使用该容器,因此请用不会立即退出的命令启动,使其持续运行。

确认容器内的 PID 1

dk-ns 内部以显示 PID 列的格式导出进程列表,保存到 /root/int1/pid1.txtdocker exec dk-ns ps -o pid,comm)。PID 1 应为 sleep——即启动容器时给出的命令。重点在于它既不是 systemd,也不是 init。直接在这台 VM 中运行 ps -e 时,PID 1 是 systemd,可用于对照。

导出进程列表时必须同时显示 PID 列。容器的主命令就是 1 号进程。如果在主机上导出,1 号会是另一个进程。

由 UTS 命名空间隔离的主机名

dk-ns 的主机名保存到 /root/int1/uts.txt。内容应只有一行 labhub-uts

此步骤不是修改主机名,而是确认创建容器时指定的主机名借助 UTS 命名空间与主机彼此独立。请在容器内查询主机名,并只保存该值。

挂载命名空间——相同内核,不同文件树

将此环境的 /etc/os-release 和 alpine 镜像中的 /etc/os-release 追加到同一个文件 /root/int1/mnt-proof.txt 中。文件内必须同时出现主机侧的 ubuntu 和容器侧的 alpine。 将 docker run --rm alpine:3.20 cat /etc/os-release 的结果与这台 VM 中同一文件的内容追加到一个文件即可。也请一并输出 uname -r——**内核相同,只有文件树不同。**挂载命名空间所做的,归根结底只是把进程的 / 换成另一棵树,这两行便体现了这一事实。

只保存一方就无法比较。请将主机读取的内容与容器内读取的内容追加到同一个文件。追加时不要覆盖写入,应使用追加重定向。

网络命名空间的接口列表

将在 dk-ns 内读取的 /proc/net/dev 保存到 /root/int1/ctr-net.txt。其中必须有 lo: 行,而且内容必须与主机上的同一文件不同——接口配置和收发统计在每个命名空间中分别管理。

接口列表也可以通过 /proc/net/dev 读取(在此环境中比 ip/ifconfig 更可靠)。从容器内读取时,应只能看到 lo 和容器接口等少数条目,行数应少于主机。

让两个容器共享命名空间

在后台启动一个加入 dk-ns 网络命名空间的 dk-sidecar 容器;将在 dk-ns 内读取的 /proc/self/ns/net 保存到 /root/int1/ns-a.txt,将在 dk-sidecar 内读取的同一值保存到 /root/int1/ns-b.txt。两个值都必须采用 net:[숫자] 格式,且彼此相同。

有一个选项可以让容器加入其他容器的命名空间,而不是创建新的网络命名空间。这与 Kubernetes Pod 中 pause 容器所做的事情相同。如果加入成功,两个容器的 net inode 编号必须完全相同。

用户命名空间的 UID 映射

将主机的 /proc/self/uid_map 保存到 /root/int1/host-uidmap.txt,将在 dk-ns 内的 /proc/self/uid_map 保存到 /root/int1/ctr-uidmap.txt。两份映射内容必须不同。

/proc/self/uid_map컨테이너안UID 호스트UID 개수 三列组成。此环境采用 rootless 模式,因此主机侧 Shell 与容器内部的映射会不同——请思考它为何与阅读材料中的实测表(Docker 默认设置下 user 命名空间相同)不同。