KVM/QEMU虚拟化在线备份方案:外部快照与文件系统冻结实战 线上环境跑着 KVM/QEMU 虚拟机老板丢过来一句话“数据不能丢但服务也不能停。”这事儿听起来简单做起来全是坑。KVM/QEMU 在线备份就是在不关机、不打断业务的前提下拿到一份一致性、可用性都说得过去的备份数据。作为一个常年和虚拟化运维打交道人我可以直接告诉你真正的难点不是“怎么把文件拷出来”而是“怎么保证数据拷出来之后还能正常启动、还能恢复”。这篇文章适合正在维护 KVM/QEMU 生产环境的运维也适合自己搭私有云、玩了很久但始终没解决热备问题的开发者。我尽量把原理、命令、坑位一次讲透。1. 为什么说在线备份是第一优先级1.1 直接拷贝磁盘文件等于备份了一堆垃圾数据很多人第一反应是虚拟机不就是个文件吗cp一下不就完事了。同一个 qcow2 镜像虚拟机正在运行的时候内存里的脏页、文件系统缓存、日志缓冲区随时在往这个文件写数据。你cp的速度赶不上写入速度最后拷出来的文件可能正好停在某个中间状态目录项和位图不一致、数据库 WAL 日志写到一半、qcow2 的 L1/L2 表不完整——这种备份拿去恢复运气好能启动运气不好直接损坏。更隐蔽的问题是 qcow2 元数据本身。它靠 L1/L2 表映射逻辑块和物理块拷贝过程中如果元数据在变你拿到的就是一张前后对不上的映射表。稍不留神虚拟机看起来启动了一访问某个数据块就 I/O 错误。所以“不能直接 cp”并不夸张而是要意识到备份的是一台一直在动的机器不是在存静态文件。1.2 停服备份的窗口在真实业务里根本等不起理论上最安全的备份方式是先把虚拟机关机或挂起再做完整镜像拷贝。这样的话磁盘状态完全一致文件系统干净恢复也没有兼容性问题。但问题在于“关机窗口”在业务侧往往是一个不可接受的代价。现实中我见过很多场景白天跑业务晚上有定时任务周末有批量结算。你根本找不到一个真正空闲的时间段去停虚拟机。即使找到了一旦备份任务失败要重跑窗口就得跟着顺延业务方很快就会有意见。更惨的是等你终于把备份做完恢复演练又发现数据不一致那整个“备份”就只剩心理安慰作用了。所以在生产环境里在线备份不是“花活”而是刚需。大家需要的不是一种能用的技术而是一套能重复执行、出错可恢复、经过恢复验证的流程。2. 三种主流在线备份方案别一上来就选最难的这里我不会用那些听着高大上但实际很少有人跑通的做法只挑真实环境里验证过、能落地的三类方案agent 冻结加拷贝、外部快照加合并、原生位图做增量。搞清楚区别你才知道自己的场景该选哪条路。2.1 方案一guest agent 冻结文件系统后直接拷贝这个思路最容易理解既然文件系统在写那就先让 guest 系统里的文件系统“停下笔”。给虚拟机安装 qemu-guest-agent备份之前向 agent 发指令guest 内部的 OS 会调用 fsfreeze 冻结文件系统把缓存刷到磁盘暂时冻结写操作。冻结之后再进行磁盘拷贝拷贝完成后再解冻。优点在于实现简单、依赖少备份产物是一个扁平完整的 qcow2 或 RAW 镜像不需要维护快照链。缺点也很明显冻结时间不能太长否则业务写入被卡住等于变相停机。如果磁盘很大、I/O 很慢拷贝时间一长照样扛不住。所以它更适用于中小型虚拟机或者磁盘内容本身不大、能快速完成拷贝的场景。2.2 方案二外部快照加自动合并我的主力方案这个方案适合大多数生产场景思路非常巧妙。创建外部快照时KVM 会把当前活跃磁盘从业务盘变成 backing file然后生成一个新的 overlay 磁盘继续承接新的写入。老磁盘文件在快照创建后就不会再变了哪怕虚拟机还在跑只要新写入都落在 overlay 里老文件就是安静的、可安全备份的。这样你的备份时间窗口就等于“拷贝老文件的时间”业务写入完全不受影响。拷贝完成后再用 blockcommit 把 overlay 合并回原磁盘恢复成单磁盘文件继续给下次快照用。这套流程兼顾了两头备份端拿到的是稳定数据业务端几乎没有感知。这也是我在后面实操部分重点展开的方案。2.3 方案三基于 dirty bitmap 的原生增量备份如果虚拟机数量很多、磁盘很大每次都做全量拷贝肯定不合适。QEMU 从 4.0 开始支持 persistent dirty bitmapqcow2 磁盘可以挂一个位图记录“哪些块被修改过”。备份时拿着位图可以和上次备份时的位图做差集只拷增量区域从而把备份周期压缩得很短。但位图方案有个前置条件备份链路的初始全备份必须基于一个干净的位图状态以后的每次增量都围绕它累积。如果某次增量备份失败位图又没有正确推进后面的数据可能全部错位。这个方案的成熟度也在逐步提高新版本 libvirt 已经有了封装命令但它对链路的完整性要求更高对备份平台或脚本的容错设计要求也更严。2.4 三个方案怎么选看这张对比表方案一致性保障业务影响备份粒度运维复杂度适合场景agent冻结拷贝高冻结期间写停顿全量低中小盘、低频备份外部快照合并高几乎无感知全量中生产主力、常规虚拟机dirty bitmap增量高极短增量高大磁盘、高频备份3. 手把手实操agent 冻结加外部快照加自动合并下面这套是我最常用、也反复验证过的流程。前提是你的宿主机已经安装了 libvirt 工具链虚拟机磁盘格式是 qcow2guest 系统里运行着 qemu-guest-agent。这三项缺一不可RAW 磁盘无法直接做外部快照agent 没装则无法在文件系统层面冻结掉缓存。3.1 备份前的环境检查先看看虚拟机列表和磁盘配置virsh list --all virsh domblklist 虚拟机名称正常情况下输出里会有 vda 或 sda 这种磁盘目标别名对应后面的操作。磁盘格式必须确认是 qcow2qemu-img info /var/lib/libvirt/images/虚拟机名称.qcow2如果输出里显示file format: qcow2继续。如果显示raw要么先做格式转换要么换一个支持 RAW 的备份方式否则外部快照创建会失败或行为不符合预期。接着检查 guest agent 是否在线virsh qemu-agent-command 虚拟机名称 {execute:guest-ping}如果返回return: {}说明 agent 是通的。不通的话后面的冻结步骤没法做你只能退回到内部快照或直接拷贝风险更高的方案。3.2 冻结文件系统锁住一致性窗口冻结前如果 guest 里跑的是数据库强烈建议手动先做一层数据库级的落盘。以 MySQL 为例FLUSH TABLES WITH READ LOCK之后再刷一次 binlog。数据库可能还有自己的内存缓冲光靠文件系统冻结虽然能保证磁盘角度一致但数据库日志和应用数据之间的顺序关系最好由数据库自己先收敛。完成应用层处理后执行冻结virsh qemu-agent-command 虚拟机名称 {execute:guest-fsfreeze-freeze}老版本 libvirt 也有兼容命令virsh domfsfreeze 虚拟机名称。冻结成功后guest 内文件系统对外表现为只读所有脏数据都会刷盘。这里要特别提醒冻结时间越短越好一般建议不超过 30 秒。如果磁盘文件特别大、备份流程预计很久就不要选择这种方式老老实实走外部快照。3.3 创建外部快照把生产盘“换岗”执行下面这条命令创建 disk-only 外部快照virsh snapshot-create-as 虚拟机名称 backup-$(date %Y%m%d%H%M) --disk-only --atomic--disk-only表示只对磁盘做快照不冻结内存。--atomic保证要么所有磁盘都完成快照要么一个都不做避免多磁盘虚拟机出现一部分快照成功、一部分失败的情况。执行成功后查看磁盘链路virsh domblklist 虚拟机名称你会发现原来的 vda 对应的镜像变成了一个新增的 overlay 文件。此时原 qcow2 文件的角色已经变成 backing file新写入全部落到 overlay原文件不再变化。从这一刻起你对原文件的任何拷贝都是基于一个“静止”的数据面这就是在线备份的窗口。提示如果快照创建命令报错大概率是磁盘格式不支持或目标分区空间不足先检查 /var/lib/libvirt/qemu/snapshot 目录权限和宿主机的剩余空间。3.4 把底层镜像安全地拷贝出来冻结加外部快照完成后就可以拷贝原 qcow2 了。同步拷贝的命令用 rsync加上-avP看进度rsync -avP /var/lib/libvirt/images/虚拟机名称.qcow2 /备份目录/虚拟机名称-$(date %Y%m%d).qcow2原镜像此时是静止的所以这次拷贝是稳定、一致的文件副本。拷完别急着收工做一个基本校验qemu-img check /备份目录/虚拟机名称-$(date %Y%m%d).qcow2这一步能检查镜像文件有没有基础逻辑错误。如果 guest 是数据库建议再用qemu-img info --backing-chain看一眼镜像链信息确认有没有断链风险。3.5 合并快照恢复生产链路备份文件已经拿到了但宿主机的磁盘现在还是两层结构base 加 overlay。必须把它合并回单一活跃磁盘否则每次备份都会叠加快照链到最后链太长、性能下降、吃满磁盘空间。执行 blockcommit 合并并让活跃层重新切回原镜像virsh blockcommit 虚拟机名称 磁盘目标如vda --wait --verbose --pivot--wait让命令一直阻塞到合并结束--verbose输出实时进度--pivot表示合并完成后把活跃层直接切换到 base 文件。合并完成后你可以再次执行virsh domblklist确认磁盘路径已经回到原来的 qcow2。此时备份目录里已经有了一份可用镜像宿主机上的虚拟机也恢复成单层磁盘结构整条链路清爽干净。剩下的就是验证恢复。这里有个非常推荐的做法把备份文件转换成一个独立镜像然后克隆一台测试机来启动验证。qemu-img convert -O qcow2 /备份目录/虚拟机名称-$(date %Y%m%d).qcow2 /备份目录/虚拟机名称-flat.qcow2flat版本不依赖任何 backing file可以直接挂到新虚拟机上做启动测试。没有独立测试环境的也可以用guestmount把镜像挂载到宿主机查看关键文件确认目录、配置、数据库文件都还在。3.6 数据库场景的额外提醒上面这套流程对普通 Web 服务问题不大但如果是数据库虚拟机只做文件系统冻结还是不够严格。常见数据库都有自己的一致性约定。MySQL 建议用mysqlbackup或物理备库来代替裸备份Oracle 用 RMANPostgreSQL 用pg_basebackup。如果坚持用磁盘快照就必须确保 binlog/WAL 与数据文件在备份恢复后能对上。更稳妥的做法是冻结前先触发FLUSH TABLES WITH READ LOCK刷新二进制日志然后同步执行 fsfreeze最大限度缩小两份日志之间的时间差。这点踩过坑的人都懂光靠外力冷冻文件系统数据库日志和数据文件之间还是存在毫秒级的竞争窗口。4. 备份自动化给脚本加一道保险4.1 手动流程能跑通之后就该考虑自动化了生产环境不能每次都靠人肉敲命令总要落到脚本和定时任务上。脚本需要考虑的点不多但都是关键备份前检查 agent 状态、创建外部快照、拷贝、合并、清理过期备份、记录日志。我建议你不要把备份和合并写成一个脚本直接跑到底而是分成两个独立步骤备份成功后先合并失败则报警并保留快照状态等人处理。理由很简单万一合并步骤出问题至少你的 base 镜像还在overlay 还在还有机会通过手动方式恢复。4.2 一个可用的备份脚本思路下面是核心流程的浓缩版实际使用时把虚拟机名、路径替换成自己的#!/bin/bash VMweb-biz-01 DOMBLK$(virsh domblklist $VM | awk /vda/{print $1}) DATE$(date %Y%m%d%H%M) BACKUP_DIR/data/backup/$VM # 1. 检查 agent 可响应 virsh qemu-agent-command $VM {execute:guest-ping} || exit 1 # 2. 创建外部快照 virsh snapshot-create-as $VM backup-$DATE --disk-only --atomic || exit 1 # 3. 拷贝 base 镜像 rsync -avP /var/lib/libvirt/images/$VM.qcow2 $BACKUP_DIR/$VM-$DATE.qcow2 # 4. 校验 qemu-img check $BACKUP_DIR/$VM-$DATE.qcow2 /dev/null || exit 1 # 5. 合并快照恢复到活跃盘 virsh blockcommit $VM $DOMBLK --wait --verbose --pivot || exit 1 # 6. 清理快照记录 virsh snapshot-delete $VM backup-$DATE --metadata这个脚本在合并失败时会让整个流程终止并且不会删除快照。人工介入时可以先用virsh snapshot-list查看快照再手动执行 blockcommit 恢复不会陷入“数据都不知道去哪”的窘境。4.3 保留策略与定时任务定时任务用 crontab 加一行就行0 2 * * * /usr/local/sbin/kvm-backup.sh /var/log/kvm-backup.log 21建议在每天的凌晨低峰期执行避开业务高峰。保留策略就在这里体现脚本里只放最新备份的产出没有自动清理旧备份的话磁盘早晚会被填爆。保留策略我常用的是“最近 3 天全量 最近一周基础保留”也就是备份目录里只留近 3 次文件超过 7 天的直接删除。如果业务合规要求更长的历史就别只依赖宿主机本地盘备份到独立存储或对象存储再讲策略宿主机本地盘并不适合做长期归档。脚本里加清理的时候也要注意只删文件不碰虚拟机的运行态。绝不能因为清理备份把快照删了如果脚本崩溃在合并前快照链必须保留待查。5. 生产环境常见问题排查与避坑5.1 快照合并时报“磁盘忙”或超时blockcommit 合并快照时如果虚拟机磁盘 I/O 压力大可能出现 I/O 阻塞或超时。这时候先别慌blockcommit没有回滚失败后 overlay 和 base 都还在重试一次一般就行。也可以先调低合并带宽再试一次virsh blockcommit 虚拟机名称 vda --wait --verbose --pivot --bandwidth 104857600单位是字节每秒上面这条限速 100MB/s理论上比抢占所有 I/O 更稳。排障完记得看 QEMU 日志确认合并进程是否还挂着如果还有残留 job等它结束再操作。5.2 备份文件恢复后网卡对不上很多人在恢复测试时中招备份是正常的但克隆出来的虚拟机启动后 network 起不来原因是新虚拟机的 PCI 设备地址和原虚拟机不同guest 里配置的 udev 规则或 netplan 还在按原地址识别。解决思路有两个恢复前修改新 VM 的 XML把网卡模型和 PCI 槽位尽量对齐或者恢复后在 guest 里替换网卡规则本质上属于 guest 内网卡 MAC 绑定的问题。这个不是备份流程的错误只是很多人会误以为备份坏了。5.3 冻结失败或 agent 无响应qemu-guest-agent 长时间没更新、版本和 QEMU 不兼容或者 guest 系统负载极高时freeze 指令可能超时。冻结失败时绝对不要强行拷贝因为底层数据还在动。更好的做法是退回到“不冻结但创建外部快照”的流程因为外部快照本身也能提供一个静止面。只是这个静止面的意义在于“文件和元数据的不变性”而不是 guest 内部文件系统的逻辑一致性。数据库类场景对内部一致性要求高的宁可不做备份也不要拿一个坏快照糊弄自己。5.4 磁盘空间被快照撑爆这是外部快照最常见的事故。快照创建后如果不及时合并或者重叠的备份周期没错开磁盘空间会被 overlay 文件急剧占满。有一次我在某环境里发现一天一次快照备份且一直没合并三天后业务虚拟机把磁盘撑到了 90% 以上。原因就是每次新快照都生成新 overlay大量写入全落在这些覆盖盘上guest 里的删除操作也只会让原始文件“变墓碑”在 overlay 里标记删除实际空间不回收。规避方式就是严格执行“备份完即刻合并”的原则同时用监控盯住 qcow2 overlay 文件的体积。我建议在 zabbix 或 prometheus 里加一条针对快照目录的磁盘使用率告警阈值设 70%别等满了再处理。5.5 疑难杂症速查表现象最常见成因处理建议snapshot-create-as 报错磁盘格式非 qcow2先转换格式或换方案agent 冻结超时agent 未更新/系统繁忙检查 agent 版本退回快照方案blockcommit 卡住磁盘 I/O 压满限带宽重试恢复后服务起不来网卡规则或磁盘标识符不一致调整新机配置备份文件 qemu-img check 报错冻结前没有等缓存刷完检查冻结时机重新备份说实话KVM/QEMU 在线备份这条路方案本身并不复杂难的是把每个细节都钉死冻结时机、快照合并时机、备份验证习惯。我自己的体会是备份做完不测试等于白做备份脚本上线后至少每个月手动恢复一次。上面这套外部快照加 agent 冻结的流程已经在不少中小型生产环境里跑得很稳如果你也是维护 KVM 的兄弟可以从这套先起步跑通后再去碰位图增量。后续等我继续更新这个系列再聊聊增量备份和远程容灾的扩展玩法。