Docker 背后的秘密:Namespace、CGroup 和 UnionFS 是怎么工作的?


Docker 背后的秘密:Namespace、CGroup 和 UnionFS 是怎么工作的?

用了这么久的 Docker,你可能会好奇:它到底是怎么做到“隔离”和“轻量”的?为什么一个容器看起来像一台独立的机器,却又能和宿主机共享内核?今天咱们就来揭开 Docker 的实现原理,聊聊 Linux 底层的三大核心技术:Namespace(命名空间)Control Group(控制组)UnionFS(联合文件系统)。懂了这些,你对容器的理解会上一个新台阶。

Namespace:让进程“看”不到外面

想象一下,你在一间屋子里,透过窗户只能看到院子里的景色,却看不到外面的街道。这就是 Namespace 的作用——它让容器里的进程只能“看到”一部分系统资源,仿佛自己独占了一台机器。

Linux 内核提供了多种 Namespace,每种负责隔离一类资源:

  • PID Namespace:隔离进程 ID。容器里的进程看到的 PID 是从 1 开始的(比如 init 进程),但实际上它在宿主机上可能是一个很大的 PID。
  • Network Namespace:隔离网络设备、IP 地址、端口等。每个容器有自己的虚拟网卡、路由表,所以可以拥有独立的 IP 和端口空间。
  • Mount Namespace:隔离文件系统挂载点。容器只能看到自己的文件系统,看不到宿主机的其他目录。
  • UTS Namespace:隔离主机名和域名。容器可以有自己的 hostname。
  • IPC Namespace:隔离进程间通信资源,比如消息队列。
  • User Namespace:隔离用户和用户组。容器里的 root 不一定等同于宿主机的 root,增强了安全性。

当 Docker 启动一个容器时,它会为这个容器创建一组新的 Namespace,然后让容器内的进程加入这些 Namespace。这样一来,进程就以为自己在独立的世界里运行,和其他容器、宿主机互不干扰。

Control Group:给容器戴上“紧箍咒”

有了 Namespace,容器之间互不看见了,但如果某个容器疯狂占用 CPU、内存,会不会把宿主机搞垮?会的。所以我们需要限制容器的资源使用,这就是 Control Group(cgroups) 的工作。

cgroups 可以限制、记录、隔离进程组使用的物理资源(CPU、内存、磁盘 I/O、网络带宽等)。Docker 在启动容器时,会为容器创建对应的 cgroup,并设置限制参数。比如你可以通过 --cpus=1.5 限制容器最多使用 1.5 个 CPU 核心,通过 --memory=512m 限制内存最多 512MB。

一旦容器内的进程试图超限使用,cgroups 就会出手干预,比如限制 CPU 时间、触发 OOM(内存溢出)杀死进程。这样就能保证宿主机和其他容器的稳定性。

UnionFS:镜像分层的魔法

我们之前聊过 Dockerfile 的每一行指令都会生成一个镜像层,最终叠加成一个完整的镜像。这个“叠加”靠的就是 UnionFS(联合文件系统),它允许将多个目录(或文件系统)挂载到同一个挂载点,看起来就像是一个目录。

Docker 常用的 UnionFS 实现有 AUFS、overlay2、devicemapper 等。以 overlay2 为例,它把镜像层(只读层)和容器层(读写层)叠加在一起:

  • 镜像层:只读,多个镜像可以共享相同的层,节省磁盘空间。
  • 容器层:可读写,容器运行时产生的文件修改都写在这一层。当你删除容器时,这一层也被丢弃,但镜像层依然保留。

这就是为什么从同一个镜像启动多个容器,它们各自有独立的可写层,互不影响。而当你提交一个新镜像(docker commit)时,就是把当前容器的可写层保存为一个新的镜像层。

数据卷:绕过 UnionFS 的持久化

容器层是临时的,容器删除后数据就没了。那需要持久化的数据怎么办?Docker 提供了 数据卷(Volume) 的概念。数据卷是直接挂载到容器内的宿主机目录(或专用存储),它绕过 UnionFS,不受容器生命周期影响。即使容器删除,数据卷里的数据依然存在。

你可以把数据卷想象成一条管道,直接连接到宿主机硬盘的某个位置,容器读写那里就像读写本地文件,但数据实际在宿主机上。这就是为什么挂载数据卷能实现数据持久化。

镜像层的具体结构

我们来看一个实际例子:假设你的 Dockerfile 有三条指令:FROM ubuntu、RUN apt-get update、COPY app /app。构建时会生成三个镜像层:

  • 第一层:ubuntu 基础镜像层
  • 第二层:apt-get update 产生的层(包含更新的包索引)
  • 第三层:COPY 产生的层(包含 app 文件)

当你运行容器时,Docker 会在这些只读层之上添加一个可写层(容器层)。如果你在容器里修改了 /app 下的文件,修改会写入容器层,下面的镜像层保持不变(通过写时复制技术)。这样既节省空间,又保持了镜像的不可变性。

把这些串起来:Docker 的完整工作流程

  • docker build:读取 Dockerfile,每一条指令生成一个镜像层,最终构建成一个镜像。
  • docker run:基于镜像创建容器,为容器创建新的 Namespace 和 cgroups,挂载 UnionFS(镜像层+容器层),然后启动容器内的进程。
  • 如果指定了数据卷,Docker 会在宿主机创建(或使用已有)目录,并挂载到容器的指定路径,绕开 UnionFS。
  • 容器运行时,进程通过 Namespace 隔离资源,通过 cgroups 限制资源使用,通过 UnionFS 实现文件系统分层。

小结

现在你应该明白,Docker 并不是什么黑科技,而是巧妙组合了 Linux 内核已有的特性:

  • Namespace 实现隔离;
  • Control Group 实现限制;
  • UnionFS 实现镜像分层和存储。

理解了这些,你就能更好地解释为什么容器是轻量的(共享内核)、为什么镜像可以分层复用、为什么数据卷能持久化。这些都是 Docker 的基石。

声明:麋鹿与鲸鱼|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - Docker 背后的秘密:Namespace、CGroup 和 UnionFS 是怎么工作的?


Carpe Diem and Do what I like