Lesson 02:容器底层三件套 —— 不依赖 Docker 手工构建容器

admin 发布于 14 天前 8 次阅读


学习目标

完成本课后,你将能够:

  • 使用 unshare 命令手动创建进程隔离环境,亲眼验证 Namespace 的效果
  • 使用 Cgroups 手动限制进程的 CPU 和内存,观察超限后进程被杀死
  • 理解 OverlayFS 分层挂载的工作原理
  • 认识到容器不是黑魔法,而是内核特性的组合

前置知识

  • Lesson 01 中关于 Namespace / Cgroups / OverlayFS 的概念

1. 用 unshare 亲手创建 Namespace 隔离

unshare 是 Linux 自带的命令,可以让我们手动创建新的 Namespace 并在其中运行进程。

实验 2.1:PID Namespace —— 让进程以为自己是 1 号

bash# 在宿主机上先看看当前的进程 ID
echo "当前 shell 的 PID: $$"

# 创建新的 PID + Mount Namespace 并启动一个 bash
sudo unshare --pid --mount --fork bash

# 现在我们在新的 Namespace 里了
# 重新挂载 /proc 让 ps 命令读取新 Namespace 的进程信息
mount -t proc proc /proc

# 查看进程列表
ps aux

预期结果:你会发现 ps aux 只显示了 2 个进程(bash 本身和 ps 命令),而且 bash 的 PID 变成了 1。在宿主机视角,这个 bash 可能是 PID 58392,但在新 Namespace 里,它"以为"自己是整个世界的 1 号进程。

bash# 退出隔离环境
exit

[!WARNING] 退出前不要忘记执行 exit。如果在隔离环境内做了破坏性操作(比如删除文件),由于 Mount Namespace 的存在,影响范围取决于你是否共享了宿主机的文件系统挂载。

实验 2.2:Network Namespace —— 独立的网络栈

bash# 创建一个名为 "test-ns" 的网络命名空间
sudo ip netns add test-ns

# 在这个独立的网络空间里查看网卡
sudo ip netns exec test-ns ip addr

# 对比宿主机的网卡
ip addr

预期结果:在 test-ns 中只有一个 lo(回环)网卡,且处于 DOWN 状态。它完全感知不到宿主机的 eth0 网卡。这就是每个 Pod 拥有独立 IP 地址的底层原理。

bash# 清理
sudo ip netns delete test-ns

为什么这很重要?

Kubernetes 中的每一个 Pod 实际上就是运行在独立 Namespace 中的一组进程。当你理解了 unshare 的原理,你就能理解:

  • 为什么 Pod 内的容器可以通过 localhost 互相通信(它们共享同一个 Network Namespace)
  • 为什么不同 Pod 之间需要通过 IP 地址通信(它们在不同的 Network Namespace 中)
  • 为什么容器内看到的进程列表和宿主机不一样(PID Namespace 隔离)

2. 用 Cgroups 手动限制进程资源

Cgroups(Control Groups)是 Linux 内核提供的资源配额机制。在现代 Linux 系统中(Ubuntu 22.04+),使用的是 Cgroups v2。

实验 2.3:限制进程的内存上限

bash# 查看当前系统的 cgroup 版本
mount | grep cgroup

# 创建一个 cgroup 子组来限制内存
sudo mkdir -p /sys/fs/cgroup/memory-test

# 设置内存上限为 50MB
echo "52428800" | sudo tee /sys/fs/cgroup/memory-test/memory.max

