KVM qcow2 磁盘与快照:空间管理和回滚策略

介绍 qcow2、内部快照和外部快照的使用边界,避免快照链过长导致性能与空间风险。

技术文章约 22 分钟163 次阅读

这篇文章要解决一个很具体的运维问题:KVM 虚拟机用 qcow2 做磁盘时,空间该怎么算、快照该怎么建、回滚该怎么做,以及为什么"删除快照链中间的一层""对运行中的虚拟机直接 cp 磁盘文件"这类看起来很自然的操作会失败、甚至毁掉数据。要讲清楚这些问题,只需要抓住一个最基础的事实:qcow2 不是把一块虚拟磁盘原样摊平存成一个"大文件 + 固定偏移",而是把虚拟磁盘切成大量固定大小的簇(cluster),再用两级表(L1/L2)把虚拟地址映射到文件内部的物理位置;快照、写时复制(COW)、合并、回滚,本质都是这一张地址映射表和每个簇上的引用计数在不同时间点的不同状态。

#原理层:qcow2 是怎么把一块磁盘装进一个文件

#簇:分配与寻址的基本单位

qcow2 文件开头是一个 header,记录虚拟磁盘大小、簇大小、backing file 路径、引用计数(refcount)配置等元数据。真正有意义的是"簇"这个概念:qcow2 把虚拟磁盘的地址空间等分成大小相同的簇,默认簇大小是 64 KiB(合法取值是 512 B 到 2 MiB 之间的 2 的幂)。数据不再按"扇区号 = 文件偏移"直存,而是按簇为单位、在文件里任意位置摆放,通过表结构找到它。这样做换来三样东西:按需分配(没写过的簇根本不占空间)、支持压缩、支持快照——代价是每次读数据要先查表。

#L1/L2 两级表:用很小的索引覆盖很大的磁盘

为什么不用一张大索引表?表会太大。qcow2 的折中方案是两级表:

  • L1 表:每个表项 8 字节,指向一张 L2 表 在文件中的偏移;
  • L2 表:每张 L2 表有 簇大小 / 8 个表项(默认 64 KiB 簇时是 8192 项),每个表项指向一个数据簇的物理偏移,并携带标志位(是否写时复制、是否为全零簇、是否压缩);
  • 引用计数表与引用计数块:记录每个簇被引用了几次,这是写时复制能否"原地改写"的判断依据。

算一下数字。默认 64 KiB 簇时,一张 L2 表有 8192 个表项,每项覆盖一个簇,所以一张 L2 表能索引 8192 × 64 KiB = 512 MiB 的虚拟空间。一块 40 GiB 的虚拟盘需要 40960 / 512 = 80 张 L2 表,L1 表只需要 80 个 8 字节表项——本身只有 640 字节。这就是两级表的意义:L1 表小得可以长期驻留缓存,L2 表按需加载。

读取路径:虚拟偏移除以簇大小得到虚拟簇号,高 13 位索引 L1 表找到 L2 表,低 13 位索引 L2 表得到物理簇偏移,然后读数据。写入路径更复杂,因为要先处理"这个簇现在被谁共享",见下文。

#按需分配、稀疏与空洞

qcow2 是**按需分配(thin provisioning)**的:用 size=40G 创建的空镜像,文件实际只有几百 KB——header、引用计数表、空的 L1 表;首次写入某个簇时才在文件尾部追加并登记进 L2 表。查看真实占用用两条命令对比:

du -h /var/lib/libvirt/images/web01.qcow2
qemu-img info /var/lib/libvirt/images/web01.qcow2
# 输出里的 virtual size(虚拟大小)和 disk size(实际占用)对比,就是真实占用

这带来两个必须记住的风险:

  1. 宿主机磁盘可能被撑爆:20 台虚拟机各声明 40G,宿主机只有 200G,客机们都写满就是磁盘满的事故(虚拟化宿主机上这是灾难级的连锁故障)。规划时按"虚拟 size 总和"和"物理磁盘实际容量"两个维度都留余量,并做容量监控。
  2. 空洞不会自己回来:在客机里删除文件,qcow2 通常不会自动收缩,du 看到的空间不会回来。原因:客机文件系统删文件只改自己的元数据(把块标记为空闲),不会主动通知 qcow2"这个簇可以释放",除非走 discard/TRIM 通道(见"边界与坑")。瘦身需要手动 qemu-img convert 重建镜像。

