1900 字
10 分钟
容器的文件系统
2026-01-06 13:45:10

1. 容器的核心需求与文件系统挑战#

在设计容器文件系统时,必须解决两个看似矛盾的需求:

  1. 效率与共享: 一个镜像(如 ubuntu:latest)可能被成千上万个容器使用。如果每个容器都复制一份完整的根文件系统,将造成巨大的存储浪费和启动延迟。
  2. 隔离与写入: 每个容器都需要一个独立的、可写的文件系统空间,容器内的进程对文件的增、删、改操作不能影响其他容器或宿主机,也不能影响镜像本身。

传统的虚拟机会为每个实例分配一个完整的磁盘镜像,这解决了隔离问题,但牺牲了效率和共享。容器的解决方案非常巧妙:分层镜像与联合挂载


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 逻辑。以下是一些主流驱动:

  1. OverlayFS(现代最常用):

    • 原理:使用 Linux 内核的 overlayoverlay2 驱动。
    • 结构
      • lowerdir(s): 镜像的只读层。可以有多个。
      • upperdir: 容器的可写层。
      • merged: 最终的统一视图目录,即容器的根文件系统 /
      • workdir: OverlayFS 内部的工作目录。
    • overlay2overlay 的改进版,原生支持多达128个下层目录,性能更好。
  2. AUFS(早期驱动):

    • 在 OverlayFS 被合并进内核前,Docker 主要使用 AUFS。原理类似,但非内核主线,需要额外安装。
  3. Device Mapper

    • 在 CentOS/RHEL 等系统上,早期使用。它在块设备级别工作,为每个容器分配一个稀疏的块设备文件(thin-provisioning),实现 CoW,性能开销较大。
  4. Btrfs / ZFS

    • 利用其原生快照和克隆特性来实现分层。它们也在块设备级别工作,性能特点鲜明。
  5. containerd的Stargz / eStargz

    • 为优化镜像拉取和懒加载设计。可以按需拉取文件的某个“范围”,而不是整个层,特别适合大型镜像。

4. 为什么这样设计?(设计哲学与优势)#

  1. 极高的存储效率与分发速度

    • 共享基础层: 100个基于 alpine 的容器,在宿主机上只存储一份 alpine 基础层。拉取新镜像时,如果本地已存在某些层,则无需重复下载。
    • 分层缓存: Dockerfile 构建时,每一层都被缓存。修改 Dockerfile 后面的指令,前面的层可以直接复用缓存,极大加速构建。
  2. 快速的容器启动

    • 启动容器无需复制整个镜像,只需要创建一个微小的可写层并挂载联合视图,这个过程几乎是瞬时的。
  3. 保证镜像的不可变性

    • 镜像是只读的。这保证了环境的一致性:无论在何处运行,从同一个镜像启动的容器,其初始文件状态是完全相同的。这是“基础设施即代码”和可重复部署的基石。
  4. 高效的隔离机制

    • CoW 机制确保了容器间的写入隔离。一个容器对共享文件的修改,不会影响到其他容器,因为修改只存在于它自己的可写层中。
  5. 支持多种存储后端和高级特性

    • 通过存储驱动抽象,可以适配不同的宿主机文件系统(ext4, xfs, btrfs, zfs等)。
    • 基于此模型,可以方便地实现数据卷。数据卷是绕过 UnionFS 的可写层,直接挂载到容器的宿主机目录或网络存储。这使得数据的生命周期独立于容器,实现了数据持久化和高性能 IO。

5. 总结与比喻#

一个生动的比喻: 把镜像层想象成一张张透明的幻灯片。基础镜像(如 Ubuntu)是第一张幻灯片,Dockerfile 的每条指令都在前一张幻灯片上贴一些新的贴纸(增加、修改文件),形成新的幻灯片。

  • 镜像:就是这一叠幻灯片本身。
  • 启动容器:就是把这叠幻灯片(只读)放到一个投影仪下,然后在最上面盖一张完全空白的透明胶片(可写层)。
  • 容器内操作:所有修改都画在这张空白胶片上。如果要在下面某张幻灯片已有的图案上修改,就把那个图案临摹到空白胶片上再改。
  • 最终视图:从投影仪看下去(merged 视图),你看到的是所有幻灯片和顶层空白胶片叠加后的完整画面。
  • 删除容器:就是扔掉最上面那张空白胶片,下面的幻灯片(镜像)完好无损,可以继续用来启动新的容器。

这种设计的核心精髓在于:通过“写时复制”和“联合挂载”,在提供隔离性的同时,最大限度地实现了资源共享和效率提升,从而完美支撑了容器轻量、快速、一致的核心特性。

Comment seems to stuck. Try to refresh?✨