qcow2 快照、扩容缩容与压缩:镜像维护的安全边界实测
本篇是入门路线第 04 篇,讨论的对象是「硬盘文件」的维护动作。还不清楚镜像和快照是什么的读者,先读第 01 篇 认识 QEMU:从零看懂虚拟机、镜像与快照 的名词表,再回来动手。
第 02 篇 QEMU 安装与第一个 RISC-V 虚拟平台 跑完启动横幅后,用 qemu-img create 留下了一块 qcow2 磁盘。镜像一旦装了系统、跑过实验,维护问题马上跟着来:磁盘建小了想扩容、想在升级前留一个回滚点、镜像文件太大想压缩传输。这三件事分别对应 qemu-img resize、qemu-img snapshot 和 qemu-img convert -c,本篇用一组在 QEMU 11.1.2 上真实跑过的实验,把它们的机制和安全边界一次讲透。建盘、info 与格式互转的基础操作在 qemu-img 用法:建盘、查真实占用与格式转换 里展开,本篇专注镜像生命周期里的「变更」操作。
本文要回答的问题
resize扩容一条命令就返回,它到底改了镜像里的什么东西?为什么扩容后真实占用几乎不增长?- 缩容为什么必须显式加
--shrink,不加会发生什么?数据写在高偏移位置时强行缩容有什么后果? snapshot -c打出来的 qcow2 快照存在哪里,它和 backing file 组成的外部增量链是同一套机制吗?convert -c的压缩按什么粒度进行,为什么 24MiB 的镜像能被压到 352KiB?压缩会不会改动源镜像?- 这三组操作各自的安全前提是什么,哪些动作必须在虚拟机关机后做?
下图是本篇实验的完整路径:先建一块 32MiB 的小盘写入数据建立基线,再做两个方向的 resize,然后打两个内部快照并删除其中一个,最后用同一份高重复数据对照普通转换与压缩转换的体积差。
一、先建立基线:两个尺寸必须分清
讨论任何变更操作之前,先要把 qcow2 的两个容量数字钉死,后面所有现象都靠它们解释。qemu-img info 输出里的 virtual size 是 Guest 操作系统看到的盘容量,也就是虚拟盘自己声明的几何尺寸;disk size 是这个镜像文件在宿主机文件系统上的真实占用。qcow2 是稀疏分配格式:Guest 从没写过的区域不占宿主空间,只有真正写入的数据簇(cluster,本篇镜像的簇大小为 65536 字节,即 64KiB)才会落地。
实验第一步建一块 32MiB 的空盘,再用 qemu-io 从偏移 0 写入 8MiB 的固定模式数据(-P 0x55 表示每个字节填 0x55),然后用 du -h 和 info 对照两个尺寸。
读这段输出抓住三个数字。建盘时格式化横幅写明 cluster_size=65536、size=33554432,这就是 64KiB 簇与 32MiB 虚拟容量的来历。8MiB 写入完成后,virtual size: 32 MiB 纹丝不动,而 disk size: 8.31 MiB——比 8MiB 略大,多出的约 0.31MiB 是 qcow2 头部、L1/L2 映射表和引用计数表等元数据开销。du 给出的宿主占用同样是 8.3M,与 info 完全对得上。记住这个基线:数据簇按写入落地,元数据另算一份小开销,后面 resize 和压缩的现象都回到这条规则上解释。
二、resize 扩容:改的只是头部里的容量字段
先做扩容:qemu-img resize disk.qcow2 +16M 把虚拟容量从 32MiB 抬到 48MiB,紧接着再用 resize --shrink ... -8M 缩到 40MiB,每次操作后都用 info 复核。
两次 Image resized. 之后最值得注意的现象是:disk size 始终是 8.31 MiB,一次都没变过,变的只有 virtual size(32 MiB → 48 MiB → 40 MiB)。这揭示了扩容的机制:resize 并不搬移任何数据簇,它只改写 qcow2 头部里记录虚拟盘几何尺寸的字段,并按需增补对应容量范围所需的映射表条目。新增的 16MiB 范围对 Guest 表现为一片从未写过的空白区域,按照稀疏分配规则,没有写入就没有数据簇落地,宿主占用自然不增长。等 Guest 将来真的往新区域写数据,簇才会按需分配,这和第一节建盘后的写入行为完全一致。
这个机制带来两个直接结论。其一,扩容是近乎零成本的元数据操作,速度与扩容幅度无关,给一块盘加几百 GB 也是瞬间返回。其二,扩容本身不负责让 Guest 用上新空间——Guest 内核看到的块设备变大了,但分区表、文件系统并不知道,还需要在 Guest 内扩分区、扩文件系统(如 growpart 加 resize2fs)才能真正使用新增容量,这是线上扩容最常见的遗漏点。
三、resize 缩容:--shrink 保险栓与高偏移数据风险
缩容命令必须带 --shrink,这不是可选项而是一道刻意设计的保险栓:不带它直接给负增量,qemu-img 会拒绝执行并提示缩容需要显式授权。原因在于缩容是真正可能丢数据的操作,而扩容不会。
要理解风险,得结合 qcow2 的寻址方式看。Guest 的每个虚拟偏移通过 L1/L2 两级映射表找到宿主文件里的数据簇,映射关系只认偏移、不认文件系统语义。缩容把虚拟容量从 48MiB 砍到 40MiB,意味着新容量边界之外的映射条目和数据簇被判定为「盘外」而废弃。如果 Guest 曾在 40MiB 以后的偏移写过有效数据——比如某个文件恰好落在磁盘尾部——这些簇在缩容后不可访问,数据等于被静默截断。qemu-img 无法读懂 Guest 内的文件系统,不知道高偏移处存的是有效文件还是空白,所以它把判断责任交给操作者:你必须先在 Guest 内确认高偏移区域没有数据(必要时先缩文件系统、迁移尾部数据),再显式加上 --shrink 确认。
本篇实验里缩容是安全的:此前只在偏移 0 起写过 8MiB、在 8MiB 起写过 4MiB,全部数据都远低于 40MiB 的新边界,所以两次 info 里 disk size 保持 8.31 MiB,没有任何簇被丢弃。工程上的稳妥顺序永远是「先缩 Guest 内文件系统并验证,再缩块设备」,与扩容的顺序正好相反。另外两个操作纪律:resize 要求镜像未被运行中的虚拟机占用,对带内部快照的镜像做 resize 还可能因快照保留旧几何状态而被拒绝,变更前先关机、必要时先删快照。
四、内部快照:snapshot 操作的是簇引用状态
进入本篇主角 qcow2 快照。实验在当前镜像上打第一个快照点 snap1,然后在偏移 8MiB 处写入 4MiB 的 0xAA 覆盖旧数据,再打第二个快照点 snap2,列出快照清单,最后删除 snap1 再列一次。
清单里每个快照点有 ID、TAG、日期等字段,VM_SIZE 为 0 是因为实验在关机态的镜像上操作、没有虚拟机内存状态需要保存。要读懂这套机制,关键是理解 qcow2 内部快照保存的不是「整块盘的复制件」,而是打快照那一刻的簇引用状态:L1 映射表被复制一份挂到快照表里,同时相关数据簇的引用计数(refcount)加一。下图画出打快照后再写入时,簇层面发生的变化。
读这张图抓住「蓝色数据、紫色快照元数据、橙色新簇」三条语义线。打快照的瞬间并不复制任何数据簇,紫色的快照表条目只存一份 L1 副本并给蓝色旧簇的引用计数加一,所以打快照极快、体积代价很小。随后 0xAA 覆盖写入时,qcow2 发现目标簇的引用计数大于 1——除了当前映射还有快照在引用它——于是写时复制(Copy-On-Write)生效:分配橙色的新簇 C 存放新数据,当前映射改指向 C,而蓝色旧簇 A 被原样保留下来。这样 snap2 记录的是覆盖后的状态,回滚到 snap1 时只要把 L1 恢复成快照表里保存的副本,Guest 看到的就又是整片 0x55 的旧世界。删除 snap1 则反向操作:删掉快照表条目、把它独占引用的簇计数减一,计数归零的簇才允许回收。这也解释了为什么长期不删快照的镜像会越长越大——所有被覆盖过的旧簇都被快照钉住无法释放。
五、内部快照与 backing file 外部链不是一回事
qcow2 生态里还有另一种也叫「快照/增量」的机制:backing file 外部链,qcow2 backing file 与快速克隆 专门展开它的建链与修复。两者都靠簇引用和写时复制工作,但形态、适用场景完全不同,混为一谈是线上事故的常见来源,这里用一张表把 qcow2 快照的两种实现路线分清。
| 对比维度 | 内部快照(本篇 snapshot -c) | backing file 外部链 |
|---|---|---|
| 存储形态 | 快照点与数据同在一个 qcow2 文件内 | 基础镜像与多层 overlay 是各自独立的文件 |
| 管理命令 | qemu-img snapshot -c/-l/-d/-a | create -b 建链、info 查链、rebase 换链 |
| 回滚方式 | -a 在同一文件内恢复映射状态 | 切换或丢弃 overlay 文件,基础镜像保持只读 |
| 可移植性 | 单文件即可拷走,自包含 | 必须连同链上所有文件一起迁移,断链即损坏 |
| 典型场景 | 单机升级前留回滚点、实验状态存档 | 一台基础机快速克隆出多台虚拟机、模板分发 |
| 风险点 | 快照堆积撑大单文件、删错快照点 | 基础镜像被改写会污染全部下游 overlay |
一句话区分:内部快照是「一个文件里的多个时间点」,backing 链是「多个文件组成的一条写时复制链」。前者方便但共享性差,后者省空间、克隆快但依赖链完整性。理解了本篇第四节的簇引用与引用计数,再看 backing 链会发现底层原语是同一套,只是元数据摆放位置不同。
六、convert -c:按簇 zlib 压缩,24MiB 到 352KiB
最后一个实验回答压缩问题。新建一块 32MiB 镜像,写入 24MiB 的 0x55 高重复数据,然后做两次转换:一次普通 convert -O qcow2,一次加 -c 的压缩转换,用 du 对照三个文件的真实占用,再对压缩产物跑 info。
+ /usr/local/bin/qemu-img create -f qcow2 src.qcow2 32M
Formatting 'src.qcow2', fmt=qcow2 cluster_size=65536 ... size=33554432 ...
+ /usr/local/bin/qemu-io -c write -P 0x55 0 24M src.qcow2
wrote 25165824/25165824 bytes at offset 0
24 MiB, 1 ops; 00.01 sec (1.796 GiB/sec and 76.6166 ops/sec)
+ /usr/local/bin/qemu-img convert -O qcow2 src.qcow2 plain.qcow2
+ /usr/local/bin/qemu-img convert -c -O qcow2 src.qcow2 compressed.qcow2
+ du -h src.qcow2 plain.qcow2 compressed.qcow2
24M src.qcow2
24M plain.qcow2
352K compressed.qcow2
+ /usr/local/bin/qemu-img info compressed.qcow2
virtual size: 32 MiB (33554432 bytes)
disk size: 352 KiB
compression type: zlib三组数字摆在一起,压缩效果非常直观。
| 文件 | 虚拟容量 | 宿主真实占用 | 说明 |
|---|---|---|---|
| src.qcow2 | 32 MiB | 24 MiB | 原始镜像,24MiB 数据簇未压缩 |
| plain.qcow2 | 32 MiB | 24 MiB | 普通转换,逐簇搬运,体积不变 |
| compressed.qcow2 | 32 MiB | 352 KiB | convert -c 按簇 zlib 压缩后的产物 |