#写时复制:共享簇如何被拆开

每个簇都有一个引用计数。L1/L2 表项里有一个"COPIED"标志位,含义是"这个簇当前只被引用一次、归我独占"。独占时写操作可以原地改写;一旦引用计数大于 1(比如被某个快照也引用了),写操作就不能动原簇,否则快照里的旧数据就被破坏了。此时 qcow2 的处理是:

  1. 分配一个新簇;
  2. 把旧簇内容拷进新簇(可按写入长度做部分拷贝);
  3. 在新簇上应用这次写入;
  4. 把 L2 表项指向新簇,旧簇引用计数减一,新簇引用计数置一。

这就是写时复制的机制:"复制"的不是全盘,而是"即将被覆盖的那个簇";触发条件是"这个簇有人共享"——内部快照变多后写性能变差、写放大明显,根因都在这里。

#backing file 与镜像链:快照的真正形态

qcow2 的 header 里可以记录一个 backing file 路径。读数据时,如果 L2 表项为空(该簇未分配),qcow2 就沿着 backing file 继续往下查;如果 backing 里也没有,再查它的 backing,直到某个镜像持有这个簇,或者整条链都不含它(此时按全零处理)。写数据时则永远只写在当前层:在顶层分配新簇并登记,不动父层。

于是"当前镜像 = 一个只记录变化的增量层"这个性质就顺理成章了。一张磁盘 + 它的 backing,就构成一条镜像链(backing chain):

base.qcow2(只读基盘,装好系统与常用软件)
   └── web01.qcow2(覆盖层,只记录 web01 这台机器的增量)

外部快照的本质就是在链顶再加一层增量文件,把原顶层降为只读基盘。沿链读取即可得到任一时刻的完整视图。

#内部快照:同一文件里的元数据视图

与外部快照不同,内部快照不创建新文件,而是把快照"视图"存在同一个 qcow2 文件里。qcow2 的 header 里有一个快照表,每个内部快照保存一份当时的 L1 表(以及虚拟机内存态信息等可选数据)。机制上,快照创建时并不复制任何数据簇,只是:

  • 记下当前 L1 表的一份拷贝(相当于把"现在的磁盘视图"冻结);
  • 从此刻起,凡是会被新写入触达的簇,引用计数都变成大于 1,写入触发 COW,分配新簇。

所以内部快照成本是渐进的:创建瞬间几乎不花钱,之后每次新写入都要为保护快照多付一次分配与拷贝,改动越大文件长得越快。回滚内部快照 = 把活动 L1 表换回快照里保存的那一份,同时修正引用计数;删除快照 = 释放掉"只有这个快照引用"的簇,保留"多个快照共享"的簇。

#为什么快照是增量的、为什么中间层不能删

现在回答两个高频问题。

为什么快照是增量? 因为快照本质上只保存"元数据视图 + 之后的差异"。无论是内部快照(同一文件内冻结一份 L1 表)还是外部快照(链顶端新加一个覆盖文件),快照本身都不包含完整数据拷贝,它包含的是"与上一个时间点的差异"。这正是它廉价的原因,也正是"快照不等于备份"的原因——基盘或链上任何一环损坏,快照都跟着完蛋。

为什么不能随意删除中间层? 看这条链:

base.qcow2 ← mid.qcow2 ← top.qcow2

top 未分配的簇,读取时回退到 mid;mid 未分配的簇,回退到 base。如果直接 rm mid.qcow2,top 那些需要从 mid 读取的簇就失去了来源,整条链读不出来,虚拟机启动即失败。删除中间层的正确做法是先合并:要么用 qemu-img rebase 把 top 重新指向 base(并把原来需要从 mid 读的数据拷进 top),要么用 qemu-img commit 把 mid 的内容并进 base,合并完成、验证通过之后,中间层才真正可以删。

#qemu-img 各子命令对镜像链的改变

