1900 字
10 分钟
容器的文件系统
1. 容器的核心需求与文件系统挑战
在设计容器文件系统时,必须解决两个看似矛盾的需求:
- 效率与共享: 一个镜像(如
ubuntu:latest)可能被成千上万个容器使用。如果每个容器都复制一份完整的根文件系统,将造成巨大的存储浪费和启动延迟。 - 隔离与写入: 每个容器都需要一个独立的、可写的文件系统空间,容器内的进程对文件的增、删、改操作不能影响其他容器或宿主机,也不能影响镜像本身。
传统的虚拟机会为每个实例分配一个完整的磁盘镜像,这解决了隔离问题,但牺牲了效率和共享。容器的解决方案非常巧妙:分层镜像与联合挂载。
2. 实现原理:分层与联合
2.1. 镜像的分层结构
一个容器镜像并非一个单一的大文件,而是由一系列只读层 叠加而成。每一层代表文件系统的一组差异(diff),包含了相对于其底层发生变化的一组文件。
- 基础层:通常是操作系统层(如 Alpine Linux、Ubuntu 的最小化版本)。
- 增量层:每执行一条 Dockerfile 指令(如
RUN apt-get install,COPY,ENV),就会在现有层之上创建一个新的只读层。 - 结果:当你拉取一个镜像时,实际上是在拉取这些层的集合。所有容器共享这些相同的只读层。
2.2. 容器的可写层
当从一个镜像启动一个容器时,容器引擎会在所有只读层之上,为这个容器创建一个新的、薄薄的可写层(也称为“容器层”或“copy-on-write层”)。
这个可写层是容器隔离性的关键:
- 容器内所有文件的修改、创建、删除都发生在这个层。
- 这个层是容器私有的,其他容器不可见。
- 当容器被删除时,这个可写层通常也会被删除(除非数据被持久化)。
2.3. 联合文件系统
如何将多个分层(一堆只读层 + 一个可写层)呈现为一个统一的单一目录视图给容器内的进程?答案是 联合文件系统。
联合文件系统 是一种特殊的文件系统,它可以将多个不同位置的目录(分支)“透明地”合并成一个统一的目录。在容器场景中,它遵循以下规则:
- 读操作:当容器进程读取一个文件时,UnionFS 会从顶层(可写层)开始向下逐层查找,找到的第一个版本就是返回给进程的版本。
- 写操作:
- 修改已有文件:UnionFS 使用 写时复制 策略。当容器试图修改一个只读层中的文件时,UnionFS 会先将这个文件复制到可写层,然后所有的修改都作用于可写层中的这个副本。从此,容器访问该文件时,看到的是可写层中的副本(因为查找顺序自上而下)。
- 创建新文件:直接在可写层创建。
- 删除文件:在可写层创建一个特殊的“白障”文件或标记,隐藏下层中的同名文件,使其看起来像被删除了。
3. 具体技术实现(存储驱动)
容器引擎(如 Docker)通过不同的 存储驱动 来实现上述 UnionFS 逻辑。以下是一些主流驱动:
-
OverlayFS(现代最常用):
- 原理:使用 Linux 内核的
overlay和overlay2驱动。 - 结构:
- lowerdir(s): 镜像的只读层。可以有多个。
- upperdir: 容器的可写层。
- merged: 最终的统一视图目录,即容器的根文件系统
/。 - workdir: OverlayFS 内部的工作目录。
overlay2是overlay的改进版,原生支持多达128个下层目录,性能更好。
- 原理:使用 Linux 内核的
-
AUFS(早期驱动):
- 在 OverlayFS 被合并进内核前,Docker 主要使用 AUFS。原理类似,但非内核主线,需要额外安装。
-
Device Mapper:
- 在 CentOS/RHEL 等系统上,早期使用。它在块设备级别工作,为每个容器分配一个稀疏的块设备文件(thin-provisioning),实现 CoW,性能开销较大。
-
Btrfs / ZFS:
- 利用其原生快照和克隆特性来实现分层。它们也在块设备级别工作,性能特点鲜明。
-
containerd的Stargz / eStargz:
- 为优化镜像拉取和懒加载设计。可以按需拉取文件的某个“范围”,而不是整个层,特别适合大型镜像。
4. 为什么这样设计?(设计哲学与优势)
-
极高的存储效率与分发速度:
- 共享基础层: 100个基于
alpine的容器,在宿主机上只存储一份alpine基础层。拉取新镜像时,如果本地已存在某些层,则无需重复下载。 - 分层缓存: Dockerfile 构建时,每一层都被缓存。修改 Dockerfile 后面的指令,前面的层可以直接复用缓存,极大加速构建。
- 共享基础层: 100个基于
-
快速的容器启动:
- 启动容器无需复制整个镜像,只需要创建一个微小的可写层并挂载联合视图,这个过程几乎是瞬时的。
-
保证镜像的不可变性:
- 镜像是只读的。这保证了环境的一致性:无论在何处运行,从同一个镜像启动的容器,其初始文件状态是完全相同的。这是“基础设施即代码”和可重复部署的基石。
-
高效的隔离机制:
- CoW 机制确保了容器间的写入隔离。一个容器对共享文件的修改,不会影响到其他容器,因为修改只存在于它自己的可写层中。
-
支持多种存储后端和高级特性:
- 通过存储驱动抽象,可以适配不同的宿主机文件系统(ext4, xfs, btrfs, zfs等)。
- 基于此模型,可以方便地实现数据卷。数据卷是绕过 UnionFS 的可写层,直接挂载到容器的宿主机目录或网络存储。这使得数据的生命周期独立于容器,实现了数据持久化和高性能 IO。
5. 总结与比喻
一个生动的比喻: 把镜像层想象成一张张透明的幻灯片。基础镜像(如 Ubuntu)是第一张幻灯片,Dockerfile 的每条指令都在前一张幻灯片上贴一些新的贴纸(增加、修改文件),形成新的幻灯片。
- 镜像:就是这一叠幻灯片本身。
- 启动容器:就是把这叠幻灯片(只读)放到一个投影仪下,然后在最上面盖一张完全空白的透明胶片(可写层)。
- 容器内操作:所有修改都画在这张空白胶片上。如果要在下面某张幻灯片已有的图案上修改,就把那个图案临摹到空白胶片上再改。
- 最终视图:从投影仪看下去(
merged视图),你看到的是所有幻灯片和顶层空白胶片叠加后的完整画面。 - 删除容器:就是扔掉最上面那张空白胶片,下面的幻灯片(镜像)完好无损,可以继续用来启动新的容器。
这种设计的核心精髓在于:通过“写时复制”和“联合挂载”,在提供隔离性的同时,最大限度地实现了资源共享和效率提升,从而完美支撑了容器轻量、快速、一致的核心特性。
Comment seems to stuck. Try to refresh?✨