KVM 虚拟化入门:libvirt 与虚拟机生命周期
从 KVM、QEMU 和 libvirt 的关系入手,掌握虚拟机创建、查看、启动和销毁流程。
一台物理机如何在不互相干扰的前提下,同时运行多个完整操作系统,而且性能接近原生?这个问题背后最基础的机制,是 CPU 出厂时就内置的硬件虚拟化扩展(Intel VT-x / AMD SVM),以及 Linux 内核里负责接管它的 KVM 模块。理解这条因果链,就理解了整个虚拟化栈:硬件提供"虚拟化指令集"→ KVM 让虚拟机里的指令近乎原生执行 → QEMU 负责模拟设备与 IO → libvirt 把"建机、启动、关机、删除"翻译成对前两者的驱动。本文从这条链的底层事实出发,先讲清 KVM、QEMU、libvirt 为什么这样分工,再给出一套完整可复现的从零建机流程,最后给出验证方法与常见坑。
#原理层:KVM、QEMU、libvirt 为什么这样分工
KVM、QEMU、libvirt 这三个词经常被混着用,但它们各管一段:KVM 是内核模块,提供 CPU 虚拟化能力;QEMU 是用户态程序,负责模拟设备、执行 IO;libvirt 是管理框架,把前两者封装成统一 API。分层不是设计者拍脑袋,而是由"硬件能力、设备模拟、管理边界"三个不同层面的问题逼出来的。
#KVM:把硬件虚拟化能力暴露给用户态的内核模块
先看 CPU 虚拟化最难的部分:虚拟机的操作系统自认为拥有整台机器,会执行特权指令、访问特权寄存器。在 x86 硬件虚拟化出现之前,想让 guest 系统跑起来,只能让它的特权指令全部被宿主捕获(trap)再逐条模拟,开销极大。Intel VT-x 与 AMD SVM 从硬件层面解决了这个问题:CPU 引入两种执行模式——宿主运行在根模式(root mode),虚拟机运行在非根模式(non-root mode);guest 在非根模式里执行绝大多数普通指令时,CPU 不让渡控制权,直接执行,只有遇到特权指令、外部中断、缺页等需要宿主介入的事件时才发生 VM exit,切回根模式交给 hypervisor 处理,处理完再 VM entry 回到 guest。绝大多数指令不经过翻译直接执行,这就是 KVM 性能接近原生的根基。
KVM 模块(Intel 平台为 kvm_intel,AMD 平台为 kvm_amd)做的事,是把这个硬件能力整理成用户态可以调用的系统接口。它暴露一个字符设备 /dev/kvm,用户态程序通过一组 ioctl 与它交互:
open("/dev/kvm")获得全局句柄;KVM_CREATE_VM创建一台虚拟机(VM fd);KVM_CREATE_VCPU为虚拟机创建 vCPU(vCPU fd);KVM_RUN让调用线程进入 guest 模式执行,直到发生 VM exit 返回。
这里有一个关键事实:每个 vCPU 不是一个内核线程,而是 QEMU 用户态进程里的一个线程。KVM_RUN 是"当前线程进入 guest"的语义,所以 QEMU 启动几核就建几个 vCPU 线程,这些线程由 Linux 内核调度器像普通线程一样调度到物理核上。你看到的"虚拟机 4 核",实际是宿主上 4 个可以被任意调度到物理核的线程在轮流进入非根模式执行。
内存虚拟化同样靠硬件:Intel EPT 与 AMD NPT(嵌套页表)让 guest 页表与宿主页表之间不再需要软件维护的影子页表,guest 内访存基本没有额外开销。
VM exit 是"这个事件归谁处理"的分水岭:guest 执行 CPUID、访问特权寄存器、写控制寄存器、访问 IO 端口或 MMIO、收到外部中断,都会触发 VM exit。其中大多数由 KVM 内核里直接处理完毕(比如模拟常见的 MSR 读写),只有真正的设备访问才需要回到用户态交给 QEMU。CPU 会在 VM exit 时记录退出原因(VM-exit reason),hypervisor 据此选择处理路径——这个设计让每次"出圈"都有明确归属,也正是 KVM 保持低开销的原因:普通运算指令根本不产生 exit,只有敏感操作才中断一次。
把这一层的作用浓缩成一句话:KVM 是内核里的小型 hypervisor 组件,它只解决"CPU 与内存虚拟化"这一件事,不负责模拟任何设备。
#QEMU:负责设备模拟与 IO 路径的用户态程序
虚拟机不只有 CPU,还有内存控制器、磁盘控制器、网卡、显卡、ACPI 电源管理等"设备"。这些设备没有硬件虚拟化加速可用,只能靠软件模拟——这正是 QEMU 的职责。
QEMU 是一个用户态多线程进程:主线程负责事件循环与管理,vCPU 线程如上文所述执行 guest 代码,IO 线程处理设备模拟。guest 里"看到"的每块磁盘、每张网卡,都是 QEMU 用软件造出来的。设备模拟有两条路:
- 完全模拟:如 e1000 网卡、IDE 磁盘。guest 用现成的通用驱动访问,QEMU 逐条模拟设备行为,通用但对每个 IO 都要模拟一遍设备寄存器读写,开销大。
- 半虚拟化(virtio):guest 安装专门的 virtio 驱动,驱动与 QEMU 后端通过共享内存环形队列直接传递 IO 请求,省掉大量设备寄存器模拟。这就是原文强调"一定要走 virtio"的原因——同样是网卡,e1000 与 virtio-net 的吞吐可以差出数倍。
KVM 与 QEMU 的协作关系是:CPU 指令执行归 KVM,设备模拟归 QEMU。KVM_RUN 返回的 VM exit 中,需要设备模拟的那部分(比如 guest 访问了网卡 MMIO 区域)由内核把控制权还给用户态,QEMU 处理完设备状态后再次发起 KVM_RUN 让 guest 继续执行。没有 KVM 时,QEMU 退化为纯软件模拟模式(TCG,Tiny Code Generator),用二进制翻译逐条执行 guest 指令,性能下降一到两个数量级,生产环境基本不可用——这正是"KVM 是性能根基"这句话的分量。
把一次磁盘读的完整路径走一遍,能看清整个分层:guest 里的应用调用 read() → guest 内核走 virtio-blk 驱动,把请求写进与 QEMU 共享内存的环形队列,并写一次 virtio 设备寄存器作"敲门"通知 → 这次寄存器写触发 VM exit → KVM 判断是 virtio 设备访问,交还用户态 → QEMU 的 virtio-blk 后端从环形队列取出请求,翻译成对宿主普通文件的读(即 qcow2 镜像文件,最终落到宿主页缓存与物理磁盘)→ 数据经共享内存直接返回 guest。整条链路里,guest 只"看见"一个 virtio 磁盘控制器,宿主只"看见"一个普通文件——两台机器的世界在 QEMU 这一层交汇。
#libvirt:把意图翻译成对 QEMU 进程的驱动
到这里,底层能力已经齐了:KVM 提供 CPU 加速,QEMU 提供设备模拟。但直接用 QEMU 命令行管理虚拟机非常痛苦:命令行参数长且易错、一台机器的完整状态没有结构化描述、生命周期管理(启动、关机、迁移)都要自己写胶水逻辑。libvirt 的出现就是为了解决管理边界问题。
libvirt 是一个守护进程(libvirtd)加一套稳定 API:上层工具 virsh、virt-manager,乃至 OpenStack 的 Nova,都通过它操作虚拟机。它把「定义、启动、停止、迁移」封装成统一接口,对不同 hypervisor(QEMU/KVM、LXC、Xen 等)提供同样的语义。普通用户接触最多的客户端是 virsh。以 root 或 sudo 执行 virsh 时,默认连接系统级连接 qemu:///system,即由系统守护进程 libvirtd 管理虚拟机;非特权用户默认连接个人的 qemu:///session,看不到系统虚拟机,日常管理建议统一使用 qemu:///system。
一台虚拟机的完整事实,libvirt 用一份 domain XML 描述:叫什么名字、几个 vCPU、多少内存、用什么 CPU 模型、接哪块磁盘、接哪个网络、有没有串口。virsh start web01 的完整因果链是:
virsh把命令通过 Unix socket RPC 发给libvirtd;libvirtd读取web01对应的 domain XML;- libvirt 的 QEMU 驱动把 XML 翻译成一条
qemu-system-x86_64命令行(进程名、参数、设备全部由 XML 推导); libvirtd以对应身份(系统连接下通常是 root 或qemu用户)fork 出 QEMU 进程,并通过 monitor socket 保持控制;- 之后
virsh shutdown、virsh destroy等命令,本质都是 libvirt 通过 monitor 向 QEMU 下指令。
所以"libvirt 管理虚拟机"的准确含义是:libvirtd 持有 domain XML 这份唯一事实,负责把它物化成 QEMU 进程并维持对进程的控制。
#完整可复现:从零建一台虚拟机
下面以一个 x86_64 物理服务器为例,给出从安装到开机自启的完整流程。命令按发行版分开给出,其他发行版参数大同小异,原理相同。
#前置条件:硬件、BIOS、内核、权限
- 硬件:x86_64 架构主机;CPU 必须支持虚拟化扩展——Intel 的 VT-x(CPUID 标志
vmx)或 AMD 的 SVM(标志svm)。 - BIOS/固件:VT-x/AMD-V 需要在 BIOS 或 UEFI 中显式开启(常见叫法 Intel Virtualization Technology / SVM Mode)。
- 内核:加载了
kvm_intel或kvm_amd模块,且/dev/kvm存在。 - 权限:
virsh等管理命令需要 root,或把运维用户加入libvirt组后重新登录。 - 网络:能访问安装 ISO 或安装源;本文用本地 ISO 讲解,路径示例为
/data/iso/rhel-9.iso。
#安装 KVM / QEMU / libvirt
以 Ubuntu 22.04 / 24.04 为例(Debian 12 将 qemu-kvm 换成 qemu-system-x86,其余包名相同):
sudo apt update
sudo apt install -y qemu-kvm libvirt-daemon-system libvirt-clients virtinst
sudo systemctl enable --now libvirtd
sudo usermod -aG libvirt "$USER" # 重新登录后生效以 RHEL 9 / CentOS Stream 9 / Rocky Linux 9 / Fedora 为例:
sudo dnf install -y qemu-kvm libvirt virt-install
sudo systemctl enable --now libvirtd
sudo usermod -aG libvirt "$USER" # 重新登录后生效装完先确认三个事实:CPU 标志、内核设备、管理栈版本。
grep -E -c '(vmx|svm)' /proc/cpuinfo # 非 0 说明 CPU 支持虚拟化
ls -l /dev/kvm # 存在说明 KVM 模块已加载
virsh version # 打印 libvirt 与 QEMU/KVM 版本常见问题:ls /dev/kvm 不存在,多半是 BIOS 里没开 VT-x/AMD-V,或者云主机没透传虚拟化——很多便宜 VPS 的 CPU 不带 vmx 标志,这类环境跑不了 KVM。先看 BIOS 设置;云主机则要买支持嵌套虚拟化的实例。
#用 virt-install 建第一台虚拟机
手写 XML 容易错,virt-install 是官方推荐的命令式建机方式。它本质上仍是在生成 domain XML:执行后可以用 virsh dumpxml web01 把生成的 XML 导出来看。以下命令在 RHEL 系主机上从本地 ISO 安装一台名为 web01 的虚拟机:
sudo virt-install \
--name web01 \
--memory 4096 --vcpus 4 \
--disk path=/var/lib/libvirt/images/web01.qcow2,size=40,format=qcow2,bus=virtio \
--os-variant rhel9 \
--network network=default,model=virtio \
--graphics none --location /data/iso/rhel-9.iso \
--extra-args 'console=ttyS0'关键参数的实际含义:
--name web01:域的名字,之后所有virsh命令都用它指代这台机。--memory 4096 --vcpus 4:4 GiB 内存、4 个 vCPU。对应 XML 里的<memory>与<vcpu>节点。--disk path=/var/lib/libvirt/images/web01.qcow2,size=40,format=qcow2,bus=virtio:创建一个 40 G 的 qcow2 磁盘文件,挂到 virtio 总线上。qcow2 是稀疏文件,初始只占几百 KB,用多少占多少,不会立刻吃掉 40 G 物理空间。bus=virtio保证 IO 走半虚拟化路径。--os-variant rhel9:告诉 libvirt 按 RHEL 9 的硬件兼容性生成设备——这是性能敏感项,详见后文"忘记 --os-variant 的代价"。可用osinfo-query os查询本机支持的 os-variant 列表。--network network=default,model=virtio:接 libvirt 默认的 NAT 网络,网卡模型 virtio。default网络由 libvirtd 自动创建:一个virbr0网桥加上内置 dnsmasq 提供 DHCP/DNS,再加 NAT 转发规则,网段通常是192.168.122.0/24。效果是虚拟机能上网,但外部访问不到它。生产环境一般用 bridge 或 macvtap,让虚拟机直接暴露在物理网络里。--graphics none --location /data/iso/rhel-9.iso --extra-args 'console=ttyS0':无图形界面,通过串口安装——服务器上装系统这是标准姿势,别指望 VNC。console=ttyS0让内核把控制台输出到串口,之后才能用virsh console web01交互。
如果 default 网络没有启动(比如全新环境),先启用:
sudo virsh net-start default
sudo virsh net-autostart default#domain XML:虚拟机唯一的"出生证明"
virt-install 适合"我要建一台装好系统的机器";但如果你要理解、备份、迁移一台虚拟机,必须能读懂它的 domain XML。这份 XML 是虚拟机的完整拓扑描述:CPU、内存、磁盘、网络、串口、电源管理行为,全在里面。它也是后续所有生命周期操作的事实来源——virsh start 不是"启动 XML 指向的那个系统",而是"把这份 XML 物化成一个 QEMU 进程"。
一个最小可用的 domain XML 示例(磁盘已预先创建好、可直接引导的情况):
<domain type='kvm'>
<name>web01</name>
<memory unit='KiB'>4194304</memory>
<currentMemory unit='KiB'>4194304</currentMemory>
<vcpu placement='static'>4</vcpu>
<os>
<type arch='x86_64'>hvm</type>
<boot dev='hd'/>
</os>
<features>
<acpi/>
</features>
<cpu mode='host-model' check='partial'/>
<clock offset='utc'/>
<on_poweroff>destroy</on_poweroff>
<on_reboot>restart</on_reboot>
<on_crash>destroy</on_crash>
<devices>
<disk type='file' device='disk'>
<driver name='qemu' type='qcow2'/>
<source file='/var/lib/libvirt/images/web01.qcow2'/>
<target dev='vda' bus='virtio'/>
</disk>
<interface type='network'>
<source network='default'/>
<model type='virtio'/>
</interface>
<serial type='pty'>
<target port='0'/>
</serial>
<console type='pty'>
<target type='serial' port='0'/>
</console>
</devices>
</domain>逐个节点看它描述了什么:
<name>web01</name>:域名字,全局唯一,是virsh命令的寻址依据。<memory>与<vcpu>:内存(单位 KiB)与 vCPU 数量。placement='static'表示固定调度。<os><type arch='x86_64'>hvm</type></os>:架构与虚拟化类型,hvm表示全虚拟化(硬件辅助),与type='kvm'呼应;<boot dev='hd'/>表示从硬盘引导。<features><acpi/></features>:开启 ACPI——这是virsh shutdown、virsh reboot优雅关机/重启能生效的前提,ACPI 电源事件要通过它注入 guest。缺了它,虚拟机只能被强制断电。<cpu mode='host-model'/>:CPU 模型跟随宿主,兼顾性能与兼容。virt-install 默认也是这个模式。<on_poweroff>、<on_reboot>、<on_crash>:guest 内执行关机/重启/崩溃时 libvirt 的反应,默认分别是 destroy、restart、destroy——注意<on_poweroff>destroy</on_poweroff>的含义是"guest 自己关机后,libvirt 把 QEMU 进程收掉",对磁盘无影响。<disk>:磁盘。<driver type='qcow2'/>是镜像格式,<source file='...'>是镜像文件路径,<target dev='vda' bus='virtio'/>表示 guest 里看到的第一块 virtio 磁盘(vda)。<interface type='network'>:网络接口。<source network='default'/>接到 default NAT 网络,<model type='virtio'/>用半虚拟化网卡。<serial>与<console>:把串口接到伪终端(pty),virsh console web01就是连到这个串口。
手写 XML 的完整流程是:先建好磁盘文件,再 define,再启动:
sudo qemu-img create -f qcow2 /var/lib/libvirt/images/web01.qcow2 40G
sudo virsh define /path/to/web01.xml
sudo virsh start web01
sudo virsh console web01注意:上面这份 XML 描述的是"磁盘已就绪、装好系统的机器"。要从 ISO 全新安装,仍然建议用
virt-install,它会自动处理安装引导、串口参数、os-variant 等一系列细节;XML 的价值在于理解、备份与微调。无论如何,这份 XML 是虚拟机唯一的"出生证明"。
#从 XML 到 QEMU 进程:验证翻译结果
理解"XML 是唯一事实"之后,值得亲手验证一次 libvirt 的翻译动作。域运行后,在宿主上执行:
pgrep -af qemu-system会看到一大串 QEMU 命令行参数:-name guest=web01、-m 4096、-smp 4、-drive file=/var/lib/libvirt/images/web01.qcow2,if=none,id=drive-virtio-disk0,format=qcow2,以及描述 tap 设备与网卡模型的 -netdev 与 -device 参数。这些参数不是人写的——它们全部由 libvirt 从 XML 推导而来:XML 里的 <memory> 变成内存参数,<vcpu> 变成 -smp,<disk> 变成 -drive,<interface> 变成 -netdev 与 -device。看到这份对应关系,再回头看 virsh start 的因果链就落到了实处:libvirt 不是"启动虚拟机",而是"把想描述的机器翻译成 QEMU 听得懂的语言,然后拉起进程"。
另两个快速核验命令是 virsh domblklist web01(把 XML 里的磁盘声明整理成目标设备与源文件的对照表)和 virsh domiflist web01(列出网络接口及其类型)。虚拟机"看不见磁盘、没接上网卡"时,先用它们对照 XML,比凭感觉猜快得多。
#指令式与声明式:两种建机方式的取舍
到这里可以看出两条路线的本质差异:
virt-install是指令式:执行一条命令,libvirt 自动生成 XML 并启动安装。适合日常快速建机。virsh define+ XML 是声明式:你亲手描述终态,libvirt 原样落地。适合模板化、批量生成、以及与代码仓库配合做基础设施即代码(IaC)。
生产环境中两者都会用到:模板机通常用 virt-install 或更上层的编排工具生成,之后的生命周期管理全部基于 XML 做声明式操作。
#生命周期命令:start / shutdown / reboot / destroy / undefine
虚拟机创建好之后,日常打交道的是这几个生命周期命令。理解每个命令"对 QEMU 进程做了什么、对磁盘数据做了什么",就不会删错东西:
virsh list --all # 所有虚拟机(含关机)
virsh start web01 # 启动:按 XML 拉起 QEMU 进程
virsh console web01 # 连串口
virsh shutdown web01 # 发 ACPI 关机信号(优雅)
virsh reboot web01 # 发 ACPI 重启信号(优雅)
virsh destroy web01 # 强制断电(慎用)
virsh undefine web01 # 删除定义(不删磁盘文件!)各命令的语义与对磁盘的影响如下:
| 命令 | 对 QEMU 进程做了什么 | 对磁盘的影响 | 备注 |
|---|---|---|---|
virsh start | 按 XML 物化出 QEMU 进程 | 无 | 首次启动前磁盘必须存在 |
virsh shutdown | 向 guest 注入 ACPI 电源事件,guest 内执行优雅关机流程 | 无,guest 正常落盘 | guest 需装 acpid 才响应 |
virsh reboot | 注入 ACPI 重启事件,guest 内优雅重启 | 无,guest 正常落盘 | 同样依赖 acpid 或 qemu-guest-agent |
virsh destroy | 立即终止 QEMU 进程 | 等价于直接拔电源,未落盘的写可能丢失 | 慎用,生产先试优雅关机 |
virsh undefine | 删除 libvirt 的域定义(XML) | 默认不碰磁盘文件 | 删之前先 dumpxml 备份 |
几个容易混淆的点:
shutdown是 ACPI 关机,虚拟机里装了 acpid 才会响应;没响应就只能destroy,所以模板机一定要装 acpid 和 qemu-guest-agent。装了 qemu-guest-agent 之后,还能用更可靠的优雅关机方式:virsh shutdown web01 --mode agent,它由 guest 内的 agent 直接执行关机,不依赖 ACPI 事件。destroy不是"优雅关机",是强制断电。文件系统缓存里没写盘的数据会丢,正在写的文件可能损坏。它对磁盘的破坏与真实断电相同,不是删除文件。undefine只删定义不删磁盘。删除虚拟机 ≠ 删除磁盘。如果确实要连磁盘一起删,用virsh undefine web01 --remove-all-storage——这一步不可逆,执行前务必确认。undefine对运行中的域默认会拒绝,需先shutdown或destroy;域若存在快照,还要加--delete-snapshots。- 删之前先
virsh dumpxml web01备份配置:这个 XML 是虚拟机唯一的"出生证明",丢了它,磁盘还在也只能靠人肉猜设备配置。
#开机自启
虚拟机默认不随宿主开机。让 web01 在 libvirtd 启动时自动启动:
virsh autostart web01 # 开启自启
virsh autostart web01 --disable # 取消自启
virsh list --autostart # 查看自启列表机制上,virsh autostart 会在 libvirt 配置目录(常见为 /etc/libvirt/qemu/autostart/)下为对应 domain XML 创建符号链接,libvirtd 启动时扫描该目录依次拉起。所以"开机自启"的准确含义是"libvirtd 启动后自动 start",宿主刚开机到 libvirtd 起来之间,虚拟机仍在等待。
#验证层:怎样确认我做对了
建好一台虚拟机后,不要直接开始用,先做四件事:确认状态、核对拓扑、确认半虚拟化路径、从宿主侧观察进程。每一步都有明确的判定标准。
#状态与配置核验
virsh list --all预期看到类似输出,web01 状态为 shut off 或 running:
Id Name State
--------------------
- web01 shut off启动后查状态与配置总览:
virsh start web01
virsh domstate web01 # 预期输出 running
virsh dominfo web01 # 配置总览virsh dominfo 会显示 Id、Name、UUID、OS Type、State、CPU(s)(vCPU 数)、Max memory、Used memory、Autostart 等字段,把它们和创建时的参数对一遍——vCPU 数、内存、自启状态都是判决依据。
#确认半虚拟化路径
这是原文里最值钱的一条经验:装完系统后,用 virsh dumpxml 检查网卡与磁盘驱动,确认走的是 virtio 而非 e1000:
virsh dumpxml web01 | grep -E 'driver name|<model type'预期看到:
<driver name='qemu' type='qcow2'/>
<model type='virtio'/>判定标准:<model type='virtio'/> 出现,说明网卡走半虚拟化路径;如果看到 <model type='e1000'/>,说明 os-variant 没生效或建机参数丢了,性能会差 3-5 倍,需要重装或用 virsh edit 修正后重启。
#从宿主侧观察 vCPU 线程
KVM 的"vCPU 是宿主线程"不是抽象概念,可以亲眼看到:
pgrep -af qemu # 看 QEMU 进程与完整命令行
virsh vcpuinfo web01 # 每个 vCPU 当前在哪个物理 CPU 上跑virsh vcpuinfo 会为每个 vCPU 输出一行,包含 VCPU 编号、当前运行的物理 CPU、State(running)、CPU time 等。这组数据直接验证了原理层的结论:guest 的"4 核"就是宿主上 4 个被内核调度器调度的 vCPU 线程。
需要更细的统计时用 virsh domstats web01,它输出 CPU 时间、内存 balloon、块设备与网络 IO 统计等字段,可用于判断虚拟机是否真的在干活、IO 是否异常。
#性能冒烟:虚拟化是否在正常加速
状态与拓扑都对,不代表性能对。装好系统后做一次最小性能冒烟,能确认 KVM 加速真的在起作用。
在 guest 内执行磁盘写测试(前提是 guest 已装好系统并可用):
dd if=/dev/zero of=/tmp/test.img bs=1M count=1024 conv=fdatasync
rm -f /tmp/test.img这条命令写入 1 GiB 并强制落盘。比较基准是你对该宿主本地磁盘速度的预期:如果 guest 内测出的速率和裸机明显不在一个数量级,优先怀疑 IO 路径没走 virtio。
再从宿主侧看 CPU 是否真的在消耗:
virsh domstats web01 | grep -E 'cpu.time|vcpu'虚拟机空闲时 cpu.time 应几乎不变;guest 里用 yes > /dev/null 之类压满一个核时,对应 vCPU 的 cpu.time 会持续增长。如果 guest 满负荷而宿主上 cpu.time 纹丝不动,说明 vCPU 线程根本没执行起来——回到上面的路径重新核查。需要更细的宿主侧计数器时,kvm_stat 可以观察 VM exit 频率与常见退出原因的分布,用来判断异常是否来自频繁的退出。
至此,一台虚拟机从"定义"到"运行"到"可观测"的闭环就完整了。
#边界与坑
#/dev/kvm 不存在
现象:ls /dev/kvm 报 No such file or directory,virt-install 报 "KVM is not available"。
排查顺序:先看 CPU 有没有标志 grep -E -c '(vmx|svm)' /proc/cpuinfo,为 0 说明硬件不支持或没开——物理机进 BIOS 开 VT-x/AMD-V;云主机找支持嵌套虚拟化的实例类型。标志存在但 /dev/kvm 没有,则检查内核模块:modprobe kvm_intel(Intel)或 modprobe kvm_amd(AMD),再看 dmesg 有无报错。
#shutdown 不响应
现象:virsh shutdown web01 执行了,但 virsh list 里状态一直不变。
原因:ACPI 事件没有消费方。guest 里没有装 acpid,就没有进程监听电源按键事件。对策:模板机安装并启动 acpid(Debian/Ubuntu 包名 acpid,RHEL 系包名 acpid),再装 qemu-guest-agent;之后优先用 virsh shutdown web01 --mode agent。如果 guest 已卡死(内核 panic 等),优雅关机注定无解,只能 destroy——这正是"先试 shutdown,不行再 destroy"的工程顺序。
#undefine 与磁盘删除
virsh undefine web01 永远不碰磁盘文件。它删的是 /etc/libvirt/qemu/ 下那份 XML 定义。所以:
- 只跑了
undefine,磁盘/var/lib/libvirt/images/web01.qcow2还在,可以用virsh define重新导入找回。 - 跑了
undefine --remove-all-storage,磁盘文件被删,找回只能靠备份。 - 执行任何删除前,先
virsh dumpxml web01 > web01.xml.backup,再决定删不删——这也呼应下一条:备份永远是 XML + 磁盘双份。
#忘记 --os-variant 的代价
我踩过最典型的坑是忘记 --os-variant:默认按最保守的配置走,导致网卡用 e1000 而不是 virtio,性能差 3-5 倍。原因链是:os-variant 决定 libvirt 为 guest 挑选的设备模型与固件特性集合;不给它,libvirt 只能选"最老最兼容"的组合,virtio 这种半虚拟化设备就不在候选里。装完 virsh dumpxml 看一眼 <driver name='qemu' type='virtio'/> 和 <model type='virtio'/>,确认走的是半虚拟化路径。
#在线调整 vCPU 的前提
virsh setvcpus web01 8 --live --config 看起来是"在线加 vCPU"一行命令,但生效有两个硬前提:domain XML 里 vCPU 的 maximum 大于 current(形如 <vcpu placement='static' current='4'>8</vcpu>),以及 guest 内核支持 CPU 热插拔。很多机器默认配置并不满足这些条件,执行会报错或无效。在线调整有前提:virtio 驱动、CPU 热插拔配置等,不是所有机器都能加。生产变更前先查文档,别在业务机器上试。
#qcow2 的特质与磁盘格式选择
创建虚拟机时 format=qcow2 不是唯一选择,但它是默认且最适合日常使用的格式。三个关键特质都与虚拟机生命周期直接相关:
- 稀疏与按需分配:40 G 的磁盘创建出来只占几百 KB,写入多少才增长多少。代价是略低于 raw 格式的性能——raw 是宿主上一个连续的普通文件,少了格式解析与按需分配逻辑。
- 写入时复制(COW)与快照:qcow2 支持在现有镜像之上叠层,快照就是冻结当前层、把新写入导向新层。libvirt 的快照命令
virsh snapshot-create-as web01 snap1 'desc'(创建)、virsh snapshot-list web01(查看)、virsh snapshot-revert web01 snap1(回滚)都建立在这个能力上。 - 镜像处理:libvirt 之外的世界用
qemu-img处理镜像,qemu-img info看镜像信息,qemu-img convert -O raw把 qcow2 转成 raw;convert 会把数据真正铺满目标文件,执行前先确认目标磁盘空间够用。
生产选型:系统盘用 qcow2 便于快照与精简分配;追求极致 IO 且快照用得少的数据盘可以选 raw。两者在 domain XML 里只差一行 <driver type='qcow2'/> 与 <driver type='raw'/> 的区别。
#快照、迁移与 CPU 模型的兼容性
生命周期不止"关机/开机",两个高频高级操作值得提前知道边界:
快照:快照保存磁盘某个时间点的状态(--disk-only 只存磁盘,可选 --memspec 连内存一起存)。它对 qcow2 是叠加层:快照越多,写路径越长,性能越差。快照不是备份的替代品——快照依赖原始 qcow2 与后续镜像链完整存在,链上任何一环损坏,快照跟着失效。
迁移:在线迁移用 virsh migrate web01 qemu+ssh://target主机/system --live。迁移对两台宿主的要求非常严格:目标机能访问同一份存储(共享存储或先把镜像同步过去)、设备模型一致、CPU 能力足够接近。这里引出 CPU 模式的选择:host-passthrough 把宿主的 CPU 特性原样透传给 guest,性能最好,但两台宿主 CPU 型号一旦有差异,迁移后 guest 可能直接崩;host-model 会自动挑选两台机器都能提供的最接近模型,是可迁移部署的默认选择。所以 domain XML 里那行 <cpu> 不是锦上添花——它直接决定你的虚拟机将来能不能活着搬到另一台机器上。
#宿主资源耗尽:内存超售与磁盘占满
虚拟机最隐蔽的杀手是宿主资源被悄悄耗尽,而每台 guest 内部毫无感知:
- 内存超售:多台虚拟机的声明内存之和超过宿主物理内存是常见配置,靠 KVM 的内存 balloon(
virsh setmem web01 2G可在线调整声明内存)在 guest 空闲时回收内存。但超售过度,宿主触发 OOM killer,优先被杀的是 QEMU 这类大进程——表现为某台虚拟机毫无征兆地消失。设计容量时给宿主留足余量,并持续监控宿主的可用内存与 swap。 - 磁盘占满:qcow2 按需增长,意味着"虚拟磁盘 40 G"在宿主上可以长得比预期快得多。宿主磁盘满时,qcow2 增长失败,QEMU 直接报 IO 错误,guest 文件系统可能脏掉甚至退化为只读。监控宿主磁盘使用率,镜像目录(
/var/lib/libvirt/images)最好放在独立分区或独立存储上,不要和根分区抢空间。 - vCPU 超售:vCPU 总数远超物理核数时,多个 vCPU 线程互相抢核,guest 内感受到的延迟上升——宿主被超售的 CPU 时间会以 steal time 的形式体现在 guest 内部。虚拟机适合超售,但要有度;重要业务给足独占核,不要让任何时刻都在抢。
#备份的完整形态
**备份永远是 virsh dumpxml + 磁盘文件,缺一不可。**只备份 qcow2 没有 XML,恢复时设备配置全靠猜;只备份 XML 没有盘,等于什么都没存。这也是上面所有"先 dumpxml"建议的总原因:XML 描述"这台机器是什么",qcow2 承载"这台机器的数据",两者合一才是一次完整的备份。
#模板化:从一台好机器批量造机器
多台同构虚拟机(比如一组 Web 节点)的正确做法不是反复跑安装器,而是把一台装好并打扫干净的机器变成模板:
- 用
virt-install装好第一台,按生产标准配置(virtio 驱动、acpid、qemu-guest-agent、时区、软件仓库、密钥)。 - 清理这台机器的独立性:删除 machine-id 与 SSH host key(
rm -f /etc/machine-id /etc/ssh/ssh_host_*),清空日志与包缓存,关机。做完这些再virsh shutdown,让模板处于"干净关机"状态。 - 用
virt-clone克隆:
sudo virt-clone --original web01 --name web02 \
--file /var/lib/libvirt/images/web02.qcow2virt-clone 会复制磁盘并自动为克隆体生成新的 UUID 与网卡 MAC 地址,避免两台机器在网络上"撞身份"。克隆完成后再 virsh start web02 即可。批量场景下,模板配合 virsh dumpxml 导出改参数,就是一套不依赖任何外部编排的最小 IaC 流程。
#收尾:什么时候该用、什么时候不该用
回到开头的因果链:CPU 硬件虚拟化扩展让 guest 指令近乎原生执行,KVM 把它变成内核接口,QEMU 补上设备模拟,libvirt 把这一切封装成"定义-启动-关机-删除"的稳定语义,而 domain XML 始终是那台虚拟机的唯一事实来源。这套栈适合的场景是:需要完整操作系统、需要二进制兼容、需要和物理机一样的隔离边界——跑传统业务系统、数据库、不兼容容器的第三方软件。
反过来,如果应用本身无状态、可以水平扩容,容器比虚拟机更省资源、启动更快,没必要为每个副本都维持一个 QEMU 进程;如果需要管理成百上千台虚拟机,直接在单机上 virsh 会失控,应该用 OpenStack Nova 这类基于 libvirt API 的编排平台。规模小、要精确控制时,virsh 加一份规范的 domain XML 模板,就是最通透的管理方式——因为你随时知道每一层在做什么。
相关阅读
Related留言讨论
0 条还没有留言,来说点什么吧。