把机制和命令对上,后面操作就不会迷路:

  • qemu-img create -b base overlay:在链上新增节点(新建覆盖层),写 backing file 指针;
  • qemu-img convert:线性化/扁平化。沿链把完整数据读出来写进一个新文件;默认产出不依赖任何 backing 的独立镜像,也可以用 -B 让它只依赖某个指定基盘;
  • qemu-img snapshot:操作文件内部快照(-c 创建、-l 列出、-a 回退、-d 删除),不改变链结构;
  • qemu-img rebase:修改镜像的 backing 指针。不带 -u 时会"搬运数据"——把旧基盘里这个镜像需要的内容拷进镜像自身,从而允许旧中间层被删除;
  • qemu-img commit:把覆盖层内容并进它的 backing(或 -b 指定的文件),链缩短一层,覆盖层变空;
  • qemu-img check:校验元数据一致性(L1/L2 表项与引用计数是否对得上、有没有泄漏簇),-r all 可尝试修复,不改变链结构。

#操作层:完整可复现的命令序列

#前置条件与版本

以下命令以 Debian 12 / Ubuntu 22.04 / RHEL 9 系为参考环境(原文未指定发行版,这里采用保守表述;命令在主流发行版上均可执行):

QEMU:qemu-img 工具,Debian/Ubuntu 上属于 qemu-utils 包,RHEL 上属于 qemu-img 包;
libvirt:virsh 客户端 + libvirtd 服务,通常与 qemu-kvm 一起安装;
权限:root 或 sudo,libvirt 默认连接 qemu:///system;
磁盘:虚拟机磁盘必须是 qcow2 格式(vda/vdb 等 virtio 盘);
版本:文中命令不依赖过新特性,QEMU 6.x、libvirt 8.x 及以上均可运行;

前置检查:确认 virsh list --all 能看到宿主机上的域,qemu-img --version 有输出。

#第一步:创建带 backing file 的镜像

先在干净环境里安装好一个基础镜像(装系统、配好常用软件,后面每个虚拟机都从它派生)。创建基础镜像:

qemu-img create -f qcow2 /var/lib/libvirt/images/ubuntu-base.qcow2 40G

用 virt-install 往里装一次系统(只需要做一次):

virt-install --name base-install \
  --memory 4096 --vcpus 2 \
  --disk path=/var/lib/libvirt/images/ubuntu-base.qcow2,format=qcow2,bus=virtio \
  --cdrom /path/to/ubuntu-22.04-server.iso \
  --os-variant ubuntu22.04 \
  --network network=default \
  --graphics vnc

装完先别删,打一个"干净基线"(见内部快照一节)再派生。派生时必须用 -F qcow2 声明 backing 格式,否则现代 qemu-img 会警告 "Deprecated: backing file without a format":

qemu-img create -f qcow2 -F qcow2 \
  -b /var/lib/libvirt/images/ubuntu-base.qcow2 \
  /var/lib/libvirt/images/web01.qcow2

再通过 --import 从现有镜像启动 web01:

virt-install --name web01 --import \
  --memory 4096 --vcpus 2 \
  --disk path=/var/lib/libvirt/images/web01.qcow2,format=qcow2,bus=virtio \
  --network network=default \
  --os-variant ubuntu22.04 \
  --noautoconsole

验证派生关系:

qemu-img info --backing-chain /var/lib/libvirt/images/web01.qcow2

--backing-chain 会递归显示 web01 → ubuntu-base 两层。镜像正被运行中的虚拟机占用时,必须加 -U(--force-share),否则 qemu-img 拿不到独占锁会报错:

qemu-img info -U /var/lib/libvirt/images/web01.qcow2

#第二步:内部快照的建立与回滚

适合"操作前临时留一手"的场景,一条命令、随镜像走、管理简单。用 virsh 创建内部快照:

virsh snapshot-create-as web01 before-upgrade --description "升级前"
virsh snapshot-list web01
virsh snapshot-revert web01 before-upgrade
virsh snapshot-delete web01 before-upgrade

细节:对运行中的域执行上面的命令,默认会同时保存内存态(RAM 落盘到 save 目录,snapshot-list 显示为 RAM 类型快照),回滚连内存一起还原;只想要磁盘状态就加 --disk-only(见下一步,那是外部快照语义)。

对应的 qemu-img snapshot 底层命令(操作对象是文件,不经过 libvirt):

