「为什么 Docker 镜像那么省空间、启动那么快?」答案在存储驱动的 Copy-on-Write(CoW)机制里。镜像由一层层只读层叠加而成,容器的写入发生在最上层的可写层——共享底层、按需复制,这是 Docker 空间效率的根基。本指南拆解 OverlayFS 与存储驱动的原理、读写路径、CoW 行为,以及磁盘占用与镜像瘦身。
关键概念:镜像层是只读的;容器启动后在最上层加一个可写层。读文件会从上到下找,写文件走 Copy-on-Write(写时复制):把底层文件复制到可写层再修改,底层镜像层原封不动。
1. 镜像分层的存储原理
1.1 镜像层与容器的关系
Dockerfile 的每个指令 ≈ 镜像的一个层(layer):
FROM base → 基础层
RUN apt install → 新层(包含变更的文件)
COPY app.py → 新层
... (最终镜像 = 若干只读层叠加)
容器运行 = 镜像只读层 + 一个可写层(容器层)
docker commit 容器 → 把可写层固化为一个新镜像层
1.2 为什么分层省空间
省空间的根源:多个容器共享底层只读层
同一个镜像跑 100 个容器 → 只读层只存一份
只有各自可写层的差异占各自空间
分层还带来:
- 镜像传输:已拉的层不重复下载
- 构建缓存:指令层不变时复用
- 回滚:层是"差异"而非"全量"
ℹ️ 核心:Docker 的空间效率来自"共享 + 差异"。理解"底层只读、上层可写、CoW 按需复制",就理解了容器存储的一切。
2. Copy-on-Write(CoW)机制
2.1 读与写的不同路径
读文件:
从可写层开始,逐层向下找
→ 最上层有就用最上层的;否则继续往下直到找到
写文件(首次写某个底层文件):
1. 把底层目标文件"复制到可写层"(copy-up)
2. 在可写层修改
3. 底层文件不变,新内容在可写层
→ 这就是 Copy-on-Write:写的时候才复制
好处:
- 未写的文件不占额外空间(共享底层)
- 镜像层永远不可变、可复用
2.2 CoW 的一个直观例子
镜像有一个 /etc/config(底层只读层)
两个容器同时修改它:
容器 A:copy /etc/config 到 A 的可写层 → 改
容器 B:copy /etc/config 到 B 的可写层 → 改
→ 底层镜像的 /etc/config 始终不变
→ 两个容器各持有自己的副本,互不干扰
代价:首次写大文件会复制(写放大)
高 IO 热路径的容器,可写层写放大是性能要点
3. OverlayFS 与 overlay2 驱动
3.1 OverlayFS 基本结构
OverlayFS 把两个目录"叠加"成一个视图:
lowerdir(底层,只读)= 镜像层
upperdir(上层,可写)= 容器可写层
merged(合并视图)= 容器看到的完整文件系统
overlay2 驱动 = Docker 用 OverlayFS 实现镜像分层存储
每个镜像层是一个 lowerdir 条目,最上层可写
文件系统要求:
- xfs(ftype=1)或 ext4
- Linux 内核支持 overlayfs
3.2 overlay2 的读写
# 查看当前存储驱动
docker info | grep -i storage
# 典型输出:Storage Driver: overlay2
# 看到 diff / merged / upper 目录等(在 /var/lib/docker/overlay2)
overlay2 的优势:
- 原生内核支持、性能好
- 页缓存共享(同一镜像层可共享内存页)
- 比旧 vfs(每层完整复制)省空间几个数量级
Docker 默认:
现代 Linux(内核 4.x+ 且支持 overlay)→ 自动用 overlay2
不支持 overlay 的环境 → 退化为 vfs(极慢、极占空间)
4. 容器可写层的行为细节
4.1 修改、删除、新增
修改文件:copy-up 到可写层再改
删除文件:在可写层加"白名单/删除标记"(overlay 的 whiteout)
底层文件其实还在,只是从合并视图"隐藏"
新增文件:直接写在可写层
影响:
- "删除"不减底层空间(白名单只是遮住)
- 这也是为什么"删除大文件后镜像不减小的原因之一"
4.2 磁盘占用与镜像瘦身
镜像越来越大,常见原因:
1. 每层都保存"差异",层数越多冗余越多
→ 合并层 / 重写 Dockerfile(把变化少的放前面)
2. RUN 里下载又删除,删除只在当前层生效
→ 下载与清理放同一 RUN(同一层)
3. 容器日志/临时文件写入可写层,容器删除后消失
→ 日志走 json-file/logrotate 或用 tmpfs 挂载
瘦身抓手:
- 多阶段构建(只留运行层)
- 同一 RUN 里装依赖 + 清理缓存
- 谨慎使用大体积基镜像
5. 不同场景的存储驱动选型
常用驱动对比:
overlay2 默认,性能好,推荐(现代 Linux)
vfs 每层全量复制,慢、费空间(仅特殊环境)
aufs overlay 的前辈,老 Debian/Ubuntu 历史选择
btrfs/zfs 结合文件系统自身快照,适合超大镜像场景
fuse-overlayfs 无特权环境(Rootless)常用
选型依据:
- 生产 Linux 服务器 → overlay2 就够了
- Rootless 模式 → fuse-overlayfs
- 超大规模镜像仓库/集群 → 可评估 btrfs/zfs
# 切换存储驱动需在 daemon.json 配置
# {"storage-driver": "overlay2"}
# 注意:切换会重建所有镜像层,慎在已有大量镜像的环境操作
6. 常见避坑
| 坑 | 现象 | 对策 |
|---|---|---|
| 文件系统不支持 overlay | Docker 退化为 vfs | 用 xfs/ext4 并启用 ftype |
| 磁盘快速占满 | 可写层写放大 | 日志外置、tmpfs、定期清理 |
| 删除文件空间不减 | 白名单机制 | 重建镜像层 / 用卷 |
| 修改大文件慢 | CoW 复制大文件 | 避免热路径写底层大文件 |
| 镜像层过多冗余 | 镜像膨胀 | 多阶段 + 同层清理 |
| 无特权跑容器 | overlay 不可用 | fuse-overlayfs / Rootless |
7. 最佳实践清单
□ 确认 daemon 使用 overlay2(docker info 检查)
□ 宿主用 xfs(ftype=1)/ext4 文件系统
□ Dockerfile 用多阶段构建,减少冗余层
□ 依赖安装 + 缓存清理放同一 RUN(同层)
□ 日志/临时文件走卷或 tmpfs,别堆可写层
□ 定期 docker system prune 清理悬空镜像与层
□ 监控 /var/lib/docker 磁盘占用
□ 有状态数据一律用 Volume,不依赖可写层
一句话原则
镜像只读分层 + 可写层 CoW,
理解了它,就理解了 Docker 的空间、速度与清理的一切。
小结
Docker 存储驱动与 OverlayFS 的核心是"只读镜像层共享 + 可写层 Copy-on-Write"。落地记住五件事:overlay2 是生产默认并确认启用、写文件才复制(CoW)所以未写不占空间、删除只加白名单所以空间不减、多阶段 + 同层清理控制镜像膨胀、有状态数据用 Volume。当你能预判"哪一层会复制、哪个操作真占空间",镜像构建与磁盘治理就不再是黑盒——这正是深度掌握 Docker 的分水岭。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。