qcow2增量卷和全量卷实现原理
结论
qcow2 全量卷是数据自包含的独立镜像;增量卷(overlay)通过头部 backing_file 指向一个只读基础卷,自身只为"被写过的 cluster"分配物理空间,未写过的部分运行时回退到 base 读,写时按 cluster 粒度 COW 把整簇拉到 overlay 再改。overlay 不是字节级差异盘,而是"整簇副本"盘。
基准:QEMU qemu-img 7.x / qcow2 v3 / Linux KVM。
一、两种卷怎么创建
全量卷
# 10G 独立 qcow2,thin provisioning,物理空间随写入增长
qemu-img create -f qcow2 base.qcow2 10G
# backing file 行为 "No backing file"
qemu-img info base.qcow2
没有任何外部依赖,可独立启动 VM。
增量卷
# 基于 base.qcow2 建一个只存差异的 overlay
# -b 指定 backing file,-F 指定 backing file 的格式
qemu-img create -f qcow2 -b base.qcow2 -F qcow2 overlay.qcow2
# 查看整条 backing chain(base <- overlay)
qemu-img info --backing-chain overlay.qcow2
# 看每个逻辑簇当前落在哪个文件(forward=overlay, backing=base)
qemu-img map overlay.qcow2
overlay.qcow2 本身只有几百 K(存 header + 空 L1/L2 表)。启动 VM 时实际打开的是 overlay,QEMU 自动把 base 挂成只读。base 不能删、不能移动路径,否则 overlay 起不来。
可多层叠加:base <- snap1 <- snap2 <- live,就是 KubeVirt/libvirt 快照和 PVC 克隆的底层机制。
二、内部结构
每个 qcow2 文件都是三层索引:header → L1 表 → L2 表 → data cluster(默认 64K)。
- header 里有
backing_file_offset/size,存基础卷路径(相对或绝对)。 - L1 entry → 一个 L2 表的偏移;L2 entry → 一个 data cluster 的偏移。
- L2 entry == 0 表示这个簇在本文件未分配。
三、读流程
给定一个逻辑扇区 offset:
- 查 overlay 的 L1 → L2;
- L2 entry 非 0 → 直接从 overlay 的 data cluster 读;
- L2 entry == 0 → 沿
backing_file跳到 base,再走一遍 base 的 L1 → L2,从 base 的 cluster 读。
读过的扇区不自动复制到 overlay。 这是 overlay 文件能保持很小的关键。
四、写流程(COW 的关键)
- 查 overlay 的 L1 → L2;
- 该簇已在 overlay 分配 → 直接覆盖写;
- 未分配(L2 entry == 0):
- 从 base 把整个 64K cluster 读出来;
- 在 overlay 新分配一个 cluster,把读出的整簇写进去;
- 在 overlay 的 L2 表登记新簇偏移;
- 用本次新数据覆盖这个新簇。
- base 里的原数据不动。
按 cluster(不是按扇区)COW,是因为 L2 表一项只能对应一个簇,做不到"同簇一半指向 base、一半指向 overlay"。整簇拉过来是为了避免部分写造成数据撕裂。
五、"只存被改过的 cluster"这句话要怎么理解
这句话有歧义,容易被误读成 bsdiff 那种字节级差异。实际机制是:
- overlay 只为"被写过的簇"分配物理空间,没写过的不分配、回退 base;
- 但一旦某个簇被写过哪怕 1 个扇区,整个 64K 簇都会被复制到 overlay,然后再用新数据覆盖。
overlay 里存的是"整簇副本",不是"差异块"。一个簇里 99% 的内容可能和 base 一模一样,但它已经在 overlay 里占了 64K。
由此带来两个实际影响:
- overlay 会膨胀:VM 随机写多了(哪怕每个簇只写 1 个扇区),overlay 物理大小会很快接近 base。
qemu-img info看的 virtual size 是 10G,du -h overlay.qcow2看到的 actual size 才是真占的空间。 - 首次写有读放大:每个簇第一次写都要先从 base 读 64K、再写到 overlay(一次读 + 一次写)。
六、启动 VM 用全量卷还是增量卷
选型看场景,没有绝对优劣:
| 场景 | 推荐 | 原因 |
|---|---|---|
| 单台长期跑、对性能敏感 | 全量卷 | 没有 backing chain,读不穿透、写无 COW 开销 |
| 从同一个母盘起 N 台 VM(云桌面、CI、弹性实例) | 增量卷(overlay) | 共享 base,秒级启动、省空间 |
| 母盘是公版/企业标准镜像 | 增量卷 | base 只读,VM 改不到母盘 |
| 需要频繁回滚 | 增量卷 + 快照 | 切回 base 或切 L1 指针,秒级完成 |
简单原则:一台 VM 用全量卷性能最好;多台 VM 共享一个母盘用 overlay 省空间。base 一旦作为 backing file 就保持只读,不要在上面直接写。
七、如何实现快照
qcow2 快照分两种,和 overlay、clone 的关系先理清:clone / overlay 是磁盘组织方式,snapshot 是"拍一个时间点"这个动作。外部快照的实现本质就是"转 base + 建 overlay"。
内部快照(internal)
快照存在同一个 qcow2 文件内部,不生成新文件:
qemu-img snapshot -c before-install disk.qcow2 # 创建
qemu-img snapshot -l disk.qcow2 # 列表
qemu-img snapshot -a before-install disk.qcow2 # 回滚
qemu-img snapshot -d before-install disk.qcow2 # 删除
原理:qcow2 文件头的 snapshot table 里只记一个"该时刻的 L1 表指针",回滚就是把当前 L1 指针切过去,不复制数据,所以创建快、占空间小。但所有快照堆在一个文件里,快照多了性能下降,生产不推荐。
外部快照(external,= overlay)
拍快照那一刻做三件事:
- 当前可写盘转成只读 base(它就是那个时间点);
- 新建空的 snap.qcow2,
backing_file指向旧盘; - VM 继续写,全部落到 snap.qcow2。
拍快照前: VM ──写──> disk.qcow2(可写)
拍快照后: disk.qcow2(只读,时间点)
VM ──写──> snap.qcow2(backing → disk.qcow2)
libvirt / KubeVirt 的快照默认走外部快照,性能好、base 和快照解耦。
三种方式对比
| clone(完整克隆) | 内部快照 | 外部快照(overlay) | |
|---|---|---|---|
| 新建文件 | 完整拷贝一份 | 不建文件 | 空 overlay |
| 创建速度 | 慢(全量拷贝) | 快(只记元数据) | 快(只建空 overlay) |
| 空间占用 | 双倍 | 几乎为零 | 只有差异 |
| 能否回滚 | 不能(两份独立) | 切 L1 指针 | 切回 base 即可 |
| 生产推荐 | 简单隔离场景 | 不推荐 | 推荐 |
八、常用运维命令
| 目的 | 命令 | 作用 |
|---|---|---|
| 增量合并回 base | qemu-img commit overlay.qcow2 |
把 overlay 的整簇差异写回 base,之后 overlay 可删 |
| 切换 base | qemu-img rebase -b new-base.qcow2 overlay.qcow2 |
base 升级/换盘后让 overlay 指向新 base |
| 压成独立全量 | qemu-img convert -O qcow2 overlay.qcow2 full.qcow2 |
整条 chain 拍平成一个自包含镜像 |
| 链完整性检查 | qemu-img check overlay.qcow2 |
验证 backing file 可达、无损坏 |
| 内部快照 | qemu-img snapshot -c name overlay.qcow2 |
qcow2 内置 A/B/C 状态(不推荐,性能差) |
九、cluster size 调参
创建时可以改 cluster size(默认 64K):
# 大块:元数据少、顺序写好,但 COW 浪费大(写 1 个扇区也要拉 2M)
qemu-img create -f qcow2 -o cluster_size=2M base.qcow2 10G
# 小块:COW 浪费少,但 L2 表变大、元数据 IO 增多
qemu-img create -f qcow2 -o cluster_size=32K base.qcow2 10G
大块适合顺序写、数据库类负载;小块适合随机小块写、桌面类负载。
十、libvirt / KubeVirt 里的挂载方式
<disk type='file' device='disk'>
<driver name='qemu' type='qcow2'/>
<source file='/var/lib/libvirt/images/overlay.qcow2'/>
<backingStore type='file' index='1'>
<source file='/var/lib/libvirt/images/base.qcow2'/>
<format type='qcow2'/>
</backingStore>
</disk>
KubeVirt 给 VM 配磁盘有两种典型方式:差分盘(overlay)是把基础镜像作为只读 base,每个 VM 一个 overlay PVC,只写差异,VM 销毁只删 overlay、base 不动;完整克隆(clone)则是把基础镜像整个拷贝一份给新 VM,VM 拿到独立副本,改的是自己那份,不影响 base。
浙公网安备 33010602011771号