qemu-img snapshot -c before-upgrade /var/lib/libvirt/images/web01.qcow2
qemu-img snapshot -l /var/lib/libvirt/images/web01.qcow2
qemu-img snapshot -a before-upgrade /var/lib/libvirt/images/web01.qcow2   # 回滚,需虚拟机停机
qemu-img snapshot -d before-upgrade /var/lib/libvirt/images/web01.qcow2

内部快照的适用边界(原文明确强调的教训):

  • 快照越多,写性能越差:每次写入都可能触发 COW(机制见原理层);
  • 镜像变大后删除内部快照不安全:删除要遍历合并引用计数,大文件上既慢又要求文件系统有余量,命令中断或虚拟机仍在运行时文件可能损坏;
  • 不适合长时间保留,适合"升级前临时留一手"。

所以内部快照的正确定位是短期保险,不是长期备份。

#第三步:外部快照(生产环境主用)

外部快照把状态写到新文件,原镜像变成只读基盘,新写入全部落到新文件。libvirt 的标准做法是 snapshot-create-as --disk-only:

virsh snapshot-create-as web01 before-upgrade \
  --disk-only --atomic \
  --description "升级前外部快照"
virsh snapshot-list --tree web01

libvirt 会在原磁盘同目录生成覆盖文件,并自动把活动盘切到新文件(默认命名规则随版本略有差异,用 virsh snapshot-list 确认)。旧文件随即成为只读基盘——这就是字面意义上的外部快照。

再看原文里的另一条路线 virsh blockcopy:

virsh blockcopy web01 vda /var/lib/libvirt/images/web01.after.qcow2 \
  --wait --pivot --finish

blockcopy 把活动盘整体复制到目标文件并 --pivot 切换过去。与 --disk-only 的差别:blockcopy(不带 --shallow)产出独立完整副本,新活动盘不依赖旧文件;外部快照产出依赖基盘的增量覆盖层。想要"新文件继续引用旧文件作为基盘"的 overlay 语义,加 --shallow;要复用已存在的目标文件,加 --reuse-external(否则目标文件不应预先存在)。--finish 在部分手册里被描述为 --pivot 的隐含行为,同传在常见版本上可执行。

#第四步:停机复制这种"最朴素的备份"

最可靠、最简单的一致性方案是:停机后直接复制 qcow2 文件。

virsh shutdown web01
# 等待域真正落到 shut off 状态(轮询,避免 cp 时还在写盘)
while [ "$(virsh domstate web01)" != "shut off" ]; do sleep 2; done
cp /var/lib/libvirt/images/web01.qcow2 /backup/web01-$(date +%F).qcow2
virsh start web01

为什么强调"停机后"?因为纯 cp 正在被写入的文件,等于复制一个中途状态的镜像——数据库的页可能只写了一半、文件系统 journal 可能不完整,备份拿到手里的是一份损坏的数据。数据库类应用如果不想停机,更推荐先冻结文件系统或用带一致性保障的快照工具:

# libvirt 调用 QEMU guest agent 冻结客机文件系统(需要客机装了 qemu-guest-agent)
virsh domfsfreeze web01
cp /var/lib/libvirt/images/web01.qcow2 /backup/web01-$(date +%F).qcow2
virsh domfsthaw web01

virsh domfsfreeze/domfsthaw 对应 Linux 侧的 fsfreeze --freeze /挂载点 / fsfreeze --unfreeze /挂载点,冻结期间客机文件系统暂停写入,副本处于一致性状态。停机复制虽然笨,但一致性和简单性都最好;fsfreeze 方案适合"不能停机又想拿一致副本"的场合。

#第五步:回滚的完整姿势

内部快照回滚(目标域先停机,避免文件句柄冲突):

virsh shutdown web01
virsh snapshot-revert web01 before-upgrade --running

外部快照 / 备份文件回滚(原文的做法,直接换文件):

virsh destroy web01
mv /var/lib/libvirt/images/web01.qcow2 /var/lib/libvirt/images/web01.corrupt.qcow2
cp /backup/web01-2026-08-20.qcow2 /var/lib/libvirt/images/web01.qcow2
virsh start web01

如果外部快照链中间某层出问题、想回到更早的时刻,也可以 snapshot-revert(libvirt 把活动层切到目标快照,后续层保留但不再活动):

