OverlayFS 联合文件系统深度实战:从 lowerdir/upperdir/workdir 到 whiteout 与 copy-up 的完整内核链路
OverlayFS 是 Linux 主线自 3.18(2014)起合入的联合挂载(union mount)文件系统,也是容器镜像分层存储的事实标准底座。它用一个可写的 upperdir 叠加在任意多个只读 lowerdir 之上,向上呈现一个统一的 merged 视图,而底层各层始终保持不可变。本文从第一性原理出发,把 OverlayFS 的目录合并算法、whiteout 隐藏机制、copy-up 写时复制、redirect 目录重定向、index 索引与 xino 等内核链路逐一拆开,并用可复现的命令演示每一个现象,最后落到 Docker overlay2 存储驱动与工程化陷阱。
一、为什么需要联合挂载
容器镜像由几十个不可变层(layer)组成,每一层只是相对于上一层的增删改。如果为容器把每一层都完整展开到磁盘,空间与启动时间都不可接受。联合挂载的目标只有一个:把 N 个目录"叠"成一个目录,上层优先、写操作只落在一层可写层上。
OverlayFS 因此只需 4 个要素:
| 要素 | 含义 | 约束 |
|---|---|---|
lowerdir |
一个或多个只读底层,最左为最高优先级 | 多个层用 : 分隔;各 lower 可在不同文件系统 |
upperdir |
唯一可写层,承载所有修改 | 必须与 workdir 在同一文件系统 |
workdir |
内部中转目录,用于原子 rename | 必须为空,且与 upperdir 同 fs |
merged |
最终挂载点,对用户呈现统一视图 | 只读/可写取决于挂载选项 |
merged (用户看到的统一目录)
/ | \ \
f.go sub/ del.me new.txt
| | | |
┌────┴────┴───────┴───────┴──────┐
│ upperdir (可写, 优先级最高) │
│ new.txt del.me(whiteout) │
├─────────────────────────────────┤
│ lowerdir L1 (只读) │
│ f.go sub/f.go del.me │
├─────────────────────────────────┤
│ lowerdir L0 (只读, 最底) │
│ f.go(旧) README │
└─────────────────────────────────┘
二、目录合并算法:同名如何裁决
查找一个名字时,OverlayFS 自顶向下遍历各层,取最先出现的那一层作为权威:
- 文件同名:upper 中的文件遮盖任意 lower 中的同名文件。
- 目录同名:上下层目录被合并成一个目录,子项取并集;若某子项在多层都存在,继续按层优先级裁决。
- 多 lower:
lowerdir=/l1:/l2:/l3中/l1最高、/l3最低。层序写错,内容就错。
可以用一条命令现场构造:
mkdir -p /tmp/ovl/{l1,l2,upper,work,merged}
echo "L1" > /tmp/ovl/l1/who.txt
echo "L2" > /tmp/ovl/l2/who.txt
echo "base" > /tmp/ovl/l2/base.txt
mount -t overlay overlay \
-o lowerdir=/tmp/ovl/l1:/tmp/ovl/l2,upperdir=/tmp/ovl/upper,workdir=/tmp/ovl/work \
/tmp/ovl/merged
cat /tmp/ovl/merged/who.txt # 输出 L1,l1 优先级高于 l2
ls /tmp/ovl/merged # who.txt 与 base.txt 同时存在(目录合并)
注意:upperdir 与 workdir 必须在同一文件系统,否则挂载直接报 EINVAL;workdir 必须为空目录。
三、whiteout:如何在只读层上"删除"
lower 层不可写,要"删除"一个 lower 中的文件,OverlayFS 用 whiteout 表达:在 upper 中创建一个主设备号、次设备号均为 0 的字符设备,名字与目标文件相同。
rm /tmp/ovl/merged/who.txt
ls -la /tmp/ovl/upper/
# 输出 c--------- 1 root root 0, 0 ... who.txt ← 字符设备 0,0 即 whiteout
此后 merged 视图里 who.txt 消失——不是真删除,而是被 upper 的 whiteout 遮盖。用 mknod c 0 0 可以手工制造一个 whiteout:
mknod /tmp/ovl/upper/hidden c 0 0 # merged 中 hidden 立即可见地"消失"
opaque 目录则用于"清空一个合并目录":当 upper 中有一个目录携带扩展属性 trusted.overlay.opaque=y(旧式)或 trusted.overlay.opaquer(新式),该目录会隐藏所有 lower 中同名的子目录内容。getfattr 需 CAP_SYS_ADMIN 才能读取:
getfattr -n trusted.overlay.opaque /tmp/ovl/upper/some_dir
四、copy-up:写时复制的代价与优化
OverlayFS 的写路径核心就是 copy-up:当首次修改一个位于 lower 的文件时,内核先把该文件完整复制到 upperdir,后续写入才真正落在 upper。
echo "from lower" > /tmp/ovl/l2/big.txt
cat /tmp/ovl/merged/big.txt # 读:直接来自 lower
echo "append" >> /tmp/ovl/merged/big.txt
ls -la /tmp/ovl/upper/ # big.txt 已被整体 copy-up 到 upper
du -h /tmp/ovl/upper/big.txt # 与 lower 中大小一致,整文件复制
这意味着:对一个大文件仅追加几字节,也会触发整文件复制,copy-up 是 OverlayFS 最主要的写放大来源。两个关键优化:
metacopy(Linux 4.19+):只把元数据复制到 upper,数据仍引用 lower,通过trusted.overlay.metacopy+trusted.overlay.redirect指向原文件;只有真正写入数据块时才落地。对"改属性不改内容"的场景收益极大。volatile(Linux 5.10+):放弃 upper/work 的 fsync,换取接近本地盘的写性能,代价是掉电后需fsck.overlay修复,且不再保证崩溃一致性。
挂载示例:
mount -t overlay overlay \
-o lowerdir=/l,upperdir=/u,workdir=/w,metacopy=on /merged
mount -t overlay overlay \
-o lowerdir=/l,upperdir=/u,workdir=/w,volatile /merged
五、redirect 目录:跨层重命名
没有 redirect_dir,重命名一个"合并目录"(即同时在 upper 与 lower 存在的目录)会返回 EXDEV,因为 rename 无法跨文件系统原子移动。Linux 4.10 引入 redirect_dir=on:在 upper 目录上写 trusted.overlay.redirect 扩展属性记录其"相对路径别名",使得上层目录被改名后,内核仍能定位到下层被合并的子树,从而允许 rename(2) 成功。
mount -t overlay overlay -o lowerdir=/l,upperdir=/u,workdir=/w,redirect_dir=on /merged
mv /merged/dir_old /merged/dir_new # 不再 EXDEV
getfattr -n trusted.overlay.redirect /u/dir_new
六、index 索引与 xino:inode 稳定性
index=on(Linux 4.13+):为 upper 中每个对象建立索引(含硬链接),保证同一文件在多次挂载间 inode 号一致,并支持 NFS 导出。对硬链接文件做 copy-up 时,所有链接会共享同一 upper 对象,避免"复制后分裂成独立文件"。xino=on/auto(Linux 4.17+):OverlayFS 默认各层用自己的 inode 号,跨层可能冲突。开启 xino 后,内核把层 ID 编码进 64 位 inode 号高位,使merged视图拿到全局唯一、稳定的 inode 号(某些应用依赖稳定 st_ino,如 rsync、硬链接判定)。
mount -t overlay overlay -o lowerdir=/l,upperdir=/u,workdir=/w,index=on,xino=auto /merged
七、Docker overlay2 存储驱动落地
Docker 的 overlay2 驱动就是 OverlayFS 的一层封装:/var/lib/docker/overlay2/ 是各镜像层的 lowerdir,容器可写层是 upperdir,同目录下的 work 为 workdir,lower 是指向各底层 diff 的符号链接集合(按层序排列)。查看一个运行中容器的挂载:
docker inspect <cid> --format '{{.GraphDriver.Data}}'
mount | grep overlay # 可见 lowerdir=...:...:...,upperdir=...,workdir=...
构建镜像时的删除操作用 .wh. 前缀的白出文件(如 .wh.app.log)表达,overlay2 驱动在解包时将其转换为真正的 OverlayFS whiteout。这也解释了为什么镜像里"删掉一个大文件"并不能减小镜像体积——那只是新增了一个 whiteout 层去遮盖它。
八、工程陷阱清单
- 多层 lower 查找放大:每多一层 lower,同名查找的平均路径就更长;把"热层"放最左能显著减少查找深度。
- copy-up 写抖动:日志、临时文件频繁写会反复触发整文件复制,应把这类路径挂到独立 volume(
tmpfs/bind mount),绕开 overlay。 - workdir 同 fs 约束:upper 与 work 必须在同一文件系统,跨盘挂载必然失败。
- inode 号不稳定:未开
xino时跨层 inode 可能重复,影响依赖稳定 inode 的工具。 - opaque/whiteout 的运维盲区:
rm -rf一个合并目录再重建,会在 upper 留下 opaque 目录;直接拷upper备份需注意这些特殊文件与trusted.overlay.*扩展属性(tar --xattrs才能保留)。
九、与 aufs/UnionFS 的关系
OverlayFS 取代早期用户态方案 aufs 成为内核主线,正是因为其实现更简洁、VFS 侵入更小。它不支持"多个可写层"(只有唯一 upper),也不支持把任意目录做双向合并,但这一限制恰恰匹配了容器"一层可写 + 多层只读"的模型,使其成为 cloud-native 存储的事实标准。
十、总结
OverlayFS 的全部魔法,本质上是四段式链路:合并查找(顶层优先)→ whiteout/opaque 隐藏(字符设备 0,0 与扩展属性)→ copy-up 写时复制(整文件上提,可由 metacopy/volatile 调优)→ redirect/index/xino 维护跨层一致性与稳定 inode。理解这条链路,就能解释容器镜像为什么"删了文件却不变小"、为什么写大文件会卡顿、为什么偶尔出现 EXDEV,以及如何在生产环境通过挂载选项把 OverlayFS 调校成既省空间又高性能的存储底座。

发表评论 取消回复