文章目录
- 🐳 Docker 入门教程(六):联合文件系统(UnionFS)
- 一、联合文件系统(UnionFS)
- 二、Docker 镜像的层级结构
- 三、写层(Copy-on-Write)
- 四、镜像构建缓存机制 = 层级缓存机制
- 五、从镜像到容器,再到新镜像的完整闭环
🐳 Docker 入门教程(六):联合文件系统(UnionFS)
一、联合文件系统(UnionFS)
联合文件系统(Union File System,简称 UnionFS)是一种支持将多个只读层合并成一个文件系统视图的文件系统技术。Docker 正是通过它,实现了镜像分层构建与容器写层隔离。
换句话说,UnionFS 允许我们把多个只读目录“叠在一起”,让用户看到的是“一个统一的文件系统”。
它是镜像、容器分离的根本支撑。
二、Docker 镜像的层级结构
Docker 镜像并不是一个打包好的压缩文件,而是由多个只读层(layer)按顺序叠加而成的。
每一层都只包含自上一次变更以来的“文件系统差异”(增删改文件),例如:
FROM ubuntu # 第1层:基础镜像层
RUN apt update # 第2层:系统更新命令
RUN apt install nginx # 第3层:安装 nginx
COPY . /app # 第4层:拷贝项目代码
Docker 会为每一条指令生成一层(layer),每层是只读的,并存储为 SHA256 哈希命名的目录。
镜像分层的意义
- 高效复用:多个镜像可以共享相同的底层层(如基础镜像层)
- 按需下载:拉取镜像时会分层下载,避免重复传输
- 构建缓存:如果上一层没变化,Docker 会重用缓存而非重建镜像层
- 只读安全:镜像不能被容器改写,天然具备版本一致性
当你运行一个容器时,Docker 会将镜像层挂载到容器中,并在最上层添加一个“可写层”:
容器文件系统结构:[ 写层 ] ← 运行时动态生成,容器唯一
[ 镜像层 4 ] ← COPY 指令
[ 镜像层 3 ] ← 安装依赖
[ 镜像层 2 ] ← 环境配置
[ 镜像层 1 ] ← 基础系统
这个结构就是典型的 UnionFS 挂载叠加形式,最上面一层是写层,其它都是只读的。
三、写层(Copy-on-Write)
容器在运行时的所有写操作(包括新增文件、修改配置、生成日志)都写入最上方的写层。
镜像层是只读的,操作都写入写层——或者叫做容器层?
而对文件的读取操作,则从顶层依次向下查找第一个匹配项。
文件删除/修改时的行为(非常重要)
- 删除文件:其实不会真的从下层删除,而是在写层记录“白名单”(whiteout)屏蔽该文件。
- 修改文件:先把下层文件复制到写层,再修改(copy-on-write)
因此,镜像层始终保持不变,容器之间互不影响。
*联合文件系统的技术支持
Docker 支持多种联合文件系统驱动(取决于操作系统):
驱动类型 | 系统支持 | 特点 |
---|---|---|
overlay2 | 推荐默认(现代 Linux) | 高性能,内核直接支持 |
aufs | Ubuntu 较老版本 | 最早使用,已过时 |
btrfs / zfs | 可选高级驱动 | 支持快照、更复杂的挂载 |
你可以通过如下命令查看当前使用的存储驱动:
docker info | grep Storage
四、镜像构建缓存机制 = 层级缓存机制
在你执行 docker build
时,每一条 Dockerfile 指令生成的层会被缓存(只要内容没有变化)。
Docker 会根据上下文(比如文件 hash)决定是否使用缓存。
这意味着:
- 如果你在 Dockerfile 的前几层频繁改动,会导致所有后续层都重新构建
- 所以我们会说:“尽量把不变的层写在前面”
容器生命周期下的数据命运
操作 | 写层会怎样? | 镜像层是否保留? |
---|---|---|
容器运行 | 写层存在 | 镜像只读保留 |
容器停止 | 写层仍在 | 镜像不变 |
容器删除 | 写层随容器删除 | 镜像不变 |
镜像被删除 | 镜像层被移除(如果未被其它容器使用) | 容器无法重新启动 |
UnionFS 的局限与演进
- 写层性能相对较低(尤其是随机写入时)
- 对高频 IO 的容器(数据库)建议使用挂载卷(Volume)代替
- 容器中的数据默认是临时的、不持久的
这就是为什么你运行完一个容器后,重启发现数据全没了 —— 因为写层随容器消失了。
五、从镜像到容器,再到新镜像的完整闭环
在理解 UnionFS 分层结构之后,我们可以进一步掌握 Docker 最核心的使用流程:镜像 → 容器 → 新镜像 → 分享。
这个过程遵循如下原则:
-
镜像层始终只读:任何
docker pull
拉下来的镜像都是不可变的,它就像一个快照模板,不会因为运行或改动而被修改。 -
容器运行时增加一个可写层:当我们执行
docker run
时,Docker 会在镜像顶部叠加一个写层(Writable Layer),所有运行时的变动(文件操作、配置更改等)都记录在这一层中。 -
容器运行中的更改,仅影响写层:即使你在容器中删除、修改了镜像文件,实际上只是对写层进行“遮盖”或“拷贝修改”,镜像层依然保持原样不动。
-
通过提交或构建形成新镜像:一旦你在容器中完成了配置或开发,可以使用如下方式生成新的镜像:
docker commit 容器ID 新镜像名
:将当前容器快照为新的镜像层docker build
:从 Dockerfile 定义新的镜像构建流程
-
新镜像仍由只读层组成:无论你是 commit 还是 build,最终产生的镜像都是只读层的组合,底层机制依旧是 UnionFS。
-
镜像可分发、复用:将新镜像
docker push
到远程仓库后,其他人可以docker pull
下来,并基于你提交的状态继续运行容器、做进一步更改。
工作流图示(逻辑流程):
[ 原始镜像层 ] ← docker pull↓+------------------+| 容器写层(可写) | ← docker run(运行时修改)+------------------+↓[ 新镜像层 ] ← docker commit / build↓推送到远程仓库 ← docker push↓其他人拉取并运行 ← docker pull + run(再加写层)
示例说明
你运行一个 Python 镜像,安装 Flask,然后发布成镜像:
docker pull python:3.10
docker run -it python:3.10 # 安装 flask...
docker commit 容器ID my-python:flask
docker push my-python:flask
其他人:
docker pull my-python:flask
docker run -it my-python:flask # 在这个基础上继续开发
这就形成了一个典型的“从模板 → 修改 → 发布新模板 → 再次复用”的开发链路。
这一机制正是 Docker 能够高效实现镜像复用、层级缓存、团队协作和 CI/CD 自动化的核心。
镜像层只读、可复用;容器层临时、可变动;新镜像则是将变动固化为新只读层,构成可传播的构建快照。
这一设计体现了 Docker 对文件系统和资源管理的高度抽象能力,是其轻量、高效、模块化的关键所在。
各级开发人员、运维人员、CI 系统可以在同一个基础镜像之上,逐层构建、逐层定制、逐层复用,实现真正的模块化构建流程。