virsh shutdown web01
virsh snapshot-revert web01 before-upgrade --running
# 确认回滚无误后,再清理后层快照:
virsh snapshot-delete web01 after-upgrade --children

回滚前记住两件事(原文的实战教训,务必遵守):

  1. 先备份当前状态——万一回滚后悔了还有退路;
  2. 回滚后检查虚拟机内时钟和数据一致性——快照恢复相当于系统时间跳变,数据库可能需要重放/回滚日志(如 MySQL 的 InnoDB redo、PostgreSQL 的 WAL 恢复),建议回滚后立即校准时间(NTP/chrony)并检查应用数据。

#第六步:合并与收敛快照链(commit / rebase / convert)

链越长,读路径越长、单点风险越大,所以要定期把链收敛。

办法一:停机 + qemu-img commit(把顶层并进基盘,链缩短一层)

virsh destroy web01
qemu-img commit -b /var/lib/libvirt/images/ubuntu-base.qcow2 \
  /var/lib/libvirt/images/web01.qcow2
qemu-img check /var/lib/libvirt/images/ubuntu-base.qcow2

commit 后顶层数据已并入基盘,覆盖层基本变空。之后要么删掉它、让域直接指向基盘(记得 virsh edit 改磁盘路径),要么留着当空 overlay 继续用。

办法二:停机 + qemu-img rebase(跳过中间层,常用在"删掉中间层"前)

链 base ← mid ← top,想把 mid 拿掉、让 top 直连 base:

virsh destroy web01
qemu-img rebase -b /var/lib/libvirt/images/base.qcow2 -F qcow2 \
  /var/lib/libvirt/images/top.qcow2
qemu-img check /var/lib/libvirt/images/top.qcow2
# 校验通过后再删中间层
rm /var/lib/libvirt/images/mid.qcow2

不带 -u 的 rebase 会把"原来要从 mid 读的数据"拷进 top,top 在 rebase 后不再依赖 mid,mid 才能安全删除。这是"删除中间层"的标准姿势:先 rebase,再删,顺序不能反。

办法三:停机 + qemu-img convert(整链扁平化成单文件)

qemu-img convert -O qcow2 -p web01.qcow2 web01.flattened.qcow2
qemu-img check web01.flattened.qcow2
# 验证无误后替换(原文的合并流程)
mv web01.qcow2 web01.old.qcow2
mv web01.flattened.qcow2 web01.qcow2

convert 会把整条链(含 backing)读出来写进一个不依赖任何 backing 的独立镜像,常用于"删除虚拟机前归档""迁移到另一台宿主机""给基盘一次性打平"。注意:转换期间镜像必须处于稳定状态(停机),否则读到的是不一致视图。

办法四:不停机实时合并(生产环境首选)

libvirt 的 blockcommit 可以在虚拟机运行时合并链,不需要停机窗口:

virsh blockcommit web01 vda \
  --base /var/lib/libvirt/images/ubuntu-base.qcow2 \
  --active --pivot --wait

--active 表示顶层就是活动层,--pivot 让合并完成后活动盘切到 --base,--wait 等待合并结束。合并完成后链缩短(覆盖层被并进基盘),虚拟机无感知。它能直接解决"链太长"的读放大问题。

#第七步:瘦身与压缩

客机里删了文件、qcow2 不自动收缩,用 convert 重建即可瘦身;加 -c 还能无损压缩(转换时对簇做压缩):

virsh shutdown web01
qemu-img convert -O qcow2 -c -p web01.qcow2 web01.compact.qcow2
qemu-img check web01.compact.qcow2
du -h web01.qcow2 web01.compact.qcow2   # 对比瘦身前后的 disk size
mv web01.compact.qcow2 web01.qcow2
virsh start web01

转换前先用 qemu-img measure 估算新镜像大小、用 qemu-img map 看哪些区域真实分配了数据:

qemu-img measure -O qcow2 web01.qcow2
qemu-img map --output=json web01.qcow2

measure 给出 required(按当前分配情况的最小值)和 fully allocated(全部写满的最大值)两档,先量后转,避免把最后一块磁盘空间用完。

#官方备份工具推荐

