联合文件系统(unionfs)是容器实现写时复制(cow)的底层支撑,其核心在于镜像层只读叠加、容器层可写分离:修改或删除lowerdir中文件时触发完整文件复制或.whiteout标记,新建文件直写upperdir,多容器共享lowerdir但upperdir完全隔离。

联合文件系统(UnionFS)是容器数据层实现 Copy-on-Write(COW)的底层支撑,理解它不是为了背概念,而是看清“为什么改一个配置文件会让容器层变大”“为什么删文件不省空间”“为什么多个容器用同一镜像却互不干扰”。关键在于把 COW 拆解成可观察、可验证的操作逻辑。
分层结构:镜像层只读 + 容器层可写
Docker 默认使用 overlay2 驱动,实际在磁盘上维护三个目录:
- lowerdir:多个冒号分隔的只读路径,对应镜像各层(如基础系统层、依赖安装层、应用代码层)
- upperdir:单个可写目录,属于当前容器独有,所有新增、修改、删除操作都落在此处
- merged:对外暴露的统一挂载点,即容器内看到的 / 根文件系统
运行 docker inspect <container_id> | grep -A 10 "UpperDir\|LowerDir\|MergedDir"</container_id> 可直接看到这些路径。注意:lowerdir 是只读叠加,顺序靠前的层优先级低;upperdir 是空的启动态,只有写发生才“长出来”。
COW 的真实触发时机:不是“一写就复制”,而是“首次写同路径”
COW 不按字节、不按块,而按完整文件粒度复制。以下行为会真正触发复制:
- 修改一个存在于 lowerdir 中的文件(例如
echo "new" > /etc/hosts)→ 系统把原/etc/hosts从某层完整拷入 upperdir,再改副本 - 删除一个 lowerdir 中的文件(例如
rm /bin/bash)→ upperdir 中生成.wh.bin/bash白名单文件,遮蔽原路径,不删原内容 - 新建一个原不存在的文件(例如
touch /app/start.log)→ 直接写入 upperdir,无复制开销
反例:反复读取 /usr/lib/libc.so.6 不触发 COW;cat /etc/passwd 也不触发——只读永远走 lowerdir,零拷贝。
容器层膨胀的常见原因与验证方法
upperdir 增长快 ≠ 写得多,常因误操作放大 COW 开销:
-
批量删除日志:
find /var/log -name "*.log" -delete会让每个被删文件都触发一次复制+whiteout,upperdir 迅速膨胀。建议用truncate -s 0 *.log清空而非删除 -
编辑大文件:用 vim 编辑 100MB 的 config.json,即使只改一行,也会复制整个文件到 upperdir。应改用流式工具(如
sed -i在支持 inplace 的场景下更轻量) -
构建时 COPY 放太靠后:Dockerfile 中
COPY . /app在RUN npm install之后,会导致每次源码变动都重建 node_modules 层——node_modules 虽未变,但 COW 机制要求上层全重做
验证 upperdir 大小:du -sh $(docker inspect <cid> | jq -r '.[0].GraphDriver.Data.UpperDir')</cid>
多容器共享与隔离的本质
同一镜像启动 5 个容器,它们的 lowerdir 完全相同(共享物理块),但 upperdir 各自独立。这意味着:
- 容器 A 写
/tmp/cache.dat,容器 B 读不到 —— 因为 B 的 merged 视图只看到自己的 upperdir 和共同 lowerdir - 容器 A 删除
/etc/motd,B 仍能正常读取 —— A 的 whiteout 不影响 B 的视图 - 镜像更新后,旧容器 upperdir 不受影响,新容器基于新 lowerdir 启动,天然隔离
这种 per-container 的 upperdir 设计,让 COW 同时满足“高效复用”和“强隔离”,不需要额外锁或同步机制。











