
1. 先搞清楚KVM虚拟机的“实体”由哪几部分组成你要导出和导入KVM虚拟机第一件必须想明白的事是一台KVM虚拟机到底由什么构成很多人一开始就把这事想复杂了觉得虚拟机是个黑盒子导出导入得用什么神秘工具。实际上KVM虚拟机的本质就是两个东西一份描述硬件配置的XML文件加上一个或多个磁盘镜像文件。就这么简单。XML文件里记录了虚拟机的“虚拟硬件配置”——分配了几颗CPU、多大内存、几块网卡、每块网卡连到哪个虚拟交换机、硬盘挂在哪个控制器上、用什么固件启动BIOS还是UEFI、有没有配置VNC/SPICE显示等。这份XML就是你虚拟机的“身份说明书”。而磁盘镜像文件通常是qcow2或raw格式也可能是一堆LVM卷或逻辑设备则是虚拟机操作系统和数据的存放地相当于物理机的硬盘。很多教程一上来就让你备份/var/lib/libvirt/images/下面的镜像文件这没错但如果不同步备份XML等你拿到新机器上virsh define的时候就会发现各种对不上——磁盘路径不对、网卡型号不匹配、virtio驱动没加载启动不了。这里有个很关键的认知KVM本身不直接管理虚拟机真正干活的是libvirt。你执行virsh list --all看到的所有域domain其实都是libvirt读XML后通过KVM内核模块帮你创建出来的。所以导出导入的完整链路就是导出把XML和磁盘镜像从源主机完整拷贝出来导入在新主机上用libvirt重新“注册”这台虚拟机。我在实际运维中见过太多人把导出做成了“只拷贝镜像文件”结果到了目标机器上要么virsh define报错要么启动后网卡起不来。所以本文第一个核心观点**导出虚拟机XML和镜像缺一不可。**后面所有操作都围绕这两部分展开。1.1 查看虚拟机当前配置与磁盘路径在动手导出之前先执行几个基础命令确认现状# 查看当前宿主机上所有虚拟机及其运行状态 virsh list --all # 导出某台虚拟机的完整XML含运行时动态配置 virsh dumpxml 虚拟机名称 # 只导出静态配置不含运行中的临时信息 virsh dumpxml --inactive 虚拟机名称 # 查看该虚拟机所用的所有磁盘设备及实际文件路径 virsh domblklist 虚拟机名称domblklist这步特别容易被忽略。你以为磁盘在/var/lib/libvirt/images/下面实际上完全可能在一个自定义存储池路径下或者直接指向一个LVM逻辑卷比如/dev/vg0/vm-data。我曾经碰到过一台机器磁盘是直接挂载在NFS共享存储上的路径写成/mnt/nfs/vm01/disk.qcow2如果只拷贝本地目录当然什么也导不出来。另外注意dumpxml和dumpxml --inactive的区别。对正在运行的虚拟机缺省dump出来的XML会包含一堆运行时临时属性比如viridian、hyperv相关的动态特征导入时反而可能造成兼容性小问题。导出迁移用的备份我习惯统一用--inactive拿到的才是干净、可移植的静态定义。1.2 磁盘镜像格式确认qcow2、raw还是LVM卷拿到磁盘路径后用下面的命令确认镜像的实际格式qemu-img info /var/lib/libvirt/images/your-vm.qcow2输出里有几个字段很关键file formatqcow2还是raw、virtual size虚拟机看到的磁盘大小、disk size实际占用宿主机多少空间、backing file如果有说明这个镜像基于某个基础镜像导出时一定把基础镜像也带上或者用qemu-img rebase先压平。这两个格式的差别会影响你导出时的策略项目qcow2raw空间占用按需增长初始很小文件创建时就占满除非稀疏文件性能略低于raw有写时复制开销接近物理盘性能更直接支持快照支持不支持内建快照导出拷贝拷贝后文件很小传输省时文件大但结构简单不易损坏这个表格不是让你纠结选哪个而是告诉你导出时的心态。qcow2镜像因为它的稀疏特性拷贝出来的文件大小可能远小于虚拟磁盘大小但正因为这种按需分配机制拷贝过程中如果源虚拟机还在跑很容易把镜像拷成损坏状态。所以除非你用了带一致性的存储快照方案否则导出前最好把虚拟机关机。2. 手工导出全流程停虚拟机、导配置、拷贝镜像现在进入正题。我下面写的是我这几年用得最顺手的“手工三板斧”停机、导XML、拷镜像。这套流程适用于CentOS/RHEL/Ubuntu/Rocky等几乎所有主流Linux发行版不依赖任何额外工具。2.1 导出前准备安全停机与数据落盘无论虚拟机跑了什么服务导出前先做一次优雅关机# 在虚拟机内部优雅关机或者在宿主机上执行 virsh shutdown 虚拟机名称 # 等待虚拟机真正进入关闭状态 virsh list --all | grep 虚拟机名称为什么强调“优雅关机”因为qcow2镜像的元数据和日志一致性需要操作系统主动进入关机流程。强制virsh destroy相当于物理机上直接拔电源下次导入启动时大概率要经历文件系统检查和修复运气不好直接起不来。停机后再确认一次磁盘信息同时把XML完整导出virsh dumpxml --inactive 虚拟机名称 /backup/kvm-exports/虚拟机名称-domain.xml这里有个实操细节导出的文件名我建议统一命名为虚拟机名称-domain.xml不要让每一台机器的配置文件都叫domain.xml否则你一次批量迁移十台虚拟机时根本分不清谁是谁。同时保留一份虚拟机名称-disk.txt记录磁盘路径、格式、大小virsh domblklist 虚拟机名称 /backup/kvm-exports/虚拟机名称-disks.txt qemu-img info /var/lib/libvirt/images/虚拟机名称.qcow2 /backup/kvm-exports/虚拟机名称-disks.txt这步看起来啰嗦但真到了半年以后要从备份里恢复某台虚拟机时你会感谢当初写了这份清单。2.2 拷贝磁盘镜像cp与rsync的取舍磁盘镜像拷贝我推荐用rsync而非cp尤其是大镜像# 用rsync -a保留文件属性适合大文件传输 rsync -avh --progress /var/lib/libvirt/images/虚拟机名称.qcow2 /backup/kvm-exports/ # 传输完成后做一次校验 sha256sum /backup/kvm-exports/虚拟机名称.qcow2拷贝完成后强烈建议做两件事第一确认目标文件大小和源文件一致且qemu-img info能正常读出格式信息。如果qemu-img info报“Could not open image”基本可以断定镜像损坏或没拷完。第二如果虚拟机有多个磁盘比如数据盘独立挂载一定把每块盘都拷全。我遇到过一台虚拟机系统盘和数据盘分别在两个目录运维导出时只拷了系统盘结果导入后数据库起不来排查半天才发现数据盘压根没迁移。2.3 为什么我用NFS中间目录做导出暂存如果你是在多台宿主机之间做迁移我的习惯是在双方都挂载的NFS共享目录里完成导出和导入。这样做的好处有三点镜像文件不用先下载到本地再上传直接在共享存储上完成校验。后续调整XML时目标宿主机能直接引用NFS上的镜像路径省去一次大文件拷贝。万一导入后发现缺文件还能在共享目录里快速补。没有NFS环境也不要紧rsync配合SSH远程拷贝一样能解决问题。关键是记住导出不追求花哨追求完整。3. 导入新主机define之前必须处理的三大隐患到目标宿主机后导入操作的基本流程是把XML和镜像放到目标位置执行virsh define然后启动。但如果你直接原样define大概率会遇到下面三个坑我逐个讲清楚。3.1 磁盘路径对不上修改XML中的source fileXML里磁盘路径是绝对路径。源主机拷贝过来的XML写的可能是/var/lib/libvirt/images/vm01.qcow2而目标主机的存储目录是/mnt/vm-storage/vm01.qcow2这时直接define会报错。处理方式有两种方式一手动改XMLvim 虚拟机名称-domain.xml # 找到类似下面的段落修改file属性 disk typefile devicedisk driver nameqemu typeqcow2/ source file/mnt/vm-storage/vm01.qcow2/ target devvda busvirtio/ /disk方式二用virsh edit配合sed处理适合批量调整sed -i s|/var/lib/libvirt/images|/mnt/vm-storage|g 虚拟机名称-domain.xml改完XML之后先做一个语法校验别急着define# 检查XML是否格式良好 xmllint --noout 虚拟机名称-domain.xml如果没有xmllint可以用python3 -c import xml.dom.minidom; xml.dom.minidom.parse(虚拟机名称-domain.xml)替代总之先排除低级的XML语法错误。3.2 MAC地址冲突两台机器联网后IP“打架”这是新手最容易忽略的问题。virsh dumpxml导出的XML里带着原虚拟机的MAC地址如果你原样导入而源虚拟机和目标虚拟机同时开机哪怕不同时只要DHCP有旧租约就会出现IP冲突或者新机器拿到了旧机器的IP导致网络混乱。处理思路是导入前删除XML里的MAC地址让libvirt在define时自动生成新的MAC或者手动改成一段你规划好的新地址。!-- 找到interface段删除或修改mac address -- interface typebridge mac address52:54:00:xx:xx:xx/ source bridgebr0/ model typevirtio/ /interface如果在同网段里做迁移且你希望保留原IP以便业务连续那就手动给新虚拟机分配一个不同但同网段的MAC地址注意别和任何现有设备冲突就行。3.3 固件与启动方式不匹配服务器上常见的现象是源主机用UEFI启动XML里有loader typepflash相关配置目标主机没有安装ovmf软件包或者反之——源主机用BIOS启动目标主机默认配置却指向UEFI。这种情况下virsh define能成功但一启动就黑屏或直接报Boot failed。排查方法# 查看XML的os段确认固件配置 grep -A5 -i os 虚拟机名称-domain.xml # 目标主机确认是否安装了OVMF ls /usr/share/OVMF/ 2/dev/null || rpm -qa | grep -i ovmf || dpkg -l | grep ovmf如果目标主机没有对应固件安装对应包# Debian/Ubuntu apt install ovmf # RHEL/Rocky/CentOS dnf install edk2-ovmf搞定固件后如果XML里UEFI段引用的路径不存在同样用sed或者virsh edit改路径。这一步问题排查起来很费时间因为virsh define通常不报错所有问题都在启动后才暴露。4. 完整导入与启动验证define只是开始处理完上面三个隐患才轮到正式导入命令。4.1 define、启动与状态检查# 注册虚拟机不会启动 virsh define /backup/kvm-exports/虚拟机名称-domain.xml # 确认已注册 virsh list --all | grep 虚拟机名称 # 启动 virsh start 虚拟机名称 # 启动后确认运行状态 virsh list --all定义完成并启动后别急着欢呼也没到做下一件事的时候先检查基本连通性。假如虚拟机IP是静态配置在系统里的穿透性用DHCP则不需要建议直接virsh console 虚拟机名称接入串口终端确认操作系统起来了、文件系统没报错、网络服务正常启动。如果虚拟机原本用了VNC或SPICE显示协议导入后还要确认XML中graphics段配置的端口没有冲突。libvirt默认会为每台新define的虚拟机分配一个随机端口但如果你的XML里写死了端口号两台虚拟机同时用同一个端口就会有一个显示连不上。4.2 导入后第一次启动很慢多半是存储路径IO问题我遇到过几次导入后启动极慢的情况虚机在grub界面停留了一两分钟才进系统。查了一圈问题是目标主机的镜像文件放在了机械硬盘上而源主机跑的是SSD。qcow2格式在随机读写场景对存储介质非常敏感为此专门把镜像转成raw格式才有所改善。如果你的业务是数据库、高IO负载导入后建议用qemu-img convert转raw或调整缓存策略# 将qcow2转为raw目标盘要有足够空间 qemu-img convert -O raw /mnt/vm-storage/vm01.qcow2 /mnt/vm-storage/vm01.raw # 修改XML中disk段的driver属性加上cache driver nameqemu typeraw cachewriteback/这只是其中一种优化思路具体取舍要看你的存储类型和业务压力不是所有场景都需要转raw。4.3 Windows虚拟机导入后激活失效的提醒如果迁移的是Windows虚拟机要提前给业务方打好招呼。Windows激活信息通常绑定到主板和BIOS特征换了宿主机之后几乎必然触发重新激活提示。如果你的Windows虚拟机是KMS激活那还好域内重新激活就行如果是零售密钥绕过可能得人工介入。这个事不属于KVM技术问题但对业务的影响不小导出导入方案里应该预留出激活重试窗口。5. 规模化迁移virt-clone和virt-sysprep帮你省事手工三板斧适合一两台机器的临时迁机如果你要做批量克隆比如把一台基准虚拟机复制成十台测试机再用copydefine就太累了这里推荐virt-clone和virt-sysprep组合。5.1 virt-clone的基本用法与原理virt-clone是libvirt工具集里的克隆利器它的核心逻辑是读取源虚拟机的XML自动生成新的名字、新的UUID、新的MAC地址、新磁盘路径然后基于源磁盘镜像做一份复制。一条命令就能完成“注册建盘”两件事# 安装 # Debian/Ubuntu: apt install virt-clone # RHEL/Rocky: dnf install virt-clone # 基本克隆 virt-clone --original 源虚拟机 --name 新虚拟机 --auto-clone # 指定新磁盘路径 virt-clone --original 源虚拟机 --name 新虚拟机 \ --file /mnt/vm-storage/新虚拟机.qcow2--auto-clone会自动在源磁盘同目录生成新虚拟机.qcow2适合快速出测试机。但要注意一点virt-clone默认是“完整克隆”不是链接克隆所以克隆大磁盘时等待时间取决于磁盘总大小。如果只是快速部署测试环境建议用--reflinkalways参数利用CoW特性节省空间virt-clone --original 源虚拟机 --name 新虚拟机 --auto-clone --reflinkalways这个reflink要求文件系统支持比如XFS的reflink能力但并不是所有文件系统都默认开启建议先确认你的存储格式。5.2 virt-sysprep克隆前必须做的系统清洗从基准虚拟机克隆出多台机器如果不做系统清洗会出现一堆问题机器名重复、SSH host key重复、机器ID一致、网卡配置文件指向同一块MAC。virt-sysprep就是干这个的——在克隆前把系统里所有“个性信息”抹掉让新虚拟机第一启动时重新生成virt-sysprep --domain 源虚拟机 --operation defaultdefault操作集包含重置SSH host key、清除机器ID、清理临时文件、清理日志、重置网卡MAC配置等。执行一次之后再克隆得到的每台新虚拟机独立性就好很多。这里有一个使用顺序问题先virt-sysprep清洗再virt-clone克隆。如果你克隆完了才想起来清洗那已经晚了——你是在新机器上清洗改的还是那份独立的副本等于每台都要处理一遍浪费时间和磁盘空间。5.3 批量导入场景下的脚本化思路如果你要一次性把十台虚拟机从旧宿主机迁移到新宿主机手工一个个virsh dumpxml再virsh define太痛苦。可以写一个简单的循环脚本#!/bin/bash # 批量导出 for vm in vm01 vm02 vm03; do virsh dumpxml --inactive $vm /backup/kvm-exports/${vm}-domain.xml virsh domblklist $vm done # 批量导入目标主机上执行 for xml in /backup/kvm-exports/*-domain.xml; do sed -i s|/var/lib/libvirt/images|/mnt/vm-storage|g $xml virsh define $xml done脚本本身不复杂但有个容易翻车的地方批量define时很可能某些XML里还带着源主机的个别特殊设备比如直通的PCI设备、USB重定向这些设备在目标主机上不存在define会直接报错。所以在批量导入前先在单台虚机上跑通完整流程确认XML中所有设备引用在目标主机都能找到再上脚本。6. 排错排查链路从define失败到启动异常的完整复盘最后这部分我按真实踩坑频率排个序把最常见的故障现象、原因和排查思路写清楚方便你出了问题时按图索骥。6.1 “Cannot access storage file”类报错virsh define或virsh start时报错信息里带Cannot access storage file或Permission denied十有八九是三个原因之一镜像文件路径和XML里的source file不一致。文件权限不对libvirt的QEMU进程没有读取权限。SELinux或AppArmor策略阻止访问。排查顺序建议# 第一步确认路径 cat /backup/kvm-exports/虚拟机名称-domain.xml | grep -A2 source # 第二步确认文件是否存在 ls -l /mnt/vm-storage/虚拟机名称.qcow2 # 第三步检查属主和权限RHEL系需要正确的SELinux上下文 ll -Z /mnt/vm-storage/虚拟机名称.qcow2在RHEL系宿主机上最坑的是SELinux。默认libvirt允许读取/var/lib/libvirt/images和/var/lib/libvirt/qemu等目录但如果你把镜像放在/mnt或/home下就可能被SELinux挡掉。推荐方案不是关掉SELinux而是给新目录打上正确的上下文semanage fcontext -a -t virt_image_t /mnt/vm-storage(/.*)? restorecon -Rv /mnt/vm-storage/Ubuntu系一般是AppArmor检查/etc/apparmor.d/下有没有对libvirt访问目录的限制必要时在配置里追加允许路径。6.2 启动时“No boot device available”镜像文件存在、路径正确、权限没问题但虚拟机启动时报找不到启动设备。这通常不是磁盘文件坏了而是XML里磁盘设备配置和源机不一致导致的。检查点集中在以下几处bus和driver type是否一致。比如源机用的是SATAbussata导入后XML里还是SATA目标主机的QEMU版本或机器类型变了可能导致设备识别异常。可以先试试改model type为兼容性更好的ide或保持virtio。机器类型machine type是否兼容。不同发行版的libvirt默认machine type可能不同比如pc-i440fx-6.1和pc-q35-8.2差异较大。如果源机用的是Q35芯片组目标库默认i440fx设备枚举顺序全变很容易出现找不到启动盘。# 查看机器类型 grep -i os -A5 虚拟机名称-domain.xml grep -i machine 虚拟机名称-domain.xml # 列出当前目标主机支持的machine type /usr/libexec/qemu-kvm -machine help # RHEL系 qemu-system-x86_64 -machine help # Debian系6.3 网卡起不来udev规则和MAC绑定Linux虚拟机导入后网卡起不来是非常高频的问题。原因通常是系统里/etc/sysconfig/network-scripts/ifcfg-*或/etc/netplan/配置的网卡名称ens3、ens5之类和XML导入后分配到的PCI槽位不一致或者udev规则里写了旧MAC对应的网卡名导致新网卡根本没被识别成eth0/ens0。处理方式有这么几条按便捷程度排第一在导入启动前就改XML让网卡的PCI地址尽量贴近源机配置。但PCI地址在目标主机上不一定能复现所以这个方案不稳定。第二更推荐的做法是导入后顺着系统内的网络配置排查将网络配置改成匹配新MAC或无MAC绑定的方式。比如RHEL系直接把ifcfg-ens3里的HWADDR一行删掉或者把NAME改成新识别出来的网卡名。第三对一些特殊系统比如BIOS里网卡顺序变化导致ens编号变化可以在内核cmdline里加上net.ifnames0统一使用旧式eth0命名但这属于“大动干戈”除非业务没得选否则不建议。6.4 console串口连不上导入后virsh console按回车没反应或者屏幕上什么都没有。排查思路是检查XML里是否配置了串口设备grep -A5 console 虚拟机名称-domain.xml如果整段都没有说明原虚拟机可能禁用了串口重定向或者用的是VNC/SPICE作为主控制台。此时要么连VNC察看要么在虚拟机内部配置好grub串口参数再迁移。如果你习惯用virsh console管理无显示环境的机器那么在源机上就要提前确认有这行console typepty target typeserial port0/ /console6.5 迁移后磁盘空间被占满虚拟机却看不到有一种情况经常被误解为导入失败目标主机上镜像文件占用的空间远大于虚拟机内部显示的已用空间。重启虚拟机后虚拟机内部磁盘满了但在宿主机上看qcow2文件也已经膨胀到实际分配大小。这其实是正常的——qcow2在被大量写入后占用的宿主空间会接近虚拟磁盘总大小尤其曾经做过删除操作但没有trim的情况下。需要收缩时用virt-sparsify --in-place /mnt/vm-storage/虚拟机名称.qcow2virt-sparsify能回收空闲块是配合迁移场景很实用的空间管理工具。不过注意在迁移前收缩会让拷贝文件小很多迁移后再收缩则是为宿主机腾空间。7. 给新手的最后几句实在话玩KVM导出导入这几年我最大的体会是这个操作并不难难点全在一个字——“全”。XML要全、镜像要全、固件类型要跟着全、MAC和网卡配置要想清楚、权限和SELinux上下文要处理全。任何一个环节漏了等虚拟机启动后才暴露问题排查代价往往是导出的好几倍。所以我现在每迁移一台重要虚拟机都会按下面这个清单校验一遍virsh dumpxml --inactive已执行且XML能正常xmllint。virsh domblklist输出的磁盘路径全部确认存在qemu-img info全部正常。目标主机已安装对应固件BIOS/OVMF。网卡MAC已妥善处理或删或改确保不冲突。磁盘路径已按目标主机实际情况调整。define之后先不启动用virsh list --all确认虚拟机已注册。首次启动后立即检查串口/网络/文件系统不要等业务方来报障。按这个流程走不敢说100%不出问题但至少能避免90%的“启动不起来”和“网络不通”的返工。KVM虚拟机迁移就是个熟练活多迁几次你自己也能总结出属于自己的清单。这套方法在单机迁移、批量克隆、跨存储复制这些场景下都适用帮你少走弯路。