原文强调"纯 cp 正在写的文件等于备份损坏的数据库",这是对的。生产环境的正解是走官方机制,而不是裸拷贝:

  • libvirt 官方备份接口(Backup API):libvirt 6.0+ / QEMU 4.2+ 起提供基于 checkpoint(检查点)+ bitmap(位图) 的备份模型。checkpoint 记录一个时间点,virsh backup-begin 只读"自某 checkpoint 以来变化的簇",天然支持增量备份:
virsh checkpoint-create-as web01 cp1 --disk-only
virsh backup-begin web01 backup.xml

backup.xml 里声明备份目标与传输方式(官方文档支持写到本地文件或经 NBD 推送到备份端;具体 XML 字段见 libvirt 文档,这里不展开)。libvirt 官方文档明确推荐用这套 API 替代"停机拷贝"作为生产备份方案。

  • QEMU 原生 bitmap 机制:不经过 libvirt 时,可以直接在 qcow2 里维护位图作为增量备份的依据:
qemu-img bitmap --add /var/lib/libvirt/images/web01.qcow2 full-bm
# 备份程序(如 drive-backup / blockdev-backup)按位图增量拷贝后,清掉已备份位:
qemu-img bitmap --clear /var/lib/libvirt/images/web01.qcow2 full-bm

社区基于这套机制的成熟工具有 virtnbdbackup(NBD + checkpoint 增量备份)等,可按需选用。快照是运维手段,备份才是数据安全底线——两者不要混淆。

#验证层:怎么确认我做对了

任何操作做完都要问"我怎么知道它对了"。验证分三类。

#链结构验证

qemu-img info --backing-chain 递归打印整条链;qemu-img info 单层打印时能看到 backing 指针:

image: /var/lib/libvirt/images/web01.qcow2
file format: qcow2
virtual size: 40 GiB (42949672960 bytes)
disk size: 1.8 GiB
cluster_size: 65536
backing file: /var/lib/libvirt/images/ubuntu-base.qcow2
backing file format: qcow2

判定标准:

  • 创建外部快照后:活动盘出现 backing 指针,且 virsh snapshot-list web01 里能看到新快照;
  • rebase/commit 后:--backing-chain 显示链变短、父指针指向预期文件;
  • convert 后:新文件没有 backing file 行,它是独立镜像。

#完整性验证

qemu-img check 是原文强调"快、值得养成习惯"的命令,任何合并、删除、回滚之后都跑一遍:

qemu-img check /var/lib/libvirt/images/web01.qcow2
# 期望输出(正常情况):
# No errors were found on the image.

有泄漏簇或引用计数错误时,check 会打印错误明细,用 -r all 尝试修复:

qemu-img check -r all /var/lib/libvirt/images/web01.qcow2

注意:修复前先留一份副本。-r 是修改性操作,修复本身也可能失败(某些损坏无法从内部恢复),有备份才有退路。

#数据一致性验证

命令层面的"成功"不等于数据正确。实操中至少做三步:

  1. 回滚/恢复后先 virsh start web01,登录系统看文件系统能否干净挂载(必要时手动 fsck);
  2. 校准时钟:timedatectl 确认时间正常、NTP 在同步;
  3. 对数据库应用执行应用自带的校验(如 MySQL CHECK TABLE、PostgreSQL 恢复流程走完、业务探活),确认业务数据可用。

#边界与坑

#宿主磁盘会被撑爆

按需分配让"40G × 20 台 = 800G 虚拟容量"看起来很美,但物理盘只有 200G 时,全部写满就是磁盘满事故。规划按两个维度都留余量:虚拟 size 总和(决定最大可写量)与物理可用空间(决定当下能不能活)。建议宿主机做容量监控(df -h/du 巡检或接入告警),并为 qcow2 所在文件系统留出安全余量。

#空洞不会自己回来

客机删除文件后 qcow2 不收缩,这是稀疏文件的正常属性。想让"删掉的空间"真正还给宿主机:

  • 最直接:停机 qemu-img convert 重建(见操作层第七步);
  • 有条件时配置 discard 透传:域 XML 里给 virtio-blk 磁盘加 discard='unmap',客机内定期 fstrim,qcow2 会把已释放簇标记为未分配;
  • 注意:即便开了 discard,回收也是按簇懒执行的,日常删除文件仍然不会立刻反映到 du。默认情况下"删除 = 空间不回来",瘦身靠 convert。

#内部快照的删除风险与写放大

