202509
systems

从零构建最小 Docker 容器:OverlayFS 分层文件系统、命名空间隔离与 CGroup 资源限制

通过从零实现 Docker-like 容器,深入理解 OverlayFS 如何实现镜像分层、命名空间提供进程隔离,以及 CGroup 控制资源使用,提供可操作的代码参数和监控要点。

容器技术是现代云计算的基础,Docker 等工具简化了其使用,但理解底层机制有助于优化部署和故障排除。本文聚焦从零构建一个最小 Docker-like 容器,使用 OverlayFS 实现分层文件系统、Linux 命名空间(namespaces)提供进程隔离,以及控制组(cgroups)管理资源限制。通过这些核心组件,我们可以模拟 Docker 的运行时行为,揭示容器如何在共享内核上实现隔离与高效资源利用。

容器核心机制概述

容器并非虚拟机,而是利用 Linux 内核特性在宿主机上创建隔离环境。命名空间隔离进程视图,cgroups 限制资源消耗,OverlayFS 则处理镜像的分层存储。这些机制结合,形成了一个轻量级、可移植的运行环境。从证据看,Linux 内核自 3.8 版本起支持 OverlayFS,用于 Docker 的默认存储驱动;命名空间从 2.6.24 引入,支持 PID、网络、挂载等类型;cgroups v1/v2 则从 2.6.24 起演进,提供 CPU、内存等限额。

构建最小容器时,我们避免依赖 Docker 守护进程,直接使用系统调用如 unshare() 和 clone(),结合 chroot 切换根目录。这不仅揭示了 Docker run 的内部实现,还允许自定义参数以适应特定场景。例如,在高密度部署中,精确的 cgroups 配置可防止资源争抢,确保稳定性。

实现命名空间隔离

命名空间是容器隔离的基础,它为进程创建独立视图,如 PID 命名空间让容器内进程从 PID 1 开始,而非继承宿主机。证据显示,使用 unshare(CLONE_NEWPID | CLONE_NEWNS) 可创建新 PID 和挂载命名空间,避免进程逃逸。

可落地参数与清单:

  • 命令行实现:使用 unshare -p -m -f -r /bin/bash 创建 PID 和 mount 命名空间。参数 -p 表示 PID,-m 表示 mount,-f fork 后进入,-r 使进程成为命名空间根。
  • 监控要点:在容器内运行 ps aux,验证 PID 仅显示容器进程;使用 lsns -t pid 检查命名空间 ID,与宿主机隔离。
  • 代码清单(C 示例)
    #include <sched.h>
    #include <unistd.h>
    #define STACK_SIZE 4096
    char stack[STACK_SIZE];
    int child() {
        if (chroot("/tmp/rootfs") != 0) { perror("chroot"); return 1; }
        chdir("/");
        execl("/bin/bash", "bash", NULL);
        return 1;
    }
    int main() {
        int pid = clone(child, stack + STACK_SIZE, CLONE_NEWPID | CLONE_NEWNS | SIGCHLD, NULL);
        waitpid(pid, NULL, 0);
        return 0;
    }
    
    编译:gcc -o container container.c。运行时准备 /tmp/rootfs 目录,模拟根文件系统。此实现确保进程隔离,适用于调试场景。

风险:未正确配置 mount 命名空间可能导致文件系统泄露;建议结合 cap_sys_chroot 能力限制非 root 用户。

设置 cgroups 资源限制

cgroups 控制容器资源使用,如限制 CPU 份额或内存上限,防止单一容器耗尽宿主机资源。证据来自内核文档:cgroups v2 使用 cgroup.controllers 文件挂载统一层,v1 则需单独挂载 cpu、memory 子系统。Docker 默认使用 v2,自 2021 年起。

观点:通过 cgroups,容器实现公平调度;在多租户环境中,设置 50% CPU 限额可避免噪声邻居问题。

