Docker 存储驱动与 OverlayFS 深度解析:Copy-on-Write 与镜像分层原理

深度解析 Docker 存储驱动(Storage Driver)与 OverlayFS:镜像分层如何实现、Copy-on-Write 原理、overlay2 vs vfs、容器的可写层、删除与写入的 CoW 行为、磁盘占用与镜像瘦身、常见存储驱动选型与避坑。

「为什么 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. 常见避坑

坑现象对策
文件系统不支持 overlayDocker 退化为 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 的分水岭。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「DevOps」更多文章

  1. 备份与容灾自动化:RPO/RTO、Velero、PITR 与恢复演练
  2. 配置漂移与安全基线:IaC漂移检测、CIS合规、供应链安全与密钥轮换
  3. 内部开发者平台(IDP)工程化:Backstage、Golden Path 与自服务能力