资讯动态

overlay写时复制全解析:qcow2外部快照与Docker镜像层原理

发布时间:2026/9/28 11:32:13 来源:尧图企业网站定制
1. 先弄清楚全网都在说overlay我们到底聊的是哪一个war3 replay overlay、[drm] failed to init overlay plane cluster0-win1、docker清理overlay数据……overlay这个词在技术圈里泛滥到什么程度打游戏录像要提overlay显卡驱动报错要提overlay容器存储还要提overlay。我甚至见过有人把汇编里warning l16: uncalled segment, ignored for overlay process当成存储问题来查——那是链接器在处理覆盖段跟磁盘镜像八竿子打不着。这篇文章里的overlay特指虚拟化磁盘镜像和容器镜像里的写时复制Copy-on-Write, CoW机制。你有一个基础镜像我们管它叫backing file然后基于它生成一个外部快照文件这个外部快照就是overlay。所有的新数据、修改数据全部落到overlay上底层的backing file完全不动。这么说可能有点干巴。打个比方基础镜像是一本已经印刷好的参考书overlay是你在书页上贴的一沓便利贴。你在便利贴上记新笔记、改错别字翻书的时候看到的效果是便利贴覆盖在书页之上的合成视图但书本身一个字都没改。你想恢复成原样把便利贴全撕掉就行。这就是backing file和overlay最核心的关系。这个机制在KVM/QEMU虚拟化里用得最多也是Docker镜像分层的底层原理。我最早接触这套东西是在管理一批KVM虚拟机的时候系统盘紧张每台虚拟机都装一整份完整的系统镜像几百GB的宿主机硬盘很快就见底了。后来用外部快照的方式所有虚拟机共享同一个基础镜像每台只占增量数据硬盘一下子就宽裕了。这篇文章我会把整条链路讲透——从qcow2镜像的原理、backing file和overlay怎么配合、外部快照怎么做、怎么提交合并数据再延伸到Docker的overlay2存储驱动和日常清理。你既能在KVM场景直接操作也能把底层原理迁移到容器场景。适合正在管理虚拟化环境、被磁盘占用逼到头秃的运维也适合刚接触容器存储、想搞明白镜像层到底是什么的新手。2. backing file和overlay的底层原理为什么能做到只存增量2.1 qcow2镜像的数据组织方式要理解backing file和overlay的关系先得搞清楚qcow2这种镜像格式本身是怎么存数据的。qcow2全称QEMU Copy On Write Version 2它天生就是为写时复制设计的。一个qcow2文件从结构上分三个部分文件头、L1表、L2表最后才是真正的数据块。文件头存的是镜像的基础信息版本号、镜像大小、簇大小、加密方式、还有指向backing file的路径。L1表是一个一维数组每个表项指向一个L2表L2表又是一个一维数组每个表项指向一个实际的数据簇。当你读写某个扇区时QEMU先把虚拟磁盘偏移换算成簇号查L1表找到L2表再查L2表找到数据簇拿到真正的数据。这套两级索引的设计本质上就是一个页表。学过操作系统的同学应该秒懂这和CPU虚拟内存的页表机制是一模一样的思路。为什么要搞两级而不是一张大表因为qcow2镜像可能非常大动辄几十GB上百GB如果用一张平铺的表来记录所有簇的位置这张表本身就得占掉天量空间。用两级表L1表只存指向L2表的指针L2表按需分配镜像文件越小表占的空间就越少。默认簇大小是64KB也就是说L2表里每一项管64KB的数据块。一个L2表默认有512个表项每项8字节所以一个L2表能覆盖512×64KB32MB的文件空间。文件再大就增加L1表的表项数来多挂几个L2表。这套设计非常精巧理解了它你就能明白overlay机制为什么能做到只存增量。2.2 写时复制新数据落到overlay老数据依然从backing file读现在把backing file和overlay放到一起看。当你执行qemu-img create -f qcow2 -b ubuntu-base.qcow2 ubuntu-instance.qcow2这条命令创建了一个新的qcow2文件ubuntu-instance.qcow2它的文件头里记录了backing file的路径是ubuntu-base.qcow2。此时ubuntu-instance.qcow2本身几乎不占空间L1表、L2表都是空的所有数据都从backing file读。当你往ubuntu-instance.qcow2写入数据时QEMU检查L2表目标簇如果有映射直接写入如果没有映射说明这个簇的数据在backing file里于是先从backing file把整个簇读出来在内存里把要修改的部分改掉然后把整个簇写入ubuntu-instance.qcow2并更新L2表映射。这就是写时复制四个字的由来——只有当你试图修改某个簇的数据时才发生复制动作把数据从backing file复制到overlay。读数据时也一样QEMU先查overlay的L2表如果查到了就返回overlay的数据查不到自动顺着backing file的路径去读底层数据。最终你能看到的、能操作的是overlay覆盖在backing file之上的合成结果。这就是外部快照的外部二字的含义快照数据不在backing file里面而是在外部的另一个文件里。有一个关键细节overlay里标记这个簇被写过靠的就是L2表是否有映射。如果L2表里该表项是0表示本层没有数据请找backing file如果非0表示本层有这个簇直接读我这里。所以QEMU判断是否要复制数据只需要一次查表效率非常高。2.3 为什么基础镜像必须只读写坏的教训这地方有一个铁律backing file在overlay存活期间不能被写入甚至不能被移动、重命名。原因不复杂但很多人栽过跟头。想象一下backing file里的某个簇是Aoverlay基于A做了修改overlay里存了A。如果此时backing file的A被改成了B那么overlay读取旧数据时就会出现混乱——有些簇读到的是B有些簇读到的是A整个文件系统结构可能彻底崩塌。我实际踩过这个坑。当时图省事直接把基础镜像做成模板文件多台虚拟机共用。某天为了给另一批虚拟机升级软件我直接在基础镜像上跑了软件包更新命令。结果所有基于这个基础镜像的外部快照虚拟机一夜之间出现了大量随机性的文件损坏ssh-keygen报key mismatch、web服务起不来、mysql表损坏排查了整整两天才定位到是backing file被污染了。正确做法是基础镜像做出来后立刻把文件权限改成只读甚至放到一个专门的模板目录通过文件系统权限挡死写入行为。虽然qcow2本身不会阻止你写但权限能保护你。提示backing file的路径是记录在overlay的文件头里的而且是纯文本路径。如果你移动了backing fileoverlay再启动时就会报Could not open backing file逻辑上就断了。要恢复得用qemu-img rebase重新指定路径。3. 外部快照实操从创建、使用到链式扩展3.1 创建第一张外部快照三条命令搞定创建外部快照本身非常简单核心命令就是上面那条qemu-img create。完整流程是这样的首先准备基础镜像。假设我有一台装好系统、打完补丁的虚拟机镜像ubuntu-base.qcow2把它放到/templates目录mkdir -p /templates mv ubuntu-base.qcow2 /templates/ chmod 444 /templates/ubuntu-base.qcow2 # 只读保护然后基于它创建实例镜像qemu-img create -f qcow2 -b /templates/ubuntu-base.qcow2 /vms/instance01.qcow2这条命令有两个关键参数。-f qcow2指定目标格式-b指定backing file路径。执行完之后instance01.qcow2是一个几乎空白的文件只有文件头和必要的表结构大小大概几百KB。用qemu-img info看一眼qemu-img info /vms/instance01.qcow2输出里会出现一行backing file: /templates/ubuntu-base.qcow2这就是两者的关联证据。如果这行路径不对虚拟机起不来。我建议任何时候都用绝对路径写backing file相对路径容易在目录切换时踩坑。创建完实例镜像用这个镜像启动一台新虚拟机虚拟机的系统盘指向instance01.qcow2。此时虚拟机里能看到完整的系统运行状态和直接用基础镜像启动一模一样。区别在于你做的任何修改都会写到instance01.qcow2里基础镜像像什么都没发生过一样。3.2 磁盘占用实测增量到底能省多少我说一个实际数据。我手头有一个精简安装后的Ubuntu Server 22.04基础镜像打完基础安全补丁大小大约2.1GB。基于它创建了8台虚拟机分别跑Nginx、MySQL、Redis、日志采集等不同业务。跑了一个月之后我统计了每台虚拟机的镜像实际占用虚拟机用途overlay镜像大小增量占比Nginx负载均衡286MB13.6%MySQL主库1.8GB85.7%Redis缓存512MB24.4%日志采集器96MB4.6%MySQL增量最大因为它的数据文件本身就在写。日志采集器增量最小因为系统装完就没怎么动过。总体算下来如果之前每台虚拟机都要完整复制2.1GB系统盘8台就是16.8GB用了外部快照之后基础镜像2.1GB加上全部增量约3.4GB加起来不到6GB省了六成以上空间。不过有一说一省空间不是唯一目的。外部快照更大的价值在于快速部署新开一台虚拟机不用复制文件qemu-img create瞬间完成虚拟机秒级启动。这在批量交付开发环境、测试环境时特别香。3.3 快照链层层叠加的正确玩法外部快照不只是一层它可以无限往下叠。比如我在instance01上跑了一段时间业务想再做一次快照作为新的回滚点操作方式是在现有实例之上再套一层qemu-img create -f qcow2 -b /vms/instance01.qcow2 /vms/instance01-snap.qcow2这样一来就形成了一条链ubuntu-base.qcow2 → instance01.qcow2 → instance01-snap.qcow2。虚拟机改用instance01-snap.qcow2启动读取数据时逐层往上找先查最顶层没有就查下一层直到基础镜像。链式快照对开发环境的试错场景非常友好。我要在系统里测一个高危脚本先做一层快照测完不满意直接把顶层快照删掉重来宿主机的数据分毫未损。这个删除操作要小心删的只是最顶层那个快照文件它底下的backing file不会被删。链式快照的代价是性能损耗和复杂度。每一层多一次查表跳转虽然kvm/qemu做了缓存优化但层数多了IO延迟仍然会上升。另外链越长排查问题和手工合并就越麻烦。我个人的建议是链深控制在3层以内超过就考虑用commit把数据合并回底层。3.4 合并数据qemu-img commit的正确姿势增量攒多了overlay文件越来越大读写性能也开始下降这时候就需要把overlay的数据合并回backing file。用的命令是qemu-img commit /vms/instance01.qcow2这条命令把instance01.qcow2里所有属于自己的数据写回它的backing file也就是ubuntu-base.qcow2。执行完instance01.qcow2里的数据被清空之后再读数据就直接走backing file了。commit有几个注意事项。第一执行commit时虚拟机必须关机否则正在写入的数据会造成不一致。第二commit后overlay文件本身还在但内容已清空如果你反复提交文件会变成一个悬空的空壳。第三也是最容易被忽略的commit会修改backing file——前面刚说完backing file必须只读commit就是一个合法的写操作。这也就是为什么我一直强调只有你知道这个基础镜像不会再被其他overlay引用时才允许commit。qemu-img rebase是用来换backing file的qemu-img rebase -b /templates/ubuntu-base-v2.qcow2 /vms/instance01.qcow2rebase是把overlay从一个backing file切换到另一个。如果新旧backing file内容相同rebase只是改个路径如果内容不同rebase会尝试把overlay中已有的数据和旧backing file的差异合并到新backing file的语义下。这在基础镜像升级时特别有用系统镜像升级版本后你不用重新创建所有实例只需要rebase一下补上缺失的差异数据就行。注意rebase操作比较重如果overlay和backing file差异巨大耗时长且风险高。稳妥起见执行前先备份overlay文件。我在生产环境里基本只对差异可控的镜像层做rebase差异大的直接新建实例迁移业务。4. Docker世界里的overlay2镜像分层是外部快照的孪生兄弟4.1 镜像层、读写层和容器层的关系聊完KVM/QEMU的外部快照再看Docker的存储你会发现逻辑完全一样。Docker镜像由一层一层的只读层叠加而成Dockerfile里每一条指令产生一个新的镜像层。这些只读层就是backing file们。启动容器时Docker在只读层之上创建一层可写层——这就是overlay。Docker默认的存储驱动是overlay2它的实现和qcow2外部快照如出一辙lowerdir存放多个只读层upperdir是可写层merged是两者的联合挂载视图。你在容器里写文件数据进upperdir读文件时overlay文件系统先查upperdir查不到再往下查lowerdir。这不就是backing file和overlay嘛。我举一个实际的例子。拉一个Nginx镜像它的构成大概是这样的基础层debian或alpine、nginx安装层、配置文件层、可能还有entrypoint脚本层。每一层都是一个独立的目录存放在/var/lib/docker/overlay2/下面。容器运行时这些目录被按顺序组装成lowerdir上面再叠加一个可写的容器层。镜像层是只读的所有容器共享容器层是每个容器自己独享的容器删除时容器层跟着销毁。4.2 清理overlay数据的正经姿势热搜词里有docker怎么清理overlay数据我猜问这个问题的人多半是看到了/var/lib/docker/overlay2目录占了几个GB甚至几十GB不知道能不能直接删。先给结论不要手动rm -rf要用docker的清理命令。Docker本身提供了三件套docker system df # 查看磁盘占用分布 docker system prune # 清理悬空镜像、停止的容器、无用网络和构建缓存 docker system prune -a # 更激进删除没有被容器使用的所有镜像执行docker system df输出会分Type、Total、Active、Size、Reclaimed几列一眼能看出什么在占空间。镜像Images占了最多的往往就是你拉的一堆大镜像Build Cache是构建镜像时的中间层缓存这个经常是最占空间的。我遇到过一台CI机器Build Cache占了40多GBdocker system prune一键清了30GB出来。为什么不能直接去/var/lib/docker/overlay2目录里手动删文件夹因为overlay2目录下的文件夹名是一串哈希ID它们之间靠元数据文件关联你根本不知道哪一层属于哪个镜像或容器。删错了可能导致某个镜像无法使用、容器起不来而且Docker内部会认为存储数据不一致比磁盘满还难处理。更安全的做法是如果确实想物理腾空间用docker system prune -a --volumes清理一切不再使用的资源然后重启Docker daemon收尾。如果真的出现了Docker的overlay2数据和daemon记录的元数据不一致这种情况多见于暴力删除目录后最稳妥的办法是彻底重置Docker存储systemctl stop docker rm -rf /var/lib/docker systemctl start docker这一招等于把所有镜像和容器全部格式化代价大但能解决所有存储不一致问题。执行前务必确认没有需要保留的容器数据卷镜像可以从仓库重新拉取。4.3 容器镜像层和qcow2快照的设计同源KVM/QEMU的外部快照和Docker的overlay2存储驱动设计思路高度一致可以互相验证。两者的核心都是只读层可写层层级查找。差别在于维护粒度。qcow2外部快照以簇为单位64KB一个块Docker overlay2以文件为单位合并视图在文件系统层面完成。qcow2的快照是把整个虚拟磁盘作为快照对象粒度粗Docker的镜像层以Dockerfile指令为边界每一层都是独立的可复用对象。这也解释了为什么Docker镜像可以共享——多个镜像如果底层基础镜像一样那基础镜像的层直接被复用不需要重复存储。理解了这层统一性之后你再看容器里的commit容器为镜像操作docker commit container-id my-image:v2这条命令把容器可写层保存成一个新的镜像层——把overlay层固化成一个新的backing file。这恰恰就是qcow2外部快照场景里你把overlay直接拿去当新的基础镜像用的逻辑。一个概念在两个工具链里反复出现说明这套设计已经被大规模生产环境验证过无数次了。5. 常见问题与排查实录那些文档里不会写的事5.1 qemu报错Could not open backing file这是外部快照场景最常见的故障原因是overlay文件头里记录的backing file路径找不到了。症状是虚拟机启动失败错误信息类似qemu-system-x86_64: -drive file/vms/instance01.qcow2: Could not open backing file: Could not open /templates/ubuntu-base.qcow2: No such file or directory排查步骤很简单。先用qemu-img info看overlay文件头qemu-img info /vms/instance01.qcow2输出里的backing file那一行会显示记录的路径。如果路径和实际位置对不上用rebase修正qemu-img rebase -u -b /new/path/ubuntu-base.qcow2 /vms/instance01.qcow2这里的-u参数很关键它让rebase只更新文件头里的路径不对底层数据做任何合并因此瞬间完成。如果你忘了加-urebase会尝试把整个overlay的数据和旧backing file做比对合并耗时可能长达几小时还会因为找不到旧文件直接报错。还有一种情况backing file路径没变但文件本身被替换成了同名的新镜像。这种情况比路径问题更危险因为rebase也救不了——新镜像的数据和旧镜像可能完全不同但overlay里引用的是旧数据的坐标。遇到这种情况老老实实新建overlay重新部署不要心存侥幸。5.2 overlay文件越写越大这是特性不是bug很多人看到overlay文件膨胀就开始慌担心是不是数据写坏了。实际上overlay变大是正常的。你每次修改一个文件修改后的整个簇都会复制到overlay文件删掉了overlay里占的簇也不会自动释放。所以overlay的体积只会单调递增。我见过一个极端案例一台虚拟机长期跑数据库基础镜像只有2GBoverlay撑到了40GB。这个不算异常。如果想要让overlay瘦身办法是利用qcow2的discard或者fstrim让guest里的空间回收动作传递到宿主机上的qcow2文件。但注意fstrim只对未分配块有作用对于overlay上已存在的、被标记为删除的数据也不一定能完全回收。如果一定要压缩overlay官方做法是把overlay里的数据导出到一个全新的qcow2qemu-img convert -f qcow2 -O qcow2 /vms/instance01.qcow2 /vms/instance01-compact.qcow2convert会按当前实际分配的数据重建一个干净的镜像文件未分配的块包括曾经写过后来删除的不会进入新文件。转换完了把新文件替换旧文件即可。这是物理层面的瘦身效果最显著。还有一点要注意convert出来的新镜像默认不会再带上backing file信息。如果你希望它继续依赖基础镜像要加-B参数qemu-img convert -f qcow2 -O qcow2 -B /templates/ubuntu-base.qcow2 /vms/instance01.qcow2 /vms/instance01-compact.qcow25.3 [drm] failed to init overlay plane和存储overlay无关把[drm] failed to init overlay plane cluster0-win1这条热搜词一并说一下因为它极具迷惑性。这个报错来自Linux内核的DRM显示子系统意思是显卡驱动初始化显示合成层overlay plane失败。它跟磁盘镜像存储半毛钱关系都没有但它确实叫overlay plane。调试思路应该转向显卡驱动、内核模块和显示服务器方向而不是去查qcow2。同样warning l16: uncalled segment, ignored for overlay process是汇编器或链接器的警告出现在嵌入式开发里表示某个代码段没有被调用因此不参与覆盖段处理。也是另一个领域的overlay。这提醒我们当报错信息里出现overlay时先想想上下文是什么——是存储、显示、还是代码链接——不要一上来就对着镜像文件猛查。5.4 Docker容器数据卷和镜像层的清理边界清理Docker overlay2数据时要分清边界。镜像层可清理容器层可清理但命名数据卷不要随便删。数据卷volume存放在/var/lib/docker/volumes/下独立于overlay2目录直接删会导致业务数据永久丢失。我用一条命令区分哪些资源能清docker system df -v这个命令会列出每个资源的详细占用和是否被引用。你重点关注Images和Build Cache两部分的Reclaimed大小这两个是最安全的清理对象。Local Volumes除非你明确知道对应业务已经下线否则跳过。还有一个很多人不知道的细节docker system prune默认不清理未被使用但带名字的数据卷。如果你要全清必须显式加--volumes参数。而docker system prune -a --volumes全敲下去效果相当于把本机Docker重置到几乎全新状态——凡是没在运行的容器的数据、镜像、网络、缓存、数据卷全部消失。这条命令我用过不少次每次执行前都会再三确认有没有-a和--volumes。5.5 快照链断裂后的数据恢复尝试最后讲一个灾后现场。有一次我把基础镜像从/templates目录挪到一个新的存储池但忘记同步更新一批overlay的backing file路径。结果这批虚拟机全部失联。我的恢复思路是先尝试rebase -u修正路径。但这要求新路径下的文件和原文件完全一样。我犯的错误是我把ubuntu-base.qcow2连同它的多个快照子文件一起拷到了新目录原文件本身的inode和内容虽然没有变但rebase -u仍然失败了——因为rebase检查的是旧backing file是否存在旧路径已经不存在了它拒绝执行。这时我用了一个绕法先在原路径建一个软链接指到新路径让rebase -u认为旧路径存在从而允许更新文件头ln -s /new/pool/ubuntu-base.qcow2 /old/path/ubuntu-base.qcow2 qemu-img rebase -u -b /new/pool/ubuntu-base.qcow2 /vms/instance01.qcow2执行成功后再把软链接删掉。虚拟机全部恢复。这个方法不保证每次都成功但如果你的backing file只是搬家没有改动内容值得一试。6. 我在实际项目里沉淀的几条经验做这套backing file和overlay方案这几年最有价值的体会是快照链不是越深越好省空间也不是唯一目标最重要的永远是数据的可恢复性。第一基础镜像一旦打磨完立刻只读。权限设成只读目录位置固定坚决不允许任何人直接在基础镜像上做修改。如果基础镜像需要升级不要在原有文件上动而是新建一个基础镜像然后用rebase把overlay切到新镜像上。这套流程虽然多几步但每一步都可回滚不会出现底层被写坏全部overlay跟着遭殃的连环事故。第二overlay文件一定要放在和backing file不同的目录并且定期做备份。backing file是模板可以随时从模板库恢复overlay是业务数据丢了你只在工具层面的工作就全白做了。我会用rsync定期把overlay文件同步到备份服务器执行前先qemu-img snapshot做一次内部快照夹心保护。第三别贪图省空间把手伸到脏数据上去。qcow2之所以设计成稀疏文件就是为了让空间按需分配。你非要手动开预分配、非要频繁convert紧凑都会带来不必要的IO开销和操作风险。该省的地方是归档已下线虚拟机的overlay——归档后及时清理原文件这才是安全的腾挪空间方式。当初我刚接触qemu-img create -b这条命令时只觉得套娃很好玩一层一层的镜像像俄罗斯方块一样叠上去。后来踩过镜像污染、踩过软链接绕路、踩过docker prune误删才真正理解只读层可写层这个组合有多聪明又有多少需要敬畏的地方。你在自己的环境里照着上面的命令走一遍就会明白我为什么反复强调基础镜像必须只读、快照深度必须克制、清理之前必须看清引用这三句话。这三句话是我能给你的最实在的经验。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