Docker 镜像黑科技:OverlayFS 联合文件系统详解

大家好!用过 Docker 的朋友都知道,镜像是分层的,容器启动快,还能节省大量磁盘空间。这背后离不开一项关键技术——联合文件系统(Union Filesystem)。今天,我们就来深入聊聊 Docker 目前默认且推荐使用的联合文件系统:OverlayFS(特别是 Overlay2),看看它是如何巧妙地实现这一切的。

一、OverlayFS 是什么?—— 文件系统的“堆叠”艺术

想象一下,你有几张透明的塑料画板(图层),你在最早的几张上画了基础背景(比如操作系统底层),这些画好后就固定了,不能再改(只读层)。然后,你拿了一张新的透明画板叠在最上面,你在这张新的画板上继续创作(读写层)。最后,当你从上往下看时,所有画板的内容会合并在一起,形成一幅完整的画作(统一视图)。

OverlayFS 就是这样一种“堆叠”式的文件系统。它本身不直接管理磁盘块,而是依赖于宿主机上已有的文件系统(如 ext4xfs)。它的核心能力在于,能将不同目录(代表不同的层)里的文件和目录虚拟地合并(union mount)到同一个目录下,让用户看起来它们好像本来就在一个地方。

关键点:

  1. 联合挂载(Union Mount): 这是 OverlayFS 的核心机制,把多个目录(层)的内容“合并”展示。
  2. Overlay vs Overlay2: Linux 内核提供了 overlayoverlay2 两种驱动。overlay2overlay 的改进版,特别是在 inode(文件系统用于索引节点的资源)利用率上更高效,是当前 Docker 的默认和推荐选项。相比更早期的 AUFS,OverlayFS 通常速度更快,实现也更简单。
  3. 环境要求(使用 Overlay2 的前提):
    • Docker 版本建议 17.06.02 或更高。
    • 宿主机的文件系统格式需要是 ext4xfs(现在大多数 Linux 发行版默认都是)。

二、OverlayFS 的三大核心组件 + 一个工作区

OverlayFS 的魔法主要通过以下几个目录(或目录组)协同工作来实现:

  1. 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 目录。
  2. upperdir (上层目录/读写层):

    • 只有一个目录,它是可读写的。
    • 当容器启动时,Docker 会为容器创建一个空的 upperdir。所有对容器的修改操作(如新建文件、修改文件、删除文件)实际上都发生在这个 upperdir 中。例如,应用写入的日志文件、用户在容器内创建的临时文件等。
    • 写时复制 (Copy-on-Write, CoW): 如果要修改一个存在于 lowerdir 中的文件,OverlayFS 会先把这个文件复制upperdir 中,然后对 upperdir 中的副本进行修改。lowerdir 中的原始文件保持不变。删除 lowerdir 中的文件,则会在 upperdir 中创建一个特殊的“删除标记”(whiteout 文件),告诉联合视图这个文件“消失”了。
  3. workdir (工作目录):

    • 一个内部使用的空目录,OverlayFS 在执行某些操作(如 copy-up)时需要它作为临时工作空间。用户通常不需要关心这个目录的内容,并且挂载后其内容对用户是不可见的。这个目录必须和 upperdir 在同一个文件系统上。
  4. merged (合并目录/统一视图):

    • 这是最终呈现给用户的挂载点。它将 lowerdirupperdir 的内容合并,提供一个统一的文件系统视图。
    • 用户(或容器内的进程)通过访问 merged 目录来与容器的文件系统交互。读取文件时,如果文件同时存在于 upperdirlowerdir,则优先读取 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 只需创建新的 upperdirworkdir,然后将 lowerdir(已存在)和 upperdir 联合挂载即可,这个过程非常快。
  • 写时复制 (CoW): 保证了镜像层的不可变性,容器的修改都隔离在自己的 upperdir 中,不会影响基础镜像或其他容器。
  • 资源利用: Overlay2 驱动对 inode 的使用进行了优化,减少了早期 Overlay 驱动可能遇到的 inode 耗尽问题。
  • 性能: OverlayFS 通常被认为是性能较好的存储驱动之一,对大多数应用场景都能提供良好的 I/O 性能。

生产环境注意事项:

  • 备份: 容器的可写层 (upperdir) 包含了容器运行时的所有修改。如果需要持久化容器数据,强烈推荐使用 Docker Volumes 而不是依赖 upperdir。Volumes 独立于容器生命周期,更易于管理和备份。upperdir 的数据通常被认为是临时的或状态性的。
  • Inode 监控: 虽然 Overlay2 改进了 inode 使用,但在极特殊的场景下(例如,容器内创建了海量的小文件),仍然需要关注宿主机文件系统的 inode 使用情况。
  • 底层文件系统: 确保宿主机使用推荐的 ext4xfs 文件系统。

结论

OverlayFS (特别是 Overlay2) 是 Docker 实现高效镜像分层和快速容器部署的关键技术。通过巧妙地“堆叠”只读层 (lowerdir) 和读写层 (upperdir),并利用写时复制机制,它既保证了镜像共享和存储效率,又实现了容器间的文件系统隔离。理解 OverlayFS 的工作原理,有助于我们更好地管理 Docker 镜像和容器,优化资源使用,并在生产环境中做出更明智的技术决策。

希望这篇深入浅出的解析能帮助大家彻底搞懂 Docker 背后的这项存储黑科技!

posted on 2025-04-05 08:26  Leo_Yide  阅读(589)  评论(0)    收藏  举报