
搞容器和Linux系统的人几乎都绕不开UnionFS和OverlayFS这两个名字。尤其是做Docker镜像分层、系统启动时做只读根文件系统合并、或者想搞一套轻量级系统更新方案的时候这两个东西总会被摆在台面上。网上讲这两个概念的教程不少但大多停留在“OverlayFS就是UnionFS的一种实现”这种一句话结论真正拉开差距的底层机制、性能差异、以及实际操作中会踩的坑反而没人系统讲清楚。这篇文章我就用自己的实战视角把UnionFS和OverlayFS从设计思路、底层机制、性能表现到手把手挂载实验、常见故障排查完整拆一遍。这篇是系列第一篇重点解决“是什么”和“为什么”下一篇再深入overlay2存储驱动的空间回收、脏数据清理和内核层面的进阶话题。先给个基础认知UnionFS是一大类“联合文件系统”方案的总称核心思想是把多个目录叠加挂载到同一个挂载点用户看到的是一个合并后的单一目录树。OverlayFS是Linux内核真正落地、并成为现代容器默认存储驱动的那个具体实现它也是UnionFS思想的一种但做了大量面向性能、稳定性和内核集成的取舍。说人话UnionFS是概念家族OverlayFS是“在Linux里真正能打的那个具体方案”。如果你正在做Docker存储驱动选型、嵌入式Linux只读文件系统设计或者纯粹想搞明白镜像分层背后的原理这篇文章都适合你。我默认你有基本的Linux操作能力但不会假设你读过内核源码所有机制我都会用场景化的大白话讲清楚。1. 先搞懂UnionFS与OverlayFS的设计目标1.1 没有联合文件系统时多目录合并到底有多痛在容器技术大规模落地之前想把一个只读根文件系统和一个可写数据目录合并成一个完整根目录传统手段非常别扭。最常见的操作是bind mount和symlink但bind mount是“覆盖”关系不是“合并”关系。你把 /data 挂到 /overlay/data原来 /overlay/data 下的内容会被整个遮住根本看不到更别提把两个目录的内容融合到一起。那时候工程师的做法通常是写一个启动脚本先把两个目录用cp -a复制到一个临时目录再把临时目录挂载为根目录。听起来简单实际用起来全是泪。复制合并意味着空间翻倍、启动时间变长而且只要源目录有更新合并结果就必须重新生成。嵌入式设备做OTA升级时明明只是更新一个应用二进制文件却要完整复制整个根文件系统浪费不说还容易在断电时把合并目录搞到一半的状态设备直接变砖。这个痛点就是UnionFS的出发点让多个目录逻辑上合并不做物理复制从内核层面解决“合并视图”的问题。1.2 “层叠分支”模型UnionFS的核心设计UnionFS的经典模型叫层叠分支branch每个被合并的目录称为一个分支或层layer。挂载时指定优先级上层优先下层兜底。读取文件时从上往下逐层查找找到第一个命中的就返回。写入则分两种情况对上层分支直接写对下层分支的内容先执行copy-up把文件复制到上层再在副本上修改。删除下层文件不是真的去删而是在上层创建一个特殊标记来“遮住”它这个标记就是whiteout。这套设计的精妙之处在于每一层可以保持独立的生命周期。下层可以整体只读上层随便折腾互不干扰。这直接启发了Docker的镜像分层每一层镜像就是一个只读分支容器运行时新增的可写层就是最上层分支。镜像可以共享容器可以随意修改但底层镜像永远不会被污染。没有这个模型Docker的镜像复用和秒级启动基本就是空中楼阁。1.3 为什么UnionFS能做的事情OverlayFS却刻意砍掉一半有人会疑惑既然层叠分支这么优雅为什么OverlayFS初期只支持一个可写上层层答案在于复杂度和正确性的平衡。文件系统要处理的元数据操作非常多open、create、rename、unlink、chmod、chown、setxattr、readdir每一类操作在多层叠加下都要重新定义语义。多个层都有同一个目录时最终目录内容怎么合并重命名一个在下层存在、在上层也存在的目录需要处理哪些边界情况如果所有分支都可以写那么同一文件出现两个副本后续修改到底以谁为准这些问题每解决一个代码复杂度和出错概率就翻一倍。OverlayFS的开发者Neil Brown没有选择把UnionFS的所有功能重新实现一遍而是砍出一个最小可行集合一个只读lower层加一个可写upper层先把最常见的“只读底座可写覆盖”场景做到内核主线可接受的水平。事实证明这是对的。正因为它足够克制才能进入Linux内核主线成为标准能力。后来OverlayFS再逐步扩展多层lowerdir也只是在只读层数量上做加法可写层始终唯一。2. 底层机制到底差在哪里copy-up、whiteout与层叠逻辑2.1 老牌UnionFS与aufs功能丰富但没搭上主线列车Linux早期最有名的UnionFS实现功能堪称全面任意层数、分支动态增删、读写分支任意混合几乎没有它做不到的联合挂载需求。但这些能力大多停留在设计文档和补丁层面加起来就是四个字又重又难。它要么运行在FUSE用户态性能开销大得惊人要么作为独立内核补丁存在和内核版本的适配简直是噩梦。当时Docker早期默认用的是aufs它是UnionFS的继任者由日本人Junjiro Okajima开发解决了一部分稳定性和性能问题但同样没能进入Linux主线内核。这意味着什么你用Ubuntu自带aufs补丁的发行版Docker跑得很欢换到RHEL或CentOS内核里根本没有aufsDocker只能换存储驱动否则直接罢工。早期做容器平台的人几乎都被“不同发行版存储驱动不兼容”折磨过。选择哪个存储驱动很多时候不是看功能而是看发行版内核里有什么。2.2 OverlayFS的诞生背景必须进主线的执念OverlayFS的出现本质上是给这场“联合文件系统混战”画上一个句号。它的目标非常明确做一个足够简洁、足够安全、能被Linux内核主线接受的联合挂载方案。所以它一开始只支持两个分支lowerdir和upperdir外加一个workdir用于元数据操作的原子过渡。“只支持两层”在早期被不少人嘲笑功能太弱但恰恰是这个限制让它活了下来。内核社区对文件系统的审查极为严格任何一个语义模糊的操作都会成为拒绝合入的理由。OverlayFS把边界定义得清清楚楚可写层只有一个其他全部只读所有写操作都必须经过copy-up或whiteout这两条明确路径。这样内核维护者才能放心把它纳入主线。从Linux 3.18开始OverlayFS正式合入内核之后逐渐支持多层lowerdir满足容器镜像多层堆叠的需求但upper可写层始终只有一个。2.3 copy-up的细节是很多人忽略的性能杀手OverlayFS的copy-up机制表面看很简单修改下层文件时先把整个文件复制到上层。但这里面有三个坑不亲手试过根本想不到。第一个坑是复制粒度。copy-up是按文件为单位进行的哪怕你只是往一个1GB文件的末尾追加一个字节内核也会把整个1GB文件完整复制到upper层。换句话说对只读层文件做“小改动”代价是复制整个文件。如果你的应用在容器里频繁修改镜像层里的日志文件或数据库文件upper层的空间增长速度和IO开销会非常难看。第二个坑是inode变化。copy-up的本质是创建一个新文件所以复制完成后文件的inode号会变硬链接关系会丢失。如果你有一些基于inode做监控、定位的运维工具在OverlayFS的merged目录里会发现监控数据“漂移”。已经打开的fd不会受到copy-up影响因为它还指向旧文件但新打开的fd指向的是upper里的新inode。第三个坑是对元数据操作的处理。chmod、chown、setxattr这类操作在较新内核版本中会尝试只复制元数据而不搬运文件内容但这个优化在不同内核版本上的行为不完全一致。我的建议是永远不要依赖“改权限不会触发copy-up”这种假设该担心的还是得担心。2.4 whiteout与rename删除和重命名的隐藏成本OverlayFS删除lower层文件会在upper层创建一个字符设备节点主设备号0、次设备号0这就是whiteout。读目录时内核遇到whiteout就跳过对应下层条目逻辑上实现“这个文件已经被删除”。但如果只停留在“创建标记遮住文件”这个层面会忽略一个特别重要的问题删除一个下层目录时OverlayFS的处理远比删除一个文件复杂。因为目录的子条目可能分散在各层删除动作需要对整个子树做处理涉及大量copy-up和whiteout的组合操作。某些版本在删除包含大量文件的lower目录时延迟会非常明显。重命名的情况更特殊。重命名一个lower层文件或目录时OverlayFS需要对旧名字创建whiteout然后把新名字复制到upper层。对于目录还要递归处理目录内的所有条目这是一个高风险高成本的操作。如果你在容器里用rename做类似“临时文件原子替换”的操作并且目标文件位于镜像层会发现性能比在普通文件系统上差很多。3. 性能、兼容性与适用场景的硬核对比3.1 读性能和写性能实测数据怎么看老规矩先摆实测结论OverlayFS的读性能通常明显优于aufs和经典UnionFS实现。原因很简单aufs需要遍历所有分支来确认文件是否存在OverlayFS的查找逻辑更紧凑对page cache的利用也更高效。写性能方面copy-up确实会带来额外开销但overlay2驱动在Docker里做了大量优化比如合并相同inode、避免重复copy-up整体表现相当稳定。我自己的粗测结果可以供参考同一台物理机、同样一个约500MB的镜像用overlay2驱动连续启动100个nginx容器耗时比aufs稳定低15%到25%。不同内核版本和磁盘类型下数值会有波动但趋势是一致的。如果你的容器启动速度明显比同事慢先检查一下存储驱动是不是被切到了vfs之类的最差选项。再提供一个思考方法读性能优化看的是“合并视图是否可以让Linux页缓存直接命中”写性能优化看的是“如何减少不必要的copy-up”。理解了这两条主线不同场景下选择存储驱动时自然就有了判断依据。3.2 兼容性进不进内核主线是生死线这是UnionFS系和OverlayFS之间最残酷的差距aufs和经典UnionFS都没有进Linux内核主线而OverlayFS在主线上。这意味着你现在随便开一台标准发行版服务器不用装任何补丁直接mount -t overlay就能用。aufs则完全依赖各发行版自行打包补丁。基于这个差异我的选型建议非常明确企业内网环境、RHEL/CentOS/SLES系列OverlayFS是唯一稳妥的选择老版本Ubuntu可能默认还是aufs但新版本全面切到overlay2你也应该跟上任何需要长期维护的嵌入式项目选OverlayFS不要选依赖内核补丁的方案很多踩过坑的人都知道aufs在某次内核升级后无人维护容器全部启动失败那种痛苦没有必要体验一次。3.3 快速决策速查表维度经典UnionFSaufsOverlayFS是否进入内核主线否否是upper可写分支数支持任意层支持多个可写分支仅1个lower合并数支持任意层支持多个支持多个冒号分隔动态添加/删除分支支持支持部分版本支持有限rename能力相对完整相对完整有限制成本高稳定性一般中等优秀常见场景学术研究Docker早期默认现代容器、系统更新、只读根FS这个表是简化后的判断具体能力受内核版本影响很大但大方向是准的。做技术选型时建议再叠加一个维度团队是否有能力维护自定义内核补丁。没这个能力就别碰非主线方案。4. 手把手挂载一个OverlayFS亲手验证copy-up和whiteout4.1 环境准备与目录结构建议在任意Linux 3.18机器上操作Ubuntu 20.04或CentOS 7.9都行需要root权限。有一点要强调别在正在运行生产容器的高负载宿主机上直接拿根目录做实验虽然OverlayFS整体安全但操作失误会影响现有容器环境。准备演示目录mkdir -p /tmp/ovl/{lower,upper,work,merged} echo I am from lower /tmp/ovl/lower/hello.txt echo I am upper init /tmp/ovl/upper/world.txt这里lower代表只读镜像层upper代表容器可写层work是OverlayFS内核用来做原子操作的临时空间merged就是最终的合并视图。4.2 最基础的overlay挂载命令mount -t overlay overlay -o lowerdir/tmp/ovl/lower,upperdir/tmp/ovl/upper,workdir/tmp/ovl/work /tmp/ovl/merged挂载后有四个关键点必须记住lowerdir可以给多个用冒号分隔从左到右优先级递减upperdir和workdir必须在同一个文件系统上workdir不能和upperdir相同workdir承担copy-up、rename、remove的中间过渡不能被随意清理merged目录是唯一对外暴露的视图业务进程只操作merged挂载完成后在/tmp/ovl/merged下应该同时看到hello.txt和world.txt一个来自lower一个来自upper。这就是联合文件系统最直观的“合并视图”。4.3 现场演示copy-up在merged里修改lower层文件echo modified /tmp/ovl/merged/hello.txt然后查看upper目录ls -l /tmp/ovl/upper/你会发现hello.txt出现在upper目录里内容包含“I am from lower”和“modified”两行。而lower目录里的hello.txt保持原样。这个现象就是copy-up的完整现场。如果你把hello.txt换成10GB的大文件再执行同样的追加操作会瞬间看到磁盘空间上涨和时间延迟这时候就能真切体会到“小改动大复制”的含义。4.4 现场演示whiteout删除merged里来自lower的文件rm /tmp/ovl/merged/hello.txt再查看upper目录ls -l /tmp/ovl/upper/你会看到一个字符设备节点主设备号0、次设备号0是这个样子c--------- 1 root root 0, 0 ...这就是whiteout标记。merged目录里hello.txt已经消失但lower目录里的原始文件其实还在。这个设计保证了只读层的完整性所有变更都沉淀在upper层。理解whiteout后再看Docker容器删除镜像层自带的配置文件你应该能猜到背后发生了什么容器可写层多了一个白色标记而不是真的把镜像文件删掉。4.5 多层lowerdir与镜像分层的对应关系再验证一下多层lowerdir。创建两个lower目录mkdir -p /tmp/ovl/lower1 /tmp/ovl/lower2 echo from lower1 /tmp/ovl/lower1/a.txt echo from lower2 /tmp/ovl/lower2/b.txt挂载时指定两个lowermount -t overlay overlay -o lowerdir/tmp/ovl/lower1:/tmp/ovl/lower2,upperdir/tmp/ovl/upper,workdir/tmp/ovl/work /tmp/ovl/mergedmerged里会同时看到a.txt和b.txt。如果lower1和lower2有同名文件c.txt那么lower1的c.txt会覆盖lower2的c.txt因为lowerdir顺序从左到右优先级递减。这就是Docker镜像分层的原理多个镜像层就是多个只读lowerdir上层镜像对同名文件的修改会覆盖下层镜像的对应文件。5. 我踩过的坑OverlayFS常见故障与排查手册5.1 挂载时报“wrong fs type”最常见的原因是内核版本太老或者内核没编译OverlayFS模块。先检查grep overlay /proc/filesystems如果输出里没有overlay说明内核没支持。可以尝试modprobe overlay加载模块如果模块不存在只能升级内核。CentOS 7.9默认支持老版本CentOS 6基本没戏。5.2 “failed to create workdir”的真相这个报错十有八九是upperdir和workdir不在同一个文件系统。OverlayFS强制要求这两者在同一个挂载点/文件系统下因为它们之间需要高频rename操作来保证原子性。你把upperdir放在/data大盘workdir放在/tmp的tmpfs上一定会报错。解决办法很简单upperdir/data/upper,workdir/data/work再强调一次workdir必须和upperdir同盘workdir不能等于upperdir。5.3 copy-up后硬链接失效这是真实踩过的坑。在merged目录里给lower层文件创建硬链接然后修改链接后的文件原文件内容没变。原因是修改触发了copy-up新文件是复制到upper层的副本原inode没被动过。硬链接不会穿透层如果应用重度依赖硬链接语义改造方案要么用软链接要么在upper层预置文件避免运行时copy-up。排查时用stat对比修改前后文件的inode号和链接数inode变了就一定触发过copy-up。5.4 Docker overlay2磁盘空间暴涨这是最常见的容器运维问题。overlay2的upper层持续增长底层镜像层不变但容器内产生的所有文件都会写到upper层。如果你在容器里写日志、缓存、临时文件删除后whiteout会保留磁盘空间不会立刻释放有时还会因为文件被容器进程占用而持续占盘。排查思路docker system df du -sh /var/lib/docker/overlay2/容器ID如果确认是容器可写层在涨建议把日志和缓存挂载到外部volume别写入容器可写层。5.5 底层文件系统不支持d_typeOverlayFS要求底层文件系统支持d_type目录项类型如果不支持挂载时会有告警readdir行为可能异常最直接的后果是Docker镜像删除时明明文件没了目录信息却残留。检查底层文件系统类型df -T /tmp/ovl/lower底层尽量用ext4或xfs避免在NFS、CIFS这类网络文件系统上直接跑overlay2存储驱动。5.6 与SELinux的权限纠缠RHEL/CentOS开启SELinux时OverlayFS需要正确的安全上下文不然容器启动就报Permission Denied。Docker的overlay2驱动在SELinux环境下会自动处理一部分但手动挂载OverlayFS做系统更新时要给upper/lower目录设置合适的标签。排查工具ausearch -m avc -ts recent看到avc denied记录后用chcon或semanage fcontext修正标签。别一上来就setenforce 0那是自己给自己埋雷。5.7 workdir被误删后的脏状态如果workdir被清理或者系统异常断电OverlayFS可能留下不完整的copy-up状态挂载时报错或者merged目录里文件消失。这时候检查upper目录里有没有异常临时文件比如以.wh.开头或者带特殊后缀的文件。可以尝试重新挂载如果还不行备份upper层内容后重建workdir。日常运维里workdir目录不要随便清理它和upper一样重要。6. 我在实际项目中的选择与体会老实说在我接触过的企业级容器平台和嵌入式Linux项目里还没有哪个场景是打死都要用aufs或经典UnionFS的。大多数团队选择aufs只是因为“老Ubuntu默认就是它”迁移到overlay2之后稳定性感知是上了一个台阶的。内核主线支持这个优势在日常运维里价值极高升级内核不怕补丁失配装新机器不用额外装模块出问题查文档也有更多案例可以参考。我给新人的建议是不要只看概念图一定亲手按第4节的步骤做一遍实验。Copy-up和whiteout这两个概念看十篇文章不如实际操作十分钟。当你亲眼看到lower目录里的文件“被删除”后其实还在upper目录里多了一个字符设备节点的时候Docker镜像分层的很多谜团会瞬间想通。这一篇主要解决UnionFS和OverlayFS的原理与基础实操。下一篇我打算把重心放到overlay2存储驱动的空间回收机制、脏数据清理、以及OverlayFS与page cache、mmap之间的交互细节上。如果你在实操中遇到本文没覆盖到的怪异现象带上内核版本、文件系统类型和挂载参数来交流我会很有兴趣。