Docker 镜像黑科技:OverlayFS 联合文件系统详解
大家好!用过 Docker 的朋友都知道,镜像是分层的,容器启动快,还能节省大量磁盘空间。这背后离不开一项关键技术——联合文件系统(Union Filesystem)。今天,我们就来深入聊聊 Docker 目前默认且推荐使用的联合文件系统:OverlayFS(特别是 Overlay2),看看它是如何巧妙地实现这一切的。
一、OverlayFS 是什么?—— 文件系统的“堆叠”艺术
想象一下,你有几张透明的塑料画板(图层),你在最早的几张上画了基础背景(比如操作系统底层),这些画好后就固定了,不能再改(只读层)。然后,你拿了一张新的透明画板叠在最上面,你在这张新的画板上继续创作(读写层)。最后,当你从上往下看时,所有画板的内容会合并在一起,形成一幅完整的画作(统一视图)。
OverlayFS 就是这样一种“堆叠”式的文件系统。它本身不直接管理磁盘块,而是依赖于宿主机上已有的文件系统(如 ext4 或 xfs)。它的核心能力在于,能将不同目录(代表不同的层)里的文件和目录虚拟地合并(union mount)到同一个目录下,让用户看起来它们好像本来就在一个地方。
关键点:
- 联合挂载(Union Mount): 这是 OverlayFS 的核心机制,把多个目录(层)的内容“合并”展示。
- Overlay vs Overlay2: Linux 内核提供了
overlay和overlay2两种驱动。overlay2是overlay的改进版,特别是在 inode(文件系统用于索引节点的资源)利用率上更高效,是当前 Docker 的默认和推荐选项。相比更早期的 AUFS,OverlayFS 通常速度更快,实现也更简单。 - 环境要求(使用 Overlay2 的前提):
- Docker 版本建议 17.06.02 或更高。
- 宿主机的文件系统格式需要是
ext4或xfs(现在大多数 Linux 发行版默认都是)。
二、OverlayFS 的三大核心组件 + 一个工作区
OverlayFS 的魔法主要通过以下几个目录(或目录组)协同工作来实现:
-
lowerdir(底层目录/只读层):- 包含一个或多个只读目录,用冒号
:分隔。这些目录对应着 Docker 镜像的基础层。比如,一个基于 Ubuntu 镜像构建的应用,lowerdir就包含了 Ubuntu 基础镜像的所有层。 - 在 Dockerfile 中,每一条
RUN,COPY,ADD等指令通常会创建一个新的镜像层,这些层最终会成为容器启动时的lowerdir的一部分。 - 特殊
init层 (Docker 特有): Docker 在lowerdir的最顶层(逻辑上)还会悄悄加入一个特殊的init层。这个层主要用来存放容器启动时需要修改、但又不希望被持久化到镜像里的文件,比如/etc/hostname,/etc/hosts,/etc/resolv.conf。对这些文件的修改只在当前容器运行时有效,执行docker commit时不会包含init层的改动。这个init层的文件通常位于/var/lib/docker/overlay2/<init_id>/diff目录。
- 包含一个或多个只读目录,用冒号
-
upperdir(上层目录/读写层):- 只有一个目录,它是可读写的。
- 当容器启动时,Docker 会为容器创建一个空的
upperdir。所有对容器的修改操作(如新建文件、修改文件、删除文件)实际上都发生在这个upperdir中。例如,应用写入的日志文件、用户在容器内创建的临时文件等。 - 写时复制 (Copy-on-Write, CoW): 如果要修改一个存在于
lowerdir中的文件,OverlayFS 会先把这个文件复制到upperdir中,然后对upperdir中的副本进行修改。lowerdir中的原始文件保持不变。删除lowerdir中的文件,则会在upperdir中创建一个特殊的“删除标记”(whiteout 文件),告诉联合视图这个文件“消失”了。
-
workdir(工作目录):- 一个内部使用的空目录,OverlayFS 在执行某些操作(如 copy-up)时需要它作为临时工作空间。用户通常不需要关心这个目录的内容,并且挂载后其内容对用户是不可见的。这个目录必须和
upperdir在同一个文件系统上。
- 一个内部使用的空目录,OverlayFS 在执行某些操作(如 copy-up)时需要它作为临时工作空间。用户通常不需要关心这个目录的内容,并且挂载后其内容对用户是不可见的。这个目录必须和
-
merged(合并目录/统一视图):- 这是最终呈现给用户的挂载点。它将
lowerdir和upperdir的内容合并,提供一个统一的文件系统视图。 - 用户(或容器内的进程)通过访问
merged目录来与容器的文件系统交互。读取文件时,如果文件同时存在于upperdir和lowerdir,则优先读取upperdir中的版本。写入或删除文件时,操作实际发生在upperdir。
- 这是最终呈现给用户的挂载点。它将
三、OverlayFS 实战演练:手动模拟挂载
为了更直观地理解,我们来手动模拟一下 OverlayFS 的挂载过程:
1. 创建工作目录:
我们需要模拟多个 lower 层、一个 upper 层、一个 work 目录和一个最终的 merged 挂载点。
# 创建目录结构,{0..2} 表示创建 lower0, lower1, lower2
mkdir -pv /oldboyedu2024/lower{0..2} /oldboyedu2024/{upper,work,merged}
# 注意:你的笔记里 upper 目录名拼写为 uppper,这里统一用 upper
2. 挂载 OverlayFS:
使用 mount 命令,类型指定为 overlay,并通过 -o 选项指定各个目录。
# lowerdir 用冒号分隔多个只读层,顺序很重要(越靠后的层优先级越高,但通常Docker层是从基础到上层)
# 这里为了演示,我们把 lower0 作为最底层
sudo mount -t overlay overlay \
-o lowerdir=/oldboyedu2024/lower0:/oldboyedu2024/lower1:/oldboyedu2024/lower2,upperdir=/oldboyedu2024/upper,workdir=/oldboyedu2024/work \
/oldboyedu2024/merged/
3. 查看挂载信息:
df -h -t overlay
# 输出应该会显示 overlay 文件系统挂载在了 /oldboyedu2024/merged
# Filesystem Size Used Avail Use% Mounted on
# overlay ... ... ... ... /oldboyedu2024/merged
4. 准备初始数据 (模拟镜像层和容器修改):
# 在 lower 层放入文件 (模拟只读镜像层)
sudo cp /etc/hosts /oldboyedu2024/lower0/
sudo cp /etc/issue /oldboyedu2024/lower1/
sudo cp /etc/resolv.conf /oldboyedu2024/lower2/
# 在 upper 层放入文件 (模拟容器启动后已有的修改)
sudo cp /etc/hostname /oldboyedu2024/upper/
# 查看 merged 目录,你会发现所有层的文件都“合并”出现了!
ls -l /oldboyedu2024/merged/
# 应能看到 hosts, issue, resolv.conf, hostname
5. 在 merged 目录写入数据 (模拟容器内操作):
我们在统一视图 merged 里创建一个新文件,观察它实际出现在哪里。
# 在 merged 目录创建一个文件
sudo cp /etc/fstab /oldboyedu2024/merged/
# 查看 upper 目录,新文件 fstab 出现在了这里!
ls -l /oldboyedu2024/upper/
# 应能看到 hostname 和 fstab
# 查看 lower 各层,它们保持不变,没有 fstab
ls -l /oldboyedu2024/lower0/
ls -l /oldboyedu2024/lower1/
ls -l /oldboyedu2024/lower2/
这清晰地展示了写操作只影响 upperdir。
6. 测试只读挂载:
如果挂载时不提供 upperdir,会发生什么?
# 先卸载之前的挂载
sudo umount /oldboyedu2024/merged
# 重新挂载,这次 *不* 指定 upperdir
sudo mount -t overlay overlay \
-o lowerdir=/oldboyedu2024/lower0:/oldboyedu2024/lower1:/oldboyedu2024/lower2,workdir=/oldboyedu2024/work \
/oldboyedu2024/merged/
# 再次尝试在 merged 写入数据
sudo cp /etc/os-release /oldboyedu2024/merged/
# 这次会失败!输出: cp: cannot create regular file '/oldboyedu2024/merged/os-release': Read-only file system
没有了 upperdir 这个读写层,整个联合视图 merged 就变成了只读的。
(清理环境)
sudo umount /oldboyedu2024/merged # 确保卸载
# rm -rf /oldboyedu2024 # 如果需要,删除测试目录
四、Docker 是如何运用 OverlayFS 的?
现在我们知道了 OverlayFS 的原理,来看看 Docker 实际应用:
1. 容器启动,OverlayFS 现身:
当你运行一个 Docker 容器时,Docker 的存储驱动(如果是 overlay2)就会自动为这个容器创建一个 OverlayFS 挂载。
# 运行一个简单的容器
docker run -d --name c1 registry.cn-hangzhou.aliyuncs.com/yinzhengjie-k8s/apps:v1
# 再次查看 overlay 挂载点
df -h -t overlay
# 你会看到除了刚才手动的挂载(如果没卸载),还多了一个 Docker 管理的挂载点
# Filesystem Size Used Avail Use% Mounted on
# ...
# overlay ... ... ... ... /var/lib/docker/overlay2/......long_id....../merged
2. 探查容器的存储层:
使用 docker inspect 可以精确找到容器对应的 OverlayFS 各层目录。
# 获取容器 c1 的 MergedDir (统一视图挂载点)
docker inspect -f '{{.GraphDriver.Data.MergedDir}}' c1
# 输出: /var/lib/docker/overlay2/......long_id....../merged
# 获取容器 c1 的 UpperDir (读写层)
# 注意:Docker 内部通常将 UpperDir 对应的目录命名为 diff
docker inspect -f '{{.GraphDriver.Data.UpperDir}}' c1
# 输出: /var/lib/docker/overlay2/......long_id....../diff
# 获取容器 c1 的 LowerDir (只读层,可能很长,包含多个镜像层路径)
docker inspect -f '{{.GraphDriver.Data.LowerDir}}' c1
# 输出: /var/lib/docker/overlay2/l/...,/var/lib/docker/overlay2/l/..., ...
# 获取容器 c1 的 WorkDir (工作目录)
docker inspect -f '{{.GraphDriver.Data.WorkDir}}' c1
# 输出: /var/lib/docker/overlay2/......long_id....../work
3. 容器内写入,UpperDir 变化:
我们在容器内创建一个文件,然后去宿主机上对应的 UpperDir (即 diff 目录) 查看。
# 在容器 c1 内部创建一个文件
docker exec c1 touch /haha.log
# 去宿主机上查看容器对应的 UpperDir (diff 目录)
ls /var/lib/docker/overlay2/$(docker inspect -f '{{.GraphDriver.Name}}' c1)/$(docker inspect -f '{{.GraphDriver.Data.UpperDir}}' c1 | cut -d '/' -f 5-)/
# 或者直接用上面 inspect 得到的完整路径
ls /var/lib/docker/overlay2/......long_id....../diff
# 输出中应该能看到刚刚创建的 haha.log 文件!
# 同时,查看 MergedDir,也能看到 haha.log
ls /var/lib/docker/overlay2/......long_id....../merged
# 输出中也应该有 haha.log
4. MergedDir 就是容器的根目录视图:
我们可以再次使用 chroot (还记得上一篇的内容吗?)进入容器的 MergedDir,体验一下容器内部看到的文件系统。
# 使用 chroot 进入容器的 MergedDir,并启动一个 shell
sudo chroot $(docker inspect -f '{{.GraphDriver.Data.MergedDir}}' c1) /bin/sh
# 或者手动输入 MergedDir 完整路径
# 在 chroot 环境里执行 ls /
# / # ls /
# 你会看到容器根目录下的所有文件和目录,包括我们刚才创建的 haha.log
# 这就是容器进程看到的“真实”文件系统视图!
# 输入 exit 退出 chroot 环境
# / # exit
五、OverlayFS 对生产环境的意义
理解 OverlayFS 对于在生产环境中使用 Docker 非常重要:
- 高效的镜像存储: 多个容器如果基于同一个基础镜像,它们可以共享相同的
lowerdir只读层。磁盘上只需要存储一份基础镜像的数据,大大节省了存储空间。 - 快速的容器启动: 创建新容器时,Docker 只需创建新的
upperdir和workdir,然后将lowerdir(已存在)和upperdir联合挂载即可,这个过程非常快。 - 写时复制 (CoW): 保证了镜像层的不可变性,容器的修改都隔离在自己的
upperdir中,不会影响基础镜像或其他容器。 - 资源利用: Overlay2 驱动对 inode 的使用进行了优化,减少了早期 Overlay 驱动可能遇到的 inode 耗尽问题。
- 性能: OverlayFS 通常被认为是性能较好的存储驱动之一,对大多数应用场景都能提供良好的 I/O 性能。
生产环境注意事项:
- 备份: 容器的可写层 (
upperdir) 包含了容器运行时的所有修改。如果需要持久化容器数据,强烈推荐使用 Docker Volumes 而不是依赖upperdir。Volumes 独立于容器生命周期,更易于管理和备份。upperdir的数据通常被认为是临时的或状态性的。 - Inode 监控: 虽然 Overlay2 改进了 inode 使用,但在极特殊的场景下(例如,容器内创建了海量的小文件),仍然需要关注宿主机文件系统的 inode 使用情况。
- 底层文件系统: 确保宿主机使用推荐的
ext4或xfs文件系统。
结论
OverlayFS (特别是 Overlay2) 是 Docker 实现高效镜像分层和快速容器部署的关键技术。通过巧妙地“堆叠”只读层 (lowerdir) 和读写层 (upperdir),并利用写时复制机制,它既保证了镜像共享和存储效率,又实现了容器间的文件系统隔离。理解 OverlayFS 的工作原理,有助于我们更好地管理 Docker 镜像和容器,优化资源使用,并在生产环境中做出更明智的技术决策。
希望这篇深入浅出的解析能帮助大家彻底搞懂 Docker 背后的这项存储黑科技!
浙公网安备 33010602011771号