一提到 KVM/QEMU 在线备份,很多人第一反应是:把 qcow2 文件直接复制走不就行了。这个思路在虚拟机停机的时候没问题,但虚拟机上还跑着服务的时候,你拷出来的文件大概率是不能用的。这篇文章是我自己整理在线备份方案时的完整记录,从为什么不能直接拷贝,到外部快照的工作原理,再到一套可以照着抄的备份脚本,最后加上还原演练和踩坑清单。适合正在用 KVM/QEMU 跑虚机、暂时不想引入商业备份产品的同学参考,也适合刚接触虚拟化的运维新人理解备份背后的机制。
1. 先搞明白:在线备份到底在备什么
1.1 直接拷贝运行中的 qcow2 等于边写边读
先说结论:对运行中的虚拟机直接执行cp或rsync,不是不能跑,而是不能保证拷出可用镜像。QEMU 进程在持续把新数据写入磁盘文件,文件系统元数据、日志和数据块都可能处于半更新状态。你复制的过程可能跨越多个数据块的写周期,拿到的是一个“不同时间点的切片”拼在一起的文件。运气好,虚拟机启动后文件系统报错;运气不好,直接无法识别为合法镜像。
你可以想象成一份正在编辑的文档,一边打字一边把文件复制到 U 盘。编辑器可能只把一部分字符写入磁盘,剩下的还在内存里。你得到的那份复制品,既不是编辑前的完整版,也不是编辑后的完整版,而是一个中间碎片。虚拟机的磁盘比这复杂得多,数据库事务日志、文件系统日志、页缓存都交织在一起,直接拷贝的风险远高于普通文档。
有人会说,那我先用 LVM 快照或者 qcow2 快照,再复制总行了吧?快照解决的是“同一时刻一致性视图”,但它默认只保证块设备层面的一致,虚拟机内部的文件系统依然是活动的。你不知道当时有多少数据还在内存页缓存里没落盘。要让快照点真正可用,必须在快照前让 guest OS 把缓存刷到磁盘,并暂停业务写入。这正是后面要讲的 guest agent 存在的意义。
1.2 崩溃一致性不等于应用一致性
备份圈常讲两种一致性等级:崩溃一致性和应用一致性。崩溃一致性,差不多等于虚拟机突然断电。启动后文件系统会通过日志回放恢复到自己认为一致的状态,虚拟机大概率能开机,但数据库这类应用可能处于“需要人工恢复”的状态,事务可能只提交了一半。
应用一致性则意味着,在备份点,虚拟机内部文件系统和应用都处于可恢复状态,数据库的事务日志、索引和数据文件是配套的。拿到应用一致性的备份,恢复后打开业务,不需要额外的数据库修复动作。
要获得应用一致性,光靠文件系统冻结还不够。MySQL、PostgreSQL 这类应用通常需要配合锁表、归档日志切换等机制,或者依赖应用自身的备份接口。所以下面表格里的“一致性等级”,你最好在备份前先和业务方对齐。
| 一致性等级 | 获取方式 | 恢复结果 | 适用场景 |
|---|---|---|---|
| 崩溃一致性 | LVM 快照 / qcow2 快照 | 能开机,文件系统回放日志,数据库需人工恢复 | 缓存类、可接受少量不一致 |
| 应用一致性 | guest agent 冻结 + 应用配合 | 业务数据可恢复,数据库基本无需修复 | 数据库、订单系统等 |
1.3 先谈 RTO 和 RPO,再谈技术选型
做在线备份前,先想清楚两个业务指标。RPO 是“允许丢多少数据”,RTO 是“多快能把业务拉起来”。如果业务允许丢 5 分钟,你完全没必要做到每秒级增量;如果只能接受秒级丢失,那光靠每天一次全量备份肯定不够,需要额外设计增量和日志归档环节。
很多人的误区是上来就研究blockcommit和dirty bitmap,结果方案能不能满足业务要求完全没验证。我建议把备份需求写清楚:全量备份频率、增量备份频率、保留份数、恢复演练周期、备份存储位置是否跨主机。这些数字定了,技术方案才能落地。否则今天备份成功,明天出了事故才发现恢复时间根本达不到要求,那才是最尴尬的。
2. 原理补课:外部快照、写时复制和快照链
2.1 秒级快照不是魔法,是 COW
qcow2 是 QEMU 的镜像格式,核心思路是写时复制(Copy-on-Write)。虚拟机要写入一个块时,QEMU 会把数据写到新分配的 cluster,同时更新元数据表。外部快照本质上就是创建一个空的 overlay 文件,把虚拟机的当前写入指针切到 overlay,底层镜像变成只读。
这个创建过程不复制任何数据,只是几个文件操作,所以能做到秒级。你看到的“快照瞬间”数据,其实就是底层镜像此刻的状态;之后的新写入全部落在 overlay 文件里。这也是为什么在线快照不会让虚拟机明显卡顿,至少快照本身不会。
但要注意,由于 overlay 只记录变化块,链越长,读取某个块时要一路往上翻的层数就越多。第一层两层影响不大,几十层之后,磁盘 IO 可能明显变慢。所以合并不是可选项,而是保持性能的必要动作。
2.2 外部快照才是在线备份的主流
qcow2 本身支持内部快照,就是把快照状态存在同一个文件里。内部快照不需要额外文件,但缺点是:快照数据占用同一个文件空间,文件一旦损坏,所有快照都跟着遭殃。更麻烦的是,备份和恢复时你没法单独抽出某一层。
外部快照则不同,每一层都是独立的 qcow2 文件,可以单独复制到备份机,也可以按层做增量。对在线备份来说,外部快照几乎是唯一合理的选择。你备份底层镜像得到的是某个时间点的完整状态,你备份 overlay 文件得到的是从底层到当前的变化量,这种天然的分层结构刚好对应全量和增量。
2.3 blockcommit 负责把临时层收掉
做完备份,临时 overlay 必须合并回底层镜像。blockcommit做的事情是把 overlay 中的变化写回 backing 文件,然后把链缩短。实际使用中,我通常用:
virsh blockcommit vm-name vda --wait --active --delete这个命令让 libvirt 等合并完成,然后删除已无效的外层文件。合并过程是在线完成的,QEMU 通过 blockjob 机制逐块拷贝,虚拟机不关机。但合并会带来额外的磁盘 IO 压力,建议放在业务低峰期执行,避免和高峰流量叠加。
如果你创建快照时带了快照元数据,合并完成后检查一下virsh snapshot-list vm-name。有残留就用virsh snapshot-delete vm-name 快照名 --metadata清理,不然快照列表会越积越多。
2.4 dirty bitmap 是增量备份的根
很多人问增量备份到底怎么做。qcow2 支持 dirty bitmap,也就是一张“这个块自上次备份以来是否改过”的位图。QEMU 在每次写入对应块时置位,备份程序按位图提取变化块。这个位图可以是持久的,写入 qcow2 文件里,重启后依然有效。
不过virsh对 bitmap 的直接操作能力有限,通常要用qemu-img或直接调 QMP 接口。很多开源备份工具也就是基于 dirty bitmap 做增量提取。
如果你不想一开始就深入 QMP,用“连续外部快照链”也能实现增量:第一次备份完整基线,之后每次创建新 overlay 时,把旧 overlay 文件当作增量备份出去。恢复时从完整基线开始,按顺序合并所有增量层。这种方式不需要额外工具,但链条管理要用心,别到最后自己都搞不清哪一层是哪一天的。
3. 在线备份方案选型:常见路线对比
3.1 停机备份只适合可以停机的业务
停机备份最简单可靠,关机、复制、开机,得到的一定是干净镜像。但它不叫在线备份,对有 SLA 的虚拟机来说通常是不可接受的。如果业务允许深夜停几分钟,停机备份的性价比仍然最高,哪怕你已经有在线备份脚本,定期做一次冷备也是很好的双保险。
3.2 路线 A:guest agent 冻结 + 外部快照 + 导出 + 合并
这是我最常用的路线,也是后面脚本的主干。完整的步骤是这样:
- 确认 guest agent 在线,确保能冻结文件系统;
- 创建带
--quiesce的外部快照,让虚拟机内部文件系统先落到一致状态; - 备份此时已经变成只读的底层镜像;
- 合并临时 overlay,恢复原来的单层磁盘结构。
这条路线全是开源工具,过程透明,方便排查。缺点是每个环节都要自己写流程,备份脚本里的异常处理很重要。对数据库应用来说,--quiesce只能保证文件系统层面冻结,数据库层的一致性还需要业务配合。
3.3 路线 B:virsh blockcopy 直接镜像
virsh blockcopy可以把运行中的虚拟磁盘在线复制到另一个文件,不需要创建快照链。基本命令是:
virsh blockcopy vm-name vda /backup/vm-full.qcow2 --wait --verbose复制完成后,源盘继续正常使用,备份文件是独立镜像。如果加--pivot,则会把虚拟机的活动磁盘切换到备份文件,原盘不再活跃。这个方案的优势是没有 overlay 要合并,不易出现快照链残留问题;劣势是整块盘复制,IO 开销比快照方式大,而且同样需要配合 guest agent 才能获得一致性。
如果你的需求就是单机简单备份,不想研究快照链管理,blockcopy会更容易理解。它的备份结果是一个完整独立文件,直接用就行。
3.4 路线 C:LVM 快照和存储侧快照
如果虚拟机磁盘本身放在 LVM 卷上,可以用lvcreate -s做 LVM 快照。它同样秒级,对上层基本不可见。但 LVM 快照也是 COW,空间会随写放大增长,快照卷满了会失效,必须给足余量并监控。
存储阵列快照更强大,能从硬件层做一致性快照,性能影响小,也不占本地磁盘。但不是所有环境都有存储阵列,而且要记住:底层用哪种快照,虚拟机内部如果不配合冻结文件系统,拿到的仍然只是崩溃一致性。guest agent 的价值在任何路线里都不该被忽略。
我简单做个对比:
| 路线 | 一致性控制 | 额外依赖 | 增量能力 | 上手难度 |
|---|---|---|---|---|
| 外部快照 + convert | guest agent | qemu-img / libvirt | 链式或 bitmap | 中 |
| blockcopy | guest agent | blockjob | 全量为主 | 低 |
| LVM / 存储快照 | 崩溃一致为主 | LVM / 存储 | 取决于存储 | 中 |
3.5 我的选择
个人环境、多虚拟机批量备份,我选路线 A。理由是可编程、源镜像路径可控、方便扩展链式增量。如果需求就是单机简单备份,路线 B 也不错。至于现成的虚拟化备份平台,解决的是调度、去重、集中还原和报表,预算允许且规模上去了再考虑。手写脚本没有你想象的省心,但能让你真正理解每一步在做什么,出了问题至少知道往哪里查。
4. 可复现的在线备份流程:从手工命令到脚本
4.1 环境准备:guest agent 必须在线
前提比较简单:宿主机有 libvirt 和 qemu-img,目标虚拟机安装了qemu-guest-agent。没有 agent,--quiesce会直接失败。
先确认 agent 在线:
virsh qemu-agent-command vm-test '{"execute":"guest-ping"}'期望输出是{"return":{}}。如果失败,进虚拟机检查qemu-guest-agent.service是否启动,顺便确认虚拟机 XML 里是否配置了 virtio-serial channel。实际踩坑中,最常见的不是没装 agent,而是 agent 装了但服务没起来,或者旧版本 agent 和宿主机 QEMU 版本不匹配导致某些命令不受理。
4.2 手工走一遍:冻结、快照、导出、合并
第一步,查看虚拟机磁盘信息:
virsh domblklist vm-test --details记下磁盘的 Target 和 Source。我习惯先看一遍,确认镜像路径没有因为上次操作漂移。
第二步,创建带冻结的外部快照:
virsh snapshot-create-as vm-test bk-manual-$(date +%F-%H%M) --disk-only --atomic --quiesce执行成功后,libvirt 会在原镜像旁边生成一个 overlay 文件,原镜像变成底层只读镜像。这时候底层镜像就是一致性的快照点。
第三步,导出备份:
qemu-img convert -O qcow2 /var/lib/libvirt/images/vm-test.qcow2 /backup/vm-test-manual.qcow2用convert而不是直接cp,是因为 convert 会按 qcow2 分配表读取数据,生成紧凑的独立镜像;直接cp会把整个底层文件原样复制,文件里可能有很多已分配但没实际数据的空洞,备份体积偏大。
第四步,合并 overlay:
virsh blockcommit vm-test vda --wait --active --delete合并完成后,virsh domblklist vm-test应该只显示原镜像路径,overlay 文件消失。最后检查快照列表是否有残留:
virsh snapshot-list vm-test有残留就用virsh snapshot-delete vm-test 快照名 --metadata清掉。
这里有个体验:如果你用--quiesce时 guest agent 不可用,快照命令会直接报错,不会产生半成品快照,这比手动调guest-fsfreeze-freeze和thaw更安全。手动冻结最大的风险是命令写到一半失败,guest 永远处于冻结状态,业务直接停摆。新手不要轻易手动调冻结接口。
4.3 整理成脚本:失败要能停,日志要能查
基于上面的流程,我整理了一个脚本。为了不啰嗦,只处理第一块磁盘,也不做多盘循环,生产环境多盘请把核心操作包在 for 循环里。
#!/bin/bash set -euo pipefail VM="${1:-vm-test}" BACKUP_DIR="/backup/kvm" TS="$(date +%Y%m%d-%H%M%S)" SNAP_NAME="bk-$TS" DISK_TARGET="$(virsh domblklist "$VM" --details | awk '$2=="disk" {print $3; exit}')" DISK_SOURCE="$(virsh domblklist "$VM" --details | awk '$2=="disk" {print $4; exit}')" mkdir -p "$BACKUP_DIR" echo "[$(date '+%F %T')] snapshot start" virsh snapshot-create-as "$VM" "$SNAP_NAME" --disk-only --atomic --quiesce echo "[$(date '+%F %T')] export base image" qemu-img convert -O qcow2 "$DISK_SOURCE" "$BACKUP_DIR/${VM}-${TS}.qcow2" echo "[$(date '+%F %T')] commit overlay" virsh blockcommit "$VM" "$DISK_TARGET" --wait --active --delete virsh snapshot-delete "$VM" "$SNAP_NAME" --metadata 2>/dev/null || true echo "[$(date '+%F %T')] backup done: $BACKUP_DIR/${VM}-${TS}.qcow2"脚本开头用了set -euo pipefail,任何一个命令失败都会让脚本退出,避免“快照建了、备份没做、overlay 还堆在那边”的情况。日志也直接打到标准输出,配合 crontab 重定向到文件,方便排查。
如果文件系统支持 reflink,也可以考虑cp --reflink=auto快速创建副本,但要注意目标文件系统兼容性和后续迁移。大多数场景我仍然推荐qemu-img convert,因为结果文件是独立完整的,不依赖原镜像是否存在。
4.4 多盘和增量备份的思路
上面的脚本只处理第一块磁盘。生产环境如果系统盘和数据盘分开,快照命令会为每块盘生成 overlay,导出和合并都要对所有盘执行。快照创建时是一次性对所有磁盘执行的,所以一致性窗口是同一个,这一点放心;但备份和合并循环时别漏盘。数据盘往往比系统盘更重要,只备系统盘是很多备份事故的根源。
增量备份最简单可行的做法是链式快照:第一次备份完整基线,之后每次对当前活动层再创建一个新 overlay,并把旧 overlay 文件作为增量备份出去。链条太长时做一次 blockcommit 合并,或者重新做一次全量备份重置链。
这个方案不需要深入 QMP,上手容易。缺点也很明显:恢复时要从基线开始,按顺序处理所有增量层,任一增量文件损坏都会影响后面的数据。自己写增量之前,一定先在测试环境完整演练“基线 + 增量1 + 增量2”的恢复流程,线上出问题才不至于干瞪眼。
5. 还原演练:备份能不能用,只有还原过才算数
5.1 用临时虚拟机验证备份文件
备份文件不能只留在磁盘上,必须开机验证。最快的方法是把备份文件转换成可用的镜像,然后 import 启动一个测试虚拟机:
qemu-img convert -O qcow2 /backup/kvm/vm-test-20250101-120000.qcow2 /var/lib/libvirt/images/restore-test.qcow2 virt-install --import --name restore-test --ram 1024 --vcpus 1 \ --disk path=/var/lib/libvirt/images/restore-test.qcow2,format=qcow2 \ --network bridge=virbr0 --noreboot virsh start restore-test系统启动后,不只是看进程起来了。如果是数据库应用,要检查关键表是否能查、事务日志是否连续、是否出现异常回滚。只确认“能开机”远远不够,业务的“能服务”才是还原成功的标志。
验证完记得清理测试机:
virsh destroy restore-test virsh undefine restore-test --remove-all-storage5.2 正式覆盖恢复的正确姿势
正式恢复前,先把原镜像文件改名或移到别处,不要直接覆盖。然后关机、替换镜像、启动。如果因为原文件被占用导致启动失败,确认虚拟机状态是 shut off,不是 paused 也不是 running。
恢复过程中如果目标盘的设备路径变了,启动顺序可能受影响。比如原来是 vda 启动,恢复时变成了 vdb,需要检查引导配置。这个细节在迁移和扩容时特别容易踩,别等启动失败才想起来。
5.3 常见还原失败和排查
| 症状 | 可能原因 | 处理办法 |
|---|---|---|
| 启动报不认识镜像格式 | 域配置为 raw,备份文件是 qcow2 | 修改 disk driver type;或恢复时 convert 成 raw |
| 启动报 backing file 找不到 | 增量 overlay 恢复时基线路径不对 | 用 qemu-img rebase -u -b 指向正确基线 |
| 镜像能启动但数据库异常 | 快照只有崩溃一致性 | 用数据库日志工具恢复;以后备份前配合业务锁/归档 |
| qemu-img check 报错 | 备份过程 IO 异常或空间不足 | 用 qemu-img check -r all 尝试,不行就重做备份 |
无论哪种备份工具,都要接受一个现实:备份文件本身也可能损坏。所以备份完成后顺手执行qemu-img check -n 备份文件很有必要。有条件的话,把备份复制到另一台机器,防止宿主机磁盘损坏把数据和所有备份一起带走。
6. 实战中的坑和建议
6.1 快照链越拉越长
快照不合并,刚开始只觉得多几个文件,几个月后虚拟机磁盘 IO 明显变慢,备份时间越来越长。脚本里的blockcommit是收尾关键,一定要确保每次执行成功,并检查 overlay 文件已经消失。如果失败了,别直接跑下一次备份,先查日志把链修复,否则下一次快照会在更长的链上继续叠加。
6.2 guest agent 失联导致备份中断
常见场景:测试机没问题,生产机执行备份时--quiesce失败。原因可能是 guest agent 服务没随开机启动,虚拟机内部有进程频繁占用文件系统导致冻结超时,或者虚拟机 XML 里缺少 channel 配置。建议备份前加guest-ping检查,失败就发告警并跳过这次备份。
如果你的业务是数据库,别降级成不带--quiesce的快照。崩溃一致性备份对数据库来说非常危险,宁可这次没备份,也不要制造一个看起来能用、实际恢复时才发现问题的假备份。
6.3 备份目录和镜像目录放在同一块盘
这是最容易被忽视的坑。备份目录和原镜像目录在同一个文件系统时,convert 一个大镜像可能把宿主根分区直接打满。空间一满,正在运行的虚拟机 IO 就会报错,后果比不备份更严重。
我的做法:镜像目录和备份目录放不同磁盘池;脚本在 convert 前用df -h检查剩余空间;备份空间按底层镜像当前已用容量再加 20% 余量来预估。不要等告警响了才处理,磁盘满的瞬间往往是连锁故障的开始。
6.4 给新手的建议清单
- 先在测试虚机上完成“备份 → 还原 → 数据检查”全流程,再上生产。
- 备份脚本必须写日志,包含退出码、快照名、文件大小、耗时。没有日志的脚本等于没有。
- 保留策略要明确,例如每天全量保留 7 天、每周全量保留 4 周、每月全量保留 3 个月,并让脚本自动清理过期文件。
- 定期执行真实还原演练,不要只在备份完成后看一眼文件大小就放心。
- 多用
qemu-img info --backing-chain查看自己维护的是一条什么样的链,掌握好底层结构才能不被快照问题困住。
最后说点个人体会。从手工敲virsh命令到把脚本跑进 crontab,我踩得最多的不是工具不好用,而是对一致性理解不到位。每次备份前我都会确认 guest agent 在线、底层镜像路径没变、备份目录空间够,三件事都确认完才开始跑。在线备份本来就是一个平衡窗口和开销的过程,没有一劳永逸的命令,只有适合你业务节奏的流程。下一篇我会继续写增量备份和基于 dirty bitmap 的进阶玩法,如果你也在维护 KVM/QEMU 备份,欢迎一起交流各自积累的命令和教训。