# 在这个受限的 cgroup 中启动一个进程尝试分配 100MB 内存
sudo bash -c 'echo $$ > /sys/fs/cgroup/memory-test/cgroup.procs && python3 -c "
data = []
for i in range(100):
    data.append(\"x\" * 1024 * 1024)  # 每次分配 1MB
    print(f\"已分配 {i+1} MB\")
"'

预期结果:当进程尝试分配超过 50MB 的内存时,Linux 内核会直接将它杀死。你会看到 Killed 输出。这就是 Kubernetes 中 OOMKilled(Out Of Memory Killed)的底层原理。

[!IMPORTANT] 在 Kubernetes 中,当你为 Pod 设置 resources.limits.memory: 512Mi 时,K8s 底层就是通过 Cgroups 来设置这个内存上限。超出限制的进程会被内核无条件杀死——这不是 K8s 的功能,而是 Linux 内核的行为。

bash# 清理
sudo rmdir /sys/fs/cgroup/memory-test 2>/dev/null

实验 2.4:限制进程的 CPU 使用率

bash# 创建 CPU 限制的 cgroup
sudo mkdir -p /sys/fs/cgroup/cpu-test

# 限制为 10% 的单核 CPU(period=100000us 中只能使用 10000us)
echo "10000 100000" | sudo tee /sys/fs/cgroup/cpu-test/cpu.max

# 在受限的 cgroup 中运行一个 CPU 密集型任务
sudo bash -c 'echo $$ > /sys/fs/cgroup/cpu-test/cgroup.procs && dd if=/dev/zero of=/dev/null bs=1M' &
BG_PID=$!

# 观察它的 CPU 使用率(应该被限制在 10% 左右)
sleep 2 && top -b -n 1 -p $BG_PID | tail -2

# 清理
sudo kill $BG_PID 2>/dev/null
sudo rmdir /sys/fs/cgroup/cpu-test 2>/dev/null

预期结果:即使 dd 想全力运行,它的 CPU 使用率也被锁死在约 10%。这就是 K8s 中 resources.limits.cpu 的底层实现。


3. OverlayFS 分层文件系统实操

OverlayFS 是容器镜像"千层饼"结构的底层支撑。我们可以手动搭建一个来理解它。

实验 2.5:手工构建分层文件系统

bash# 创建实验目录
mkdir -p /tmp/overlay-demo/{lower,upper,work,merged}

# lower 层 = 只读的基础镜像层
echo "我是基础层的文件,不可修改" > /tmp/overlay-demo/lower/base.txt
echo "我是共享的配置文件" > /tmp/overlay-demo/lower/config.txt

# 使用 OverlayFS 把 lower(只读层)和 upper(可写层)合并到 merged(视图层)
sudo mount -t overlay overlay \
  -o lowerdir=/tmp/overlay-demo/lower,upperdir=/tmp/overlay-demo/upper,workdir=/tmp/overlay-demo/work \
  /tmp/overlay-demo/merged

# 在合并视图中查看文件——两层的文件都可见
ls -la /tmp/overlay-demo/merged/

# 在合并视图中修改一个"只读层"的文件
echo "我被修改了!" >> /tmp/overlay-demo/merged/config.txt

# 验证:原始的只读层完全没变(Copy-on-Write 机制)
echo "=== 只读层原始文件 ==="
cat /tmp/overlay-demo/lower/config.txt

echo "=== 可写层的副本(修改写在这里)==="
cat /tmp/overlay-demo/upper/config.txt

预期结果

  • lower/config.txt 内容不变(只读层被保护)
  • upper/config.txt 出现了修改后的内容(Copy-on-Write:第一次写入时复制到上层)

这就是容器镜像的工作原理

  • lower = 镜像的只读层(基础 OS + 应用程序)
  • upper = 容器运行时的可写层
  • merged = 容器进程实际看到的文件系统
  • 100 个容器共享同一个 lower,各自有独立的 upper
bash# 清理
sudo umount /tmp/overlay-demo/merged
rm -rf /tmp/overlay-demo

[!NOTE] 当容器被删除时,它的 upper(可写层)一起被销毁,所有在容器运行期间写入的数据全部丢失。这就是为什么在后续课程中我们要学习 PV(持久卷)来保存重要数据。


4. 综合理解:容器 = Namespace + Cgroups + OverlayFS

现在你应该能清晰地理解:

容器 = Namespace(隔离视线) + Cgroups(限制资源) + OverlayFS(分层文件系统)

Docker 和 Containerd 做的事情,本质上就是把上面三个内核特性的操作封装成了简单的命令行工具。当你执行 docker run nginx 时,底层发生的就是:

  1. 用 OverlayFS 组装 Nginx 镜像的分层文件系统
  2. 用 clone() 系统调用创建新的 Namespace
  3. 用 Cgroups 设置资源限制
  4. 在隔离环境中启动 Nginx 进程

检查点

  •  能用 unshare 创建 PID 隔离的环境,并验证进程看到的 PID 变为 1
  •  能用 Cgroups 限制进程内存,观察到 OOMKilled 现象
  •  能解释 OverlayFS 的 Copy-on-Write 机制
  •  能说清楚"容器 = Namespace + Cgroups + OverlayFS"

面试高频题

Q:请解释容器的 Copy-on-Write 机制,它对生产环境有什么影响?

满分答案思路

  1. 容器镜像由多个只读层组成,容器启动时在最上层添加一个可写层
  2. 当容器修改只读层的文件时,先将文件复制到可写层再修改(Copy-on-Write)
  3. 生产影响:频繁修改大文件(如数据库)的写入性能较差,因为每次修改都要先复制。所以数据库等有状态应用应使用外部持久化存储(Volume),而不是直
此作者没有提供个人介绍。
最后更新于 2026-04-22