Docker 联合文件系统
核心定义 Docker 联合文件系统(Union File System,UnionFS)是一类将多个文件系统层按顺序叠加,并向上呈现为一个统一目录视图的机制。Docker 常用的
overlay2存储驱动基于 Linux OverlayFS 实现,用于支撑镜像分层、容器可写层和写时复制。
1. 为什么 Docker 需要联合文件系统
容器运行时需要同时满足三个目标:
- 镜像可复用:多个镜像可以共享相同基础层,例如
ubuntu、centos或公共运行时层。 - 容器可隔离:每个容器都能修改自己的文件系统,但不能破坏原始镜像层,也不能影响其他容器。
- 启动要足够快:启动容器时不应完整复制一份镜像文件系统,否则会浪费空间并拖慢启动速度。
联合文件系统通过“只读镜像层 + 容器可写层 + 统一合并视图”的方式解决这些问题。
镜像只读层:负责复用
容器可写层:负责隔离每个容器的变更
merged 视图:让容器进程看到完整的根文件系统
2. UnionFS 的核心特性
2.1. 分层存储
Docker 镜像不是一个完整的大文件系统副本,而是由多个只读层叠加而成。每条 Dockerfile 指令通常会产生一个新的镜像层。
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y openjdk-17-jre
COPY app.jar /app/app.jar
CMD ["java", "-jar", "/app/app.jar"]
上述构建可以理解为多层叠加:
基础系统层 ubuntu:22.04
↓
安装 JRE 产生的新层
↓
复制 app.jar 产生的新层
↓
设置启动命令产生的元数据层
分层的好处是:相同基础层可以被多个镜像共享,已经存在的层不需要重复下载、重复存储或重复构建。
2.2. 只读共享
镜像层在容器运行时保持只读。多个容器基于同一个镜像启动时,会共享这些只读层。
| 对象 | 是否可写 | 生命周期 | 作用 |
|---|---|---|---|
| 镜像层 | 否 | 随镜像存在 | 提供基础文件系统和应用文件 |
| 容器层 | 是 | 随容器存在 | 保存当前容器新增、修改、删除的文件 |
| 数据卷 | 是 | 独立于容器 | 保存需要持久化的数据 |
因此,启动 100 个相同镜像的容器,并不会复制 100 份完整镜像层。每个容器只额外拥有自己的可写层。
2.3. 写时复制
写时复制(Copy-On-Write,COW)的原则是:
COW 原则 文件不修改时共享只读镜像层;首次修改时,先将目标文件复制到容器可写层,再在副本上写入。
修改镜像层中已有文件时,流程如下:
- 容器进程尝试修改某个文件。
- 存储驱动发现该文件来自只读镜像层。
- 将该文件从只读层复制到当前容器的可写层。
- 后续修改发生在可写层副本上。
- 原镜像层保持不变,其他容器不受影响。
这也是容器“启动快、占用小、彼此隔离”的关键原因之一。
3. Docker 的 Overlay2 存储驱动
overlay2 是 Docker 在现代 Linux 系统上常用的存储驱动。它基于 OverlayFS,将多个下层目录和一个上层目录合并成一个统一视图。
3.1. lowerDir:镜像只读层
lowerDir 表示下层目录,通常对应镜像的只读层。它可以包含多个层,层与层之间按照顺序叠加。
特点:
- 只读,不允许容器直接修改。
- 可以被多个容器共享。
- 通常来自镜像构建或镜像拉取结果。
3.2. upperDir:容器可写层
upperDir 表示上层目录,是当前容器唯一可写的文件系统层。
容器运行期间发生的文件变更都会记录在这里:
- 新增文件:直接写入
upperDir。 - 修改文件:先从
lowerDir复制到upperDir,再修改。 - 删除文件:在
upperDir中写入 whiteout 标记,用于遮蔽下层文件。
3.3. mergedDir:容器看到的合并视图
mergedDir 是容器进程实际看到的文件系统视图。它不是一个真实的单独副本,而是 lowerDir 与 upperDir 叠加后的结果。
容器内看到的 /
↓
mergedDir(合并视图)
↓
upperDir(容器可写层) + lowerDir(镜像只读层)
容器内执行 ls /、cat /etc/xxx 或写入文件时,访问的都是这个合并后的视图。
3.4. workDir:OverlayFS 工作目录
workDir 是 OverlayFS 内部使用的工作目录,用于保证文件操作的原子性和一致性。它不直接暴露给容器进程,也不应由用户手动修改。
4. 文件读取、写入与删除流程
4.1. 读取文件
读取文件时,OverlayFS 按优先级查找:
先查 upperDir
↓
如果 upperDir 存在该文件,读取 upperDir 版本
↓
如果 upperDir 不存在,再查 lowerDir
如果上层与下层存在同名文件,容器看到的是上层文件。
4.2. 新增文件
容器内新增文件时,文件直接写入 upperDir。
docker exec app touch /tmp/new-file
该文件只属于当前容器。其他基于同一镜像启动的容器不会看到这个新增文件。
4.3. 修改已有文件
如果修改的文件来自镜像只读层,第一次写入会触发 COW:
lowerDir 中存在 /etc/app.conf
↓
容器修改 /etc/app.conf
↓
复制 /etc/app.conf 到 upperDir
↓
修改 upperDir 中的副本
后续继续修改该文件时,会直接修改 upperDir 中的副本,不再重复从 lowerDir 复制。
4.4. 删除文件
删除来自镜像层的文件时,Docker 不会真正删除只读镜像层中的原文件,而是在 upperDir 创建 whiteout 标记,表示该文件在合并视图中应被隐藏。
lowerDir 中仍然有 /opt/old.log
upperDir 中出现 whiteout 标记
mergedDir 中看不到 /opt/old.log
因此,在容器中删除镜像层的大文件,并不会让原镜像层立即变小。镜像大小需要在重新构建镜像时通过减少层内容来优化。
5. Overlay2 与镜像分层的关系
镜像分层与容器层之间的关系可以概括为:
镜像层 1(只读)
镜像层 2(只读)
镜像层 3(只读)
容器层(可写)
↓
merged 合并视图
构建镜像时,Dockerfile 指令会生成只读层;运行容器时,Docker 会在这些只读层之上再添加一个容器可写层。
| 阶段 | 产生的层 | 是否持久 | 示例 |
|---|---|---|---|
docker build | 镜像层 | 是 | RUN apt install、COPY app.jar |
docker run | 容器层 | 随容器存在 | 运行时日志、临时文件、配置变更 |
| volume 挂载 | 数据卷 | 独立持久 | 数据库数据目录、上传文件目录 |
运行时写入容器层的数据不适合保存关键业务数据。需要持久化的数据应使用 volume 或 bind mount。详见 Docker 三种挂载。
6. 为什么容器启动快、占用小
Docker 依赖联合文件系统获得以下效果:
- 启动时不复制完整镜像:只创建一个轻量级可写层。
- 镜像层可以共享:多个容器、多个镜像可以复用相同基础层。
- 写时复制减少无效 IO:只有被修改的文件才复制到容器层。
- 构建缓存可复用:Dockerfile 前序层未变化时,可以复用已有层。
- 镜像下载可增量化:本地已有的 layer 不需要重复拉取。
这些机制共同降低了磁盘占用、网络传输量和容器启动成本。
7. 常见误区
7.1. 删除容器内文件不等于缩小镜像
如果文件来自镜像层,容器中删除该文件只是创建 whiteout 标记。原文件仍然存在于镜像只读层。
镜像瘦身注意 如果大文件在某一层被添加,又在后续层被删除,最终镜像中仍可能保留历史层中的大文件。应在同一个
RUN指令中完成安装与清理,或从源头避免复制无用文件。
7.2. 容器层不适合保存业务数据
容器层随容器删除而删除。数据库文件、上传文件、业务日志等需要持久化的数据,应放入数据卷或外部存储。
7.3. 频繁修改大文件可能带来额外开销
COW 针对“首次修改已有文件”需要复制文件。如果容器内频繁修改来自镜像层的大文件,可能产生额外 IO 和空间消耗。此类数据更适合放在 volume 中。
8. 排查与观察命令
8.1. 查看 Docker 存储驱动
docker info | grep -i "Storage Driver"
典型输出:
Storage Driver: overlay2
8.2. 查看容器的 GraphDriver 信息
docker inspect <container_name_or_id> --format '{{json .GraphDriver.Data}}'
输出中通常可以看到类似 LowerDir、UpperDir、MergedDir、WorkDir 的路径。
8.3. 查看容器层变化
docker diff <container_name_or_id>
常见标记含义:
| 标记 | 含义 | 示例 |
|---|---|---|
A | 新增文件或目录 | A /tmp/new.log |
C | 修改文件或目录 | C /etc/app.conf |
D | 删除文件或目录 | D /opt/old.log |
9. 面试回答模板
标准回答 Docker 联合文件系统用于把多个只读镜像层和一个容器可写层透明叠加为统一的文件系统视图。以
overlay2为例,镜像层位于lowerDir,容器运行时变更写入upperDir,容器进程看到的是mergedDir。读取文件时优先查找上层,再查找下层;修改只读层文件时触发写时复制,将文件复制到容器层后再修改;删除只读层文件时通过 whiteout 标记在合并视图中隐藏文件。该机制让镜像层可以共享,容器启动无需复制完整镜像,从而节省磁盘空间并提升启动速度,同时保证不同容器之间的文件变更相互隔离。
关联文档
- Dockerfile 构建原理 + 镜像分层优化:理解 Dockerfile 指令如何形成镜像层。
- Docker 三种挂载:理解容器层、volume、bind mount 与 tmpfs 的使用边界。
- 容器技术:容器运行时、镜像和隔离机制的整体入口。