可落地参数与清单:

  • 创建 cgroup:cgcreate -g cpu,memory:/mycontainer。参数 -g 指定子系统 cpu 和 memory。
  • 设置限额:echo 50000 > /sys/fs/cgroup/cpu/mycontainer/cpu.cfs_quota_us(50ms/100ms 周期);echo 512M > /sys/fs/cgroup/memory/mycontainer/memory.limit_in_bytes。
  • 运行容器:cgexec -g cpu,memory:mycontainer ./container。监控:使用 cat /sys/fs/cgroup/memory/mycontainer/memory.usage_in_bytes 观察使用量。
  • 回滚策略:若超限,内核 OOM killer 终止进程;配置 memory.oom.group 统一杀进程组。
  • 清单
    1. 挂载 cgroup v2:mount -t cgroup2 none /sys/fs/cgroup。
    2. 创建子目录:mkdir /sys/fs/cgroup/mycontainer。
    3. 启用控制器:echo "+cpu +memory" > /sys/fs/cgroup/cgroup.subtree_control。
    4. 启动进程:进入 cgroup 后运行应用。

在生产中,结合 systemd 或 Kubernetes,使用 10% CPU 初始限额,动态调整基于负载。

使用 OverlayFS 实现分层文件系统

Docker 镜像由多层组成,OverlayFS 将上层(可写)叠加在下层(只读),实现高效分层。证据:man overlay 描述 lowerdir(基础层)、upperdir(变更层)、workdir(临时)。Docker 使用此驱动,scratch 镜像为空,仅添加应用层。

观点:OverlayFS 减少存储浪费;构建最小容器时,从 scratch 模拟,添加 busybox 等最小根文件系统。

可落地参数与清单:

  • 准备目录:mkdir -p lower upper work merged;复制基础文件到 lower(如从 busybox 静态二进制)。
  • 挂载 OverlayFS:mount -t overlay overlay -olowerdir=lower,upperdir=upper,workdir=work merged。参数 lowerdir 指定只读层,upperdir 捕获写操作。
  • 进入容器:chroot merged /bin/busybox sh。验证:echo "test" > file;在 upper 观察变更。
  • 代码清单(脚本)
    #!/bin/bash
    LOWER=$(pwd)/lower
    UPPER=$(pwd)/upper
    WORK=$(pwd)/work
    MERGED=$(pwd)/merged
    mkdir -p $LOWER $UPPER $WORK $MERGED
    # 模拟基础层:wget http://busybox.net/downloads/binaries/1.35.0-x86_64-linux-musl/busybox -O $LOWER/busybox
    chmod +x $LOWER/busybox
    mount -t overlay overlay -o lowerdir=$LOWER,upperdir=$UPPER,workdir=$WORK $MERGED
    chroot $MERGED /busybox sh
    umount $MERGED  # 清理
    
    此脚本创建 ~5MB 最小文件系统,远小于 Ubuntu 基础镜像的 100MB+。

监控:df -h merged 检查空间;生产中,设置 upperdir 在 tmpfs(内存)以加速 I/O,但限 1GB 大小避免 OOM。

整合与测试

将三者整合:先创建 OverlayFS 根,然后 unshare 命名空间,最后 cgexec 限资源。完整命令:cgexec -g cpu,memory:mycontainer unshare -p -m -f -r chroot merged /bin/busybox sh。

测试清单:

  1. 验证隔离:容器内 ifconfig 无宿主机网络。
  2. 资源测试:stress --cpu 4,观察 cgroup 限额生效(htop 检查)。
  3. 文件变更:写入文件,umount 后检查 upper 持久化。

此实现揭示 Docker 核心:~800 行代码模拟 runc。局限:无网络/用户命名空间,扩展需 clone(NEWNET) 等。实际部署,优先 Kubernetes,但理解底层优化镜像大小(目标 <50MB)和安全性(SELinux 集成)。

通过这些参数,开发者可自定义容器运行时,适用于边缘计算或嵌入式场景。未来,eBPF 可进一步增强监控。

(字数:1024)