内部快照的每一个优点都伴随代价:

  • 写放大:快照存在期间,所有改动簇都要 COW,内部快照数量越多、保留越久,性能损耗越明显;
  • 删除不安全:镜像文件大幅增长后,删除内部快照需要把共享簇与独占簇重新归并,遍历整个文件,耗时长且对文件系统余量有要求;运行中操作或命令中断都可能损坏文件。删除前先停机、先 check、先备份;
  • 定位:它是"操作前临时留一手",不是长期备份方案。长期保护用外部快照 + 官方备份工具。

#链过长的代价

外部快照可以叠出很长的链(base → snapshot1 → snapshot2 → …),但每加一层:

  • 读路径变长:冷数据命中要逐层回退,链越长延迟越高;
  • 单点故障面变大:中间任何一环损坏,整条链都读不出来;
  • 回滚语义变复杂:层数一多,很容易回错层级,把"以为在回滚"变成"丢了一大段状态"。

所以快照链要控制深度。健康的做法:定期 virsh blockcommit(在线收敛)或停机 qemu-img convert(整链打平),把链长度压回 1–2 层,再按需开新链。

#中间层删除导致链断裂

这是最常见的翻车点:以为快照是独立备份,rm 掉中间层,然后整条链读不出来。机制上,子层未分配的簇依赖父层数据,删除中间层 = 掐断回退来源。正确顺序永远是:先 rebase(把子层指向祖父层)或 commit(把中间层并入父层),验证 check 通过,再删中间层文件。

#一致性:内存态、文件系统与数据库

  • 外部快照 --disk-only 默认不含虚拟机内存态,回滚后应用运行状态(进程、socket、未落盘的页缓存)不还原——它只还原磁盘视图;
  • 数据库场景:快照/备份时若没有 fsfreeze(或 qemu guest agent 的 quiesce),文件系统内部一致性无法保证,恢复后数据库需要走崩溃恢复流程;
  • 回滚后系统时钟跳变,依赖时间的应用(证书、会话、任务调度)可能异常,记得校准并观察。

#兼容性:qemu-img 版本与特性

qcow2 有版本特性(compat)差异:现代 qemu 默认创建 compat=1.1(v3)的镜像,支持位图等特性;老版本 QEMU 可能打不开。跨版本升级或跨宿主机迁移前,先用对应工具 qemu-img info 确认,必要时降级:

qemu-img convert -O qcow2 -o compat=0.10 web01.qcow2 web01.legacy.qcow2

另外,backing file 路径记录在镜像 header 里,移动/改名基盘后,覆盖层会找不到 backing。修正指针用安全模式(只改 header,不动数据):

qemu-img rebase -u -b /新路径/ubuntu-base.qcow2 -F qcow2 /var/lib/libvirt/images/web01.qcow2

-u 是 unsafe 模式,仅当新基盘内容与旧基盘完全一致时才能用——它只改指针,不做数据搬运。

#回滚前备份、回滚后检查

两条铁律:回滚前先备份当前状态(后悔药),回滚后检查时钟与数据一致性(数据库日志恢复是正常现象,不是快照坏了)。

#收尾:什么时候该用什么

把整篇文章压成一张决策表:

  • 日常派生多台虚拟机:装好 base 后,每台用 qemu-img create -b base overlay 派生,基盘打"干净基线"只读;
  • 升级/变更前留后路:短时间窗口用 virsh snapshot-create-as(内部快照,配 --disk-only 只留磁盘);不想有任何风险或有数据库时,停机复制或 domfsfreeze 后复制;
  • 长期增量保护:外部快照 + 定期收敛链(blockcommit/convert)+ libvirt Backup API/bitmap 做真正备份;
  • 日常体检:定期 qemu-img check,便宜且值得养成习惯。

快照的本质是"廉价但脆弱的增量视图":它依赖整条链的完好,替代不了完整备份。把机制(L1/L2 映射、引用计数、COW、backing 链)记在心里,任何"为什么这个操作会失败"的问题,回到那张映射表上都能找到答案——这也是本文从底层机制而不是命令表写起的原因。

相关阅读

Related

留言讨论

0 条

还没有留言,来说点什么吧。

KVM qcow2 磁盘与快照:空间管理和回滚策略 · LXH·BLOG