图中 du 三行是整个实验的落点:源镜像和普通转换产物并排占着 24M,而压缩产物缩成 352K;紧接着的 info 输出说明它的 virtual size 仍是 32 MiB——容量契约没变,变的只是宿主上的簇落地方式,compression type: zlib 一行标明了压缩算法。
24MiB 压到 352KiB,约为原体积的 1.4%,这个夸张的比例来自实验数据的极端特性:每个字节都是同一个 0x55,zlib 对这种高重复模式的压缩率极高。真实镜像里混合了日志、二进制程序和已删除痕迹,压缩率通常在 50% 到 80% 之间,不要把实验峰值当成日常预期。机制上有三个要点必须讲清。第一,压缩粒度是单个簇:qcow2 对每个 64KiB 簇独立做 zlib 压缩后落地,L2 表项标记该簇为压缩态,读取时即时解压,因此压缩镜像可以正常挂载、正常启动,代价是读路径多一份 CPU 开销。第二,压缩是只读转换:convert 读取源镜像、写出一个全新文件,全程不修改 src.qcow2 的一个字节,实验后源文件仍是 24MiB 就是证据;不满意压缩产物直接删掉新文件即可,这也是它比任何原地操作都安全的原因。第三,已压缩簇被 Guest 重新写入时会按普通簇重新分配,压缩态不会自动保持,想重新收紧体积需要再跑一次 convert -c,所以压缩最适合归档、传输这类只读分发场景,而不是日常运行的活动镜像。
这一篇在知识体系里的位置
本篇是 qcow2 镜像管理三件套的「变更操作」篇,与另外两篇构成完整闭环:qemu-img 用法:建盘、查真实占用与格式转换 解决「镜像怎么建、两个尺寸怎么读、raw 与 qcow2 怎么互转」,是本篇所有 info 输出的读图基础;qcow2 backing file 与快速克隆 把本篇第四节的簇引用原语延伸到多文件外部链,讲清模板化克隆与断链修复。往上游追原理,qcow2 的写时复制与缓存系列里的假共享与缓存行状态同属「引用状态决定数据去留」的思路;往应用方向走,后续文章会把维护好的镜像挂到 virtio 总线上,在 virt 虚拟平台里完成 OpenSBI 到 Linux 的完整启动链,环境准备见 QEMU 安装与第一个 RISC-V 虚拟平台。
代码获取:本篇全部实验的可复现脚本与说明见公开仓库 derekzhuo/geek-doc.cn 的
experiments/qemu-qcow2-basics/。克隆后在该目录执行./02-resize-snapshot-compress.sh即可复现建盘、双向 resize、内部快照增删查与压缩对照的全部输出,产物落在脚本自身的output/02/下,可随时重跑。
实验环境
- 机器/系统:MacBook Pro(Intel x86_64)/ macOS 26
- 工具版本:qemu-img 11.1.2、qemu-io 11.1.2(QEMU 11.1.2 Homebrew 安装,路径
/usr/local/bin/qemu-img) - 日期:2026-11-07
- 完整命令:
qemu-img create -f qcow2 disk.qcow2 32M→qemu-io -c write -P 0x55 0 8M disk.qcow2→qemu-img resize disk.qcow2 +16M→qemu-img resize --shrink disk.qcow2 -8M→qemu-img snapshot -c snap1/snap2、-l、-d snap1→qemu-img convert -O qcow2与convert -c -O qcow2对照,完整序列与原始输出见公开仓库experiments/qemu-qcow2-basics/02-resize-snapshot-compress.sh
总结
qcow2 镜像的三类变更操作共享同一套簇映射底层,却各有安全边界:resize 扩容只改写头部容量字段、不分配数据簇,所以瞬间完成且不增占用,真正用上新空间还要在 Guest 内扩分区和文件系统;缩容因为会截断新边界之外的高偏移数据,必须用 --shrink 显式确认,顺序上永远先缩 Guest 文件系统再缩块设备。qcow2 快照的内部快照保存的是 L1 映射副本与簇引用计数,覆盖写入触发写时复制、旧簇留给回滚,它和 backing file 外部链是同一原语的两种形态——一个文件里的多个时间点,对多个文件组成的增量链。convert -c 按簇做 zlib 压缩且只读不动源文件,实测把 24MiB 高重复镜像压到 352KiB,适合归档传输而不适合活动运行镜像;三组操作都应在虚拟机关机态执行,变更前留好回滚点,是镜像维护不变的纪律。