深入理解 Docker 存储机制:OverlayFS 原理与镜像分层实现
Union 文件系统在容器技术中的核心作用
Union File System(联合文件系统)是一种将多个目录合并为单一视图的文件系统机制。它允许不同路径下的文件和目录以"叠加"方式呈现,形成一个统一的访问入口。这种设计特别适用于容器环境,其中镜像层需要被高效共享,而容器本身的修改则独立保存。
其核心特性之一是写时复制(Copy-on-Write, CoW)。当应用尝试修改一个来自只读层的文件时,并不会直接更改原始数据,而是先将该文件复制到可写层,再进行更新操作。这样既保证了底层镜像的不可变性,又实现了运行时的灵活性。
OverlayFS 的工作原理与实操演示
自 Linux 内核 3.18 版本起,OverlayFS 被正式纳入主线,逐渐成为主流存储驱动。它通过上下层结构实现文件系统的堆叠:
- lowerdir:底层目录,通常为只读,用于存放基础镜像内容;
- upperdir:上层目录,可读写,记录所有变更;
- workdir:工作目录,由内核使用,处理文件操作的中间状态;
- merged:最终对外暴露的联合视图。
实验环境搭建
创建如下目录结构:
mkdir lower upper work merged
echo "base data" > lower/file1
echo "original" > lower/common
echo "new in upper" > upper/file2
echo "modified" > upper/common
执行挂载命令:
sudo mount -t overlay overlay \
-o lowerdir=./lower,upperdir=./upper,workdir=./work \
./merged
此时查看 merged 目录内容:
$ ls merged/
file1 file2 common
$ cat merged/common
modified
可见同名文件被上层覆盖,新增文件也一并显示。
写入行为分析
若修改仅存在于 lower 中的文件:
echo "updated" >> merged/file1
虽然从 merged 视角能看到新内容,但检查 lower/file1 发现未变化。实际上,OverlayFS 自动执行了 copy-up 操作,将 file1 复制至 upper 并完成写入。
删除机制揭秘
当删除一个位于 lower 的文件时,例如:
rm merged/file1
底层文件并未真正移除,而是在 upper 中生成一个特殊设备文件——whiteout 文件,类型为字符设备(c),大小为 0。这会使得在联合视图中该文件"消失",实现逻辑删除。
新建文件流向
任何在 merged 中创建的新文件都会直接写入 upperdir。验证方法:
echo "hello" > merged/newfile
ls upper/ # 可见 newfile 出现在 upper 目录
Docker 如何利用 OverlayFS 构建容器文件系统
Docker 镜像由一系列只读层构成,每一层代表一次构建指令的变更。运行容器时,Docker 引擎会在这些镜像层之上添加一个可写层,形成完整的 rootfs。
镜像构建过程解析
以以下 Dockerfile 为例:
FROM ubuntu:20.04
RUN echo "Hello world" > /tmp/newfile
构建后使用 docker history 查看层级:
IMAGE CREATED CREATED BY SIZE
290d8cc1f75a 22 seconds ago /bin/sh -c echo "Hello world" > /tmp/newfile 12B
ba6acccedd29 3 months ago /bin/sh -c #(nop) CMD ["bash"] 0B
<missing> 3 months ago /bin/sh -c #(nop) ADD file:... 72.8MB
最顶层的 12 字节层即为 RUN 指令带来的变更。
存储目录结构剖析
Docker 默认将数据存放在 /var/lib/docker 下。启用 overlay2 驱动时,关键路径包括:
/var/lib/docker/image/overlay2/imagedb/content/sha256/:存储镜像元信息,包含每层的 diff ID;/var/lib/docker/image/overlay2/layerdb/sha256/:记录各层元数据,如 cache-id、size 等;/var/lib/docker/overlay2/<cache-id>:实际文件内容存储位置。
层标识计算机制
Docker 使用 ChainID 来唯一标识每个层及其依赖链:
- 第一层的 ChainID 等于其 DiffID;
- 后续层的 ChainID = SHA256(前一层 ChainID + " " + 当前层 DiffID)。
举例说明:
# Layer0 DiffID: sha256:9f54eef4...
# Layer1 DiffID: sha256:b3cce2ce...
ChainID(layer1)=$(echo -n "sha256:9f54eef4... sha256:b3cce2ce..." | sha256sum | awk '{print $1}')
由此生成的 ChainID 将对应 layerdb 中的具体目录,进而通过 cache-id 定位物理存储路径。
容器启动时的挂载流程
当运行容器时,Docker 执行类似如下挂载操作:
mount -t overlay overlay \
-o lowerdir=/layerN:/layerN-1:...:/layer0,upperdir=/container-upper,workdir=/work \
/container-rootfs
所有镜像层作为 lowerdir 以冒号分隔依次排列,容器专属的可写层作为 upperdir,最终挂载点即为容器内部的根文件系统。
总结
OverlayFS 通过分层机制与写时复制策略,使 Docker 能够高效管理镜像与容器文件系统。镜像层被多个容器共享,节省磁盘空间;容器的每次修改仅记录增量,提升性能并保障安全性。理解这一底层机制有助于优化镜像构建、排查存储问题以及深入掌握容器运行时行为。