做虚拟化运维这些年,QEMU/KVM 环境里的快照是我用得最勤的功能之一。调试内核、测试软件包、给客户机做系统升级,手滑搞崩了,一个快照就能让你从“白血病患者”恢复到“刚体检完的健康人”。这篇文章就把我在实际操作中怎么用 virsh 和 qemu-img 给 QEMU/KVM 客户机做快照、什么时候用哪种方法、那些文档里不会写的坑,一次性说清楚。内容既覆盖刚接触虚拟化的新手需要的基础,也包含已经会敲命令但想搞明白底层原理的老手关心的细节。
1. 先弄清快照的底层原理和分类
1.1 内置快照与外置快照的本质区别
快照在虚拟化里的含义,简单说就是在某个时间点把客户机的磁盘状态甚至内存状态完整地“记一笔”。这里最关键的是磁盘层怎么实现,而这又取决于镜像格式。QEMU/KVM 最常用的格式是 qcow2,它原生支持快照;而 raw 格式本身就是一块裸数据,想在上面做原生快照几乎不可能,只能靠 LVM 快照这类外部存储层方案。
qcow2 能支持快照,靠的是 COW(Copy-On-Write,写时复制)机制。这个机制可以这么理解:你有一本纸质台账,里面记着各种数据,COW 就是在你动笔改某一页之前,先给这一页拍一张照保存到抽屉里,然后再改动。快照记录的就是这些“被改动前的旧数据”。所以你哪怕把当前数据改得面目全非,只要拿着快照记录就能把被改过的数据块还原成拍照那一刻的样子。
内置快照和外置快照的区别,主要在于快照数据存在哪里:
| 对比项 | 内置快照 | 外置快照 |
|---|---|---|
| 存储位置 | 全部保存在同一个 qcow2 文件内部 | 独立生成一个新的 qcow2 覆盖文件 |
| 创建速度 | 快,但要改写原镜像内部结构 | 极快,几乎瞬间完成(只需要新建文件) |
| 链式关系 | 内嵌在单文件里的快照表中 | base 镜像 + overlay 覆盖文件组成链条 |
| 恢复方式 | 通过 qemu-img / virsh 直接在当前文件上回滚 | 把覆盖文件废弃或合并回 base |
| 管理复杂度 | 单文件,便于拷贝迁移 | 多个文件需要一起管理,链条容易乱 |
| 适用场景 | 模板镜像、开发调试、单机离线快照 | 生产环境在线快照、需要零停机的场景 |
内置快照的优点就是干净,整个虚拟机就一个 qcow2 文件,拷走、备份、换机器都方便。缺点是快照多了以后,这个文件内部结构会变得复杂,而且做在线内存快照时,QEMU 要把整个内存镜像塞进同一个文件里,体积会急剧膨胀。外置快照则把“基础盘数据”和“改动增量数据”拆开了,越写越多的是 overlay 文件,base 可以始终保持原始状态不可变。这样一来,即使 overlay 哪天出了问题,base 依然能作为干净基线使用。
1.2 在线快照与离线快照的差异
除了内置/外置的区别,还要搞清楚在线快照和离线快照。离线快照就是在客户机关机状态下打快照,这时候磁盘上没有进程在写数据,文件系统处于一致状态,快照内容干净可靠。恢复的时候也不会遇到什么奇怪问题,这是我最推荐也最常用的方式。
在线快照则是客户机运行中直接做快照。virsh 默认做快照时,会把内存状态一起抓下来,这意味着 QEMU 需要短暂暂停客户机的执行,把内存镜像写入磁盘,再恢复执行。暂停时间可能只有几百毫秒,也可能长达数秒,具体取决于内存大小和磁盘写入速度,生产环境里这是一个需要注意的停顿窗口。
在线快照还有一个更隐蔽的问题:文件系统一致性。虚拟机里跑着 MySQL、Redis 这类对持久化很敏感的服务,内存 cache 里可能堆积着大量尚未落盘的数据。如果这些脏数据还没刷到磁盘,快照恢复后就会出现数据回退、甚至文件系统损坏。做在线磁盘快照之前,最好在客户机内部先执行 sync,或者配合 QEMU Guest Agent 的 fsfreeze 冻结文件系统,确保所有缓存都先落盘。这也是为什么很多生产环境的自动化脚本里,做快照前都要先走一步“冻结文件系统”的操作。
2. 实操:用 virsh 命令管理 libvirt 快照
2.1 创建快照:snapshot-create-as 的完整参数
如果你是通过 libvirt 管理 KVM,virsh 是操作快照最顺手的入口。我最常用的命令是virsh snapshot-create-as,它能在指定域名下创建以名字标记的快照,方便之后查找和恢复。
一条完整的离线快照命令长这样:
virsh shutdown testvm virsh snapshot-create-as testvm clean-install --description "刚装完系统,未做任何配置" virsh start testvm第一行是优雅关机,等 guest 完全停止后再打快照,这是做离线快照的标准动作。第二行创建名为 clean-install 的快照,并附带描述信息,方便记忆。最后一行恢复启动。整个过程逻辑清晰,适合手工操作。
snapshot-create-as的参数比较多,实际中挑着用就行,关键参数整理如下:
| 参数 | 作用 | 使用场景 |
|---|---|---|
--disk-only | 只做磁盘快照,不保留内存状态 | 在线做外置快照时常用 |
--atomic | 要求快照操作要么全部成功、要么完全不执行 | 追求强一致性的自动化脚本里 |
--quiesce | 配合 guest agent 冻结文件系统后做磁盘快照 | 在线快照且客户机运行着数据库等应用 |
--memory-only | 只保存内存状态,不保存磁盘状态 | 需要保存当时运行时现场,但磁盘变化不太关心 |
--xml | 使用 XML 文件定义快照的详细内容 | 高级定制场景 |
需要特别注意,--atomic选项是较高版本 libvirt 才支持的,早期版本没有。低版本上如果强行加这个参数会直接报错,所以我一般建议先查一下自己环境里的 libvirt 版本再决定参数能不能用。--quiesce则依赖 QEMU Guest Agent 已经安装在客户机里,并且 virtio 通道正常通信,否则参数会被静默忽略,看起来快照成功了,实际文件系统并没有被安全冻结,这种“表面稳定”比直接报错更可怕。
另外一个实操经验:给快照起名千万别拍脑袋。我见过有人用 date、data1、test 这种名字连续打了几十个快照,两周后想回滚,自己都分不清哪个该用。最稳妥的命名约定是“日期-用途”,比如20250105-before-upgrade,既保留了时间线,又点明了这是干什么用的快照。配合 description 参数写上业务场景,后续任何人接手运维都能看懂。
2.2 列出、回滚与删除快照
快照建好之后,查询信息用virsh snapshot-list:
virsh snapshot-list testvm --tree--tree参数会以树状结构显示出快照之间的父子关系。如果一个快照是在另一个快照的基础上创建的,这种层级关系在排查问题时特别有用。virsh snapshot-info testvm --current可以查看当前所在快照的信息,确认自己挡在哪个时间点上。
回滚快照的命令是snapshot-revert:
virsh snapshot-revert testvm clean-install这里有几个容易踩的坑。如果快照里包含了内存状态,回滚时默认会用快照里的内存状态去恢复客户机。但如果客户机当前正处于运行状态,回滚会面临状态冲突,这时需要显式指定--running或--paused,告诉 libvirt 回滚后客户机应该以什么状态启动。更安全的做法是:回滚前先把客户机关机,让回滚操作在干净的环境下进行,然后再启动。虽然这比在线回滚多花了一点时间,但成功率和可控性高得多。
删除快照用virsh snapshot-delete <domain> <snapshot-name>。如果快照有子快照,直接删父快照会失败,除非加上--children把子快照一起删掉,或者使用--metadata把快照关联的元数据清干净。删除当前活动快照时,后续的快照链会重新组织,这里务必想清楚再动手,因为误删会导致与其关联的增量数据无法恢复。
2.3 一个完整的离线快照流程示例
下面给出一段我平时在半自动脚本里使用的离线快照流程,每一步都有明确目的:
# 1. 优雅关闭客户机,等待进程退出、缓存落盘 virsh shutdown testvm while [ "$(virsh domstate testvm)" != "shut off" ]; do sleep 2; done # 2. 创建快照,名字带日期前缀,便于后期识别 SNAP_NAME="$(date +%Y%m%d)-before-yum-update" virsh snapshot-create-as testvm "$SNAP_NAME" --description "升级前的备份点" # 3. 确认快照创建成功,信息无误 virsh snapshot-list testvm --name # 4. 启动客户机 virsh start testvm这段脚本看似简单,但每一步都带着问题意识。第一步的 while 循环是关键:virsh shutdown只是发送 ACPI 关机信号,guest 内部可能需要几十秒才能真正退出,直接 sleep 固定时间往往不够可靠,轮询 domstate 才是稳妥做法。第二步创建快照前用户已经确保磁盘处于一致状态,所以不需要--quiesce。第三步肉眼确认快照已经出现在列表里,属于“操作后验证”的习惯,这个习惯能省掉很多后续排查的麻烦。
3. 进阶:qemu-img 与块设备级快照操作
3.1 qemu-img 的快照命令详解
当不再经过 libvirt,直接使用 QEMU 的镜像文件时,qemu-img就是操作内置快照的主力工具。它最典型的三个子命令:
# 创建快照 qemu-img snapshot -c clean-state /var/lib/libvirt/images/testvm.qcow2 # 列出快照 qemu-img snapshot -l /var/lib/libvirt/images/testvm.qcow2 # 回滚到指定快照 qemu-img snapshot -a clean-state /var/lib/libvirt/images/testvm.qcow2-c即 create,-l即 list,-a即 apply。这三个命令都直接作用于 qcow2 文件内部的快照表,不需要虚拟机管理器的配合,所以特别适合在不启动 libvirt 的纯 QEMU 环境里使用。qemu-img 的快照命令只对磁盘层生效,不包含内存状态。如果虚拟机正在运行,直接跑qemu-img snapshot操作同一个镜像文件,很容易造成镜像损坏或数据不一致,所以这条命令的限制场景也很明确:离线操作时用,在线操作时别碰。
qemu-img 还有一层更底层的角色。在批量克隆虚拟机、制作基础模板时,qemu-img 是行业内最常用的工具。用 qemu-img 把一块装好系统的 qcow2 做成黄金模板,打上一个 clean 快照,后续所有新虚拟机都从这份模板复制出来。这种场景里,qemu-img 快照命令的价值是“定义一份不可变的基线状态”。
3.2 外置快照与 blockdev-snapshot 的取舍
外置快照可以手动创建,核心命令是:
qemu-img create -f qcow2 -b /var/lib/libvirt/images/testvm-base.qcow2 /var/lib/libvirt/images/testvm-overlay.qcow2-b指定 backing file(基础镜像),新建的 overlay 会记录自创建以来发生的所有改动。把 overlay 作为虚拟机的当前磁盘继续使用,就形成了一个“基础盘不变、增量写新盘”的外置快照形态。这种方案的创建时间几乎为零,非常适合大镜像的快速快照。代价是文件数量变多,而且一旦把 overlay 和 base 的对应关系搞乱,恢复就困难得多。所以我把这个方案限定在“确认自己清楚基础镜像是什么、改动增量是什么”的前提下使用。
多了一层 libvirt 或 QEMU 监控台之后,还有更规范的做法。QEMU 的 QMP(QEMU Machine Protocol)里提供了blockdev-snapshot,它能在 QEMU 进程的块设备写入路径上直接做原子切换,从技术层面避免手动文件切换时可能出现的竞态。使用 QMP 一般通过 socat 连接 QEMU 的 QMP socket,然后发送 JSON 格式指令。如果你正在做 QEMU/KVM 相关的底层开发,或者需要通过脚本对 QEMU 做细粒度控制,blockdev-snapshot 是值得深入了解的方向。相比之下,libvirt 的virsh blockcopy、virsh snapshot-create-as --disk-only更像是对外封装好的“安全入口”。
外置快照还有一个不得不提的后续问题:合并链。创建多个外置快照后,base、overlay1、overlay2 一层叠一层,链越深,读性能越差,管理越麻烦。合并用qemu-img commit:
qemu-img commit /var/lib/libvirt/images/testvm-overlay.qcow2这条命令会把 overlay 中的数据修改写回 base,然后 overlay 文件就可以从链里移除了。不过在 commit 之前,务必确认没有虚拟机正在使用这些文件,否则会导致镜像损坏。链合并是快照运维里最耗费精力的一环,也是区分“会用快照”和“会管快照”的分水岭。
3.3 快照链合并与清理
快照链一深,事情就开始变得复杂。比如 libvirt 环境里,virsh blockcommit testvm --base vda --top vda --wait --verbose可以把当前活动的顶块数据提交到底层,减少链的长度。libvirt 会把这个过程封装好,不需要手工处理 QMP JSON。手动用 qemu-img 操作则需要更多注意:
- 合并前确认虚拟机已关闭,且没有其他进程打开这些镜像文件。
- 合并后确认 overlay 文件可以安全删除或归档。
- 如果有后续 overlay 指向当前正在合并的 overlay,需要把 backing file 重新指向上层,否则链就断了。
快照链清理的另一个关键点是空间。qcow2 文件支持稀疏文件特性,删除快照后,被释放的数据块不一定真正还给存储,文件可能在物理层面仍然占着空间。在生产环境里,我见过某个虚拟机打了几十个快照,后来逐个删除,但宿主机的存储空间却没有明显减少,这就是稀疏文件在“占用空洞”上的典型陷阱。遇到这种情况,可以考虑用qemu-img convert把 qcow2 重新整理一遍,把无用的空洞去掉,但这个过程同样需要停机操作。
4. 结合应用场景的快照运维实践
4.1 开发调试场景:交叉编译与内核调试
做嵌入式开发、内核调试的人,十有八九会用 QEMU 模拟非本机架构,比如在 x86 主机上用qemu-system-aarch64跑 ARM64 客户机。这种场景下快照的用法非常典型:每到一个稳定的代码编译状态,就打一个快照作为“里程碑”,之后无论怎么折腾内核参数、驱动模块、设备树,跑崩了都能秒退回里程碑状态,重头再来。
我认识不少做内核调试的同行,日常流程就是 VSCode 远程连到开发机上,QEMU 启动 ARM64 虚拟机,GDB 连接 QEMU 的 gdbstub 端口做断点调试。如果不用快照保护环境,每改一次内核参数都可能把虚拟机的文件系统弄坏,然后重新部署环境、重新交叉编译、重新拷贝 rootfs,一上午的时间就这么蒸发了。有了快照,试错成本直线下降:打好断点前先打快照,实验失败就回滚,实验成功才继续往下推进。
开发环境的特殊之处在于,快照的语义更接近“保存现场”而不是“备份数据”。所以这类场景里,哪怕只是内存快照也很有价值。比如调试一个偶现的并发问题,抓到现场后做一个--memory-only快照,之后可以反复分析这个内存状态,而不必担心现场因磁盘写入被破坏。
4.2 服务器与生产环境的快照策略
给服务器做系统、扩容磁盘、迁移数据这类高危险操作,正确的姿势是先打一个快照再动手。很多人习惯直接操作,等出了问题才发现没有后悔药。这个教训我亲眼见过不止一次。有一位同行给一台生产服务器做系统升级,操作到一半发现新内核和旧驱动不兼容,系统直接起不来。因为没有快照,最后只能从备份里恢复,折腾了好几个小时才把服务带回可用状态。如果当时花一分钟打个快照,回滚只需要几十秒。
生产环境中快照不能替代备份,这个认知必须刻在脑子里。快照文件通常和虚拟机的镜像文件在同一台宿主机、甚至同一块存储上,存储坏了,快照和原盘一起消失。所以生产环境里的快照策略,我的建议是:
| 操作场景 | 快照方式 | 保留时间 | 注意事项 |
|---|---|---|---|
| 日常小版本升级 | 内置快照 | 验证通过后删除 | 升级后做基本验证再清理旧快照 |
| 内核升级 / 系统迁移 | 外置快照 | 至少保留一周 | 合并时间安排在业务低峰期 |
| 存储路径变更 | 外置快照 | 全程保留至迁移完成 | 迁移期间不可删除 base |
| 日常例行维护 | 不建快照 | - | 配合备份系统做数据兜底 |
是不是所有操作都必须打快照?我的建议是分场景。高风险、影响面大的操作必须打;无风险、可重复执行的例行操作不必打,因为快照本身也占用空间和产生管理成本。
4.3 Windows 客户机与共享目录配合
Windows 客户机在 QEMU/KVM 环境里也非常常见。很多人为了方便,会用 virtiofs 或者 SMB 共享文件夹的方式让 Windows guest 直接挂载宿主机目录。这种共享目录的挂载点状态,在做快照时需要额外留一个心眼。
如果 Windows guest 里某个网络驱动器或者 virtiofs 挂载点正处于读写状态,快照恢复后,这个挂载点对应的文件内容和宿主机的实际文件很可能不一致。Windows 的文件系统缓存行为比 Linux 更激进,缓存里的数据没及时写回宿主机,快照恢复的只是 guest 视角里的“目录影印”,并非宿主机里的真实文件。这个问题即使在快照包含内存状态的情况下也无法完美解决,因为外挂的目录本质上不属于虚拟磁盘的数据范围。
我在实际操作中的处理方式很直接:做 Windows 客户机快照之前,先在 guest 内部把共享目录里打开的应用程序关掉,然后执行一次强制同步。Windows 下可以用工具触发 flush,或者直接断开再重新挂载共享目录。完成之后再创建快照,这样得到的快照中,共享目录里的数据引用才是干净一致的。这个细节文档里很少提到,只有被坑过的人才懂为什么。
5. 常见问题与排查技巧实录
5.1 快照创建失败或卡住
快照创建失败的常见原因,我整理成一张速查表,方便直接对号入座:
| 异常现象 | 常见原因 | 处理方式 |
|---|---|---|
snapshot-create-as报错内存不足 | 在线快照尝试保存内存镜像,但磁盘空间不够 | 清理镜像所在存储,或改用--disk-only |
| 创建快照时客户机一直卡住 | QEMU 暂停客户机保存内存状态的时间较长 | 等待或强制中断,评估是否改用离线快照 |
Could not open disk image | overlay 丢失或 backing file 路径被移动 | 检查磁盘镜像路径和 backing file 关联 |
| 快照列表里看不到刚创建的快照 | libvirt 元数据与磁盘镜像不一致 | 重启 libvirtd 前先确认磁盘状态,必要时手动 metadata 修复 |
--quiesce没有真正冻结文件系统 | 客户机里没有安装 QEMU Guest Agent | 安装 qemu-guest-agent 并确认 virtio 通道正常 |
卡住的问题最常见于在线快照且内存特别大的虚拟机。我之前遇到过一个 64GB 内存的客户机,做在线快照的时候整整卡了差不多一分半钟,服务当然跟着断了。从那以后,我的原则变成了:只要客户机能短暂停机,就绝不使用在线快照;在线快照只留给确实不允许停机的核心业务。
5.2 恢复后客户机启动异常
快照恢复后,客户机起不来或者文件系统报错,也是绕不开的问题。
恢复后卡在启动阶段,大概率是文件系统不一致。尤其是在线做的磁盘快照,没有冻结文件系统,恢复时 ext4 或 xfs 的日志里有大量未重放数据。处理方法是进入急救模式或 LiveCD 引导,对根分区执行 fsck。xfs 文件系统用xfs_repair,ext4 用fsck.ext4,别搞混了。
另外一类情况是恢复快照后,客户机的网络配置、磁盘设备名错乱。设备名错乱在 Linux guest 里比较常见,因为恢复后内核枚举设备的顺序和之前不同,旧系统使用eth0这种名称时最容易踩坑。现在主流用ens*或enp*这种基于 PCI 位置的命名,一般不容易出现这个问题,但老镜像就难说了。建议在客户机里提前配置好 udev 规则,把网卡名固定下来,给快照恢复加上一层保险。
还有一类问题是 UUID 冲突。如果你从快照克隆出多台虚拟机,它们的根文件系统或者磁盘的 PARTUUID 完全相同。两台机器同时接入同一个网络时,可能会因为文件系统 UUID 相同导致 mount 问题。这种情况下建议在克隆后重新生成 UUID。这也是为什么快照更适合“回滚”而不是“克隆”,克隆衍生出的副本需要额外处理身份信息。
5.3 磁盘性能下降与快照文件膨胀
快照用得越久,文件越大,性能越慢,这是正常现象。qcow2 的 COW 机制天然存在写入放大问题:每次修改一小块数据,可能要先读取原来的数据块、写入快照记录、再写入新数据,一次写操作被放大成多次读写。测试过的环境里,快照链过深时,持续 4K 随机写入性能可能下降一半以上。
性能下降的另一个来源是碎片化。qcow2 文件随着反复写入、删除、覆盖,数据块在物理层面变得零散,随机 IO 性能直线下降。快照回滚之后,qocw2 文件内部的映射表也会出现大量空洞,进一步加剧磁盘碎片问题。
解决方案也不复杂,定期做链合并和镜像整理。链合并不是写脚本定期跑就行,还要考虑业务低谷期,因为 commit 期间虚拟机不能正常提供读写服务。整理镜像使用qemu-img convert把旧 qcow2 转换成新的 qcow2,相当于把碎片化数据物理重排,效果立竿见影。不过这条命令也是重量级操作,必须先停机。
| 问题表现 | 根因 | 解决思路 |
|---|---|---|
| 虚拟磁盘写入慢 | COW 写入放大 + overlay 链过深 | 做 blockcommit 合并链,或者定期 convert 整理 |
| qcow2 文件体积异常膨胀 | 内部快照残留大量旧数据块 | 删除无用快照,必要时 convert 重做镜像 |
| 删快照后宿主机磁盘空间没释放 | qcow2 稀疏文件无法自动收缩 | 使用 convert 或 qemu-img measure 评估真实占用 |
| overlay 文件无法删除 | 有客户机进程还在使用该文件 | 先停虚拟机,再清理文件 |
能用默认参数跑通和知道为什么这么跑,是两码事。我见过太多人把快照当成万能保险,出了问题就回滚,然后重试,却从不思考这次回滚会不会把自己之前提交的数据也一起带走。快照是一个基于某个时间点的一致性状态,它记录的只是那台虚拟机的数据处理现场,不代表你的所有业务数据都安全落地。
最后再分享一个小技巧:无论哪种快照方案,打完快照先别急着做操作,先启动客户机、检查关键服务、确认系统日志正常,再开始真正的危险操作。多花这几分钟,能帮你判断这次快照到底打没打干净,是不是一个可用的回滚点。这个习惯养成了,你的快照运维才算是真正入门了。