事情得从上周一次扯皮说起。同事在群里问"给服务器做系统用 kvm 还是 qemu",另一个回"qcow2 和 raw 到底哪个快",第三个人甩出一句"COW 和 ROW 不是一回事吗"。三个问题搅在一起,本质上是在问三个完全不同层面的东西:跑虚拟化用什么组件、磁盘镜像存成什么格式、镜像格式内部用的什么写入策略。把这三层揉成一团,就会出现"kvm、qemu、qcow2、RAW、COW、ROW 傻傻分不清"的经典混乱。
我把这套东西重新梳理了一遍,顺手在 Ubuntu 24.04 上从零跑了一台机器,踩过的坑也都记下来了。适合两类人看:一类是刚开始接触 Linux 虚拟化、想搞明白这套名词到底谁是谁的人;另一类是在生产环境里用 KVM 跑业务、需要做磁盘格式选型和性能调优的人。看完之后你至少能做到三件事:说清这几个缩写分别在栈的哪一层、根据业务场景选对镜像格式、用 qcow2 的快照链做安全的回滚和瘦身。
1. 先把这堆缩写的层级关系钉死
名词混乱的根源,是它们压根不在一个层面上。KVM 是内核里的东西,QEMU 是用户态的程序,qcow2 和 RAW 是磁盘文件的格式,COW 和 ROW 是数据写入的策略。硬要类比的话:KVM 是发动机,QEMU 是整车,qcow2/RAW 是油箱的规格,COW/ROW 是加油的方式。你不能问"发动机和油箱哪个好",同理也不能问"kvm 和 qcow2 哪个快"。
1.1 KVM:内核里的一颗模块,只负责让 CPU 跑起来
KVM 全称 Kernel-based Virtual Machine,它不是一个软件包,也不是一个能双击启动的程序。它本质上是 Linux 内核的一组模块:通用的kvm.ko加上平台相关的kvm_intel.ko或kvm_amd.ko。这三个模块加载之后,内核会多出一个字符设备/dev/kvm,用户态程序通过 ioctl 调它,就能把一段代码丢进 CPU 的虚拟化扩展(Intel VT-x 或者 AMD-V)里直接执行。
这件事的意义在于:如果没有 KVM,虚拟机里的每一条指令都得靠软件模拟翻译一遍,性能会掉到个位数百分比;有了 KVM,客户机的特权指令由 CPU 硬件直接兜住,只有真正需要"假装"的部分(比如模拟一块老式网卡)才走软件路径。所以我经常跟人说,KVM 的贡献是"让虚拟机的计算性能接近物理机",它不管磁盘、不管网络、不管设备,它只管 CPU 和内存的隔离。
验证 KVM 是否可用,两条命令就够:
grep -E -c '(vmx|svm)' /proc/cpuinfo # 大于 0 说明 CPU 支持硬件虚拟化 ls -l /dev/kvm # 存在说明内核模块已加载vmx是 Intel 的旗标,svm是 AMD 的。如果第一条返回 0,通常要去 BIOS/UEFI 里开一下虚拟化相关选项;如果 CPU 支持但/dev/kvm不存在,检查kvm_intel是不是被别的模块占用了——嵌套虚拟化、部分安全策略、某些云厂商的实例类型都会屏蔽它。
1.2 QEMU:真正在用户态"拼出一台电脑"的整机模拟器
QEMU 是一个纯用户态的程序,它做的事是完整模拟一台计算机:主板芯片组、PCI 总线、磁盘控制器、网卡、显卡、USB 控制器、UEFI/BIOS 固件,全都由它用软件搭出来。它有两套执行引擎,一套是纯软件解释(TCG,Tiny Code Generator),另一套是接上 KVM 走硬件加速。
这就解释了为什么装了 KVM 还得装 QEMU。KVM 只提供"CPU 能跑"这个能力,至于客户机看到的是一块 virtio 磁盘还是 IDE 磁盘、网卡是 e1000 还是 virtio-net、启动固件是 SeaBIOS 还是 OVMF,这些全得由 QEMU 来构造和分发。你敲的qemu-system-x86_64就是 QEMU 的主程序,-accel kvm就是告诉它"这条虚拟机用 KVM 加速,别用 TCG 慢慢磨"。
顺带说一句,qemu-system-aarch64、qemu-system-riscv64这些也是 QEMU,只是模拟的目标架构不同。KVM 加速只在"宿主机架构等于客户机架构"时才能用(也就是 x86 上跑 x86),跨架构就必须退回 TCG 软件模拟,性能会差两个数量级。这是很多人做 arm64 模拟时最大的预期落差来源。
1.3 libvirt/virsh/virt-manager:站在 QEMU 上面的管理层
裸敲 QEMU 参数能干所有事,但参数太长、太容易写错,而且没有统一的"虚拟机清单"概念。libvirt 就是为了解决这个:它把虚拟机的定义写成一份 XML,存到固定位置,然后用virsh或virt-manager去操作。libvirt 底层还是调 QEMU,它不替代 QEMU,只是加了一层编排和生命周期管理。
日常能感受到的差别是:裸 QEMU 启动的虚拟机,关了终端进程就没了,virsh list里也看不见;走 libvirt 的虚拟机,virsh list --all能列出全部,还能设置开机自启、做快照、热插拔磁盘、查询块设备层信息。做生产环境,我强烈建议走 libvirt 这套,不是因为它更好看,而是因为它的可观测性和可重复性完全不是一个量级。
1.4 另一个 KVM:键盘鼠标显示器共享器
这里必须插一句,因为搜"kvm"的人里有一半其实在找硬件设备。KVM Switch(KVM 切换器、KVM 共享器)是把一套键盘、鼠标、显示器接到多台主机上,用按键或按钮切换的硬件盒子,和虚拟化里的 KVM 除了缩写一样,没有任何关系。
至于热词里提到的"怎么看 KVM 的 8K 认证",判断逻辑很直白:先看带宽,8K@60Hz 在 4:4:4 无色度抽样的情况下需要 HDMI 2.1 的 48Gbps 或者 DisplayPort 2.0 的 UHBR 通道,标称 18Gbps 的老设备一定做不到完整 8K;再看它写的具体规格,很多产品标的是 8K@30Hz 或者 8K@60Hz 4:2:0,这两种和"完整 8K 60 帧"是两码事;最后看线材,主机到切换器这段如果用的还是老线,切换器再强也没用。买之前把这三个数字对一遍,比看任何宣传语都管用。
1.5 一张表把六层关系收拢
| 名词 | 所属层级 | 具体是什么 | 少了它会怎样 |
|---|---|---|---|
| KVM | 内核模块 | /dev/kvm字符设备,提供 CPU 硬件虚拟化 | 虚拟机只能用 TCG 软件模拟,性能骤降 |
| QEMU | 用户态程序 | 整机模拟器,构造主板/设备/固件 | 内核有能力但没人去用它组机器 |
| libvirt | 管理服务 | XML 定义 + virsh/virt-manager 前端 | 能跑,但没有统一清单和生命周期管理 |
| RAW | 文件格式 | 无元数据,字节即扇区 | 无法内建快照,无法增量备份 |
| qcow2 | 文件格式 | 带 L1/L2 映射表、引用计数、快照 | 缺了灵活快照与后端文件链能力 |
| COW / ROW | 写入策略 | 数据落盘时的重定向方式 | 决定写放大、碎片与快照成本 |
2. RAW 与 qcow2:磁盘格式到底怎么挑
搞清层级之后,第二个高频问题就是格式选型。RAW 和 qcow2 是 QEMU 支持的一堆格式里最常用的两个,很多人只知道"raw 快、qcow2 功能多",但不知道具体快在哪、功能多在哪,选的时候全靠感觉。
2.1 RAW 不是格式,是"没有格式"
RAW 的本质是:文件里的第 N 个字节,就对应磁盘的第 N 个字节。没有头部、没有映射表、没有元数据。好处是很实在的:QEMU 读写的时候不需要做任何地址翻译,也不需要维护额外的元数据结构,I/O 路径最短,性能上限最高,而且这文件可以直接挂给别的工具用——dd、losetup、离线取证工具都能直接读。
代价也很明显。第一,它不支持内部快照,想做快照只能在下层文件系统或者 LVM 上想办法。第二,它不支持后端文件链,做增量备份得自己搭一套。第三,如果底层文件系统不支持稀疏文件,创建 40G 的 RAW 就真占 40G。
这里有个容易被忽略的细节:qemu-img create -f raw disk.img 40G默认会创建稀疏文件,实际占用接近 0。你可以用du -h disk.img和ls -l disk.img对比一下,前者是实际占用,后者是逻辑大小。但在 ext4 上,稀疏文件写入过程中会不断分配新块,长期运行之后碎片会比较厉害,顺序读性能会衰减。生产环境里我一般会配fallocate预分配,把碎片问题提前消掉。
2.2 qcow2 的目录结构:L1/L2 与引用计数
qcow2 全称 QEMU Copy-On-Write 格式,它做的核心事情是间接寻址。客户机发出的是"读第 12345 号扇区",qcow2 要把它翻译成"读宿主文件里的第 X 个字节"。这套翻译靠三级结构完成:文件头里有 L1 表的指针,L1 表指向若干张 L2 表,L2 表里存的是真正数据簇的位置。
理解这个结构,两个参数才算真的懂了:
- cluster_size(簇大小):默认 64K。L2 表每一项占 8 字节,所以一张 64K 的 L2 表能装 8192 项,每项覆盖一个簇,也就是一张 L2 表覆盖 8192 × 64K = 512MB 的客户机地址空间。如果你把簇改成 1M,单张 L2 表就能覆盖 8GB,元数据开销显著下降,但小文件写入的放大也会变严重。
- refcount_bits(引用计数位宽):qcow2 v3 默认 16 位。引用计数用来记录每个簇被多少个快照/后端文件共享着,共享计数归零才能回收。位数越高,能同时容纳的快照层数越多,但元数据占用也越大。
这两个参数在qemu-img info里都能看到,qemu-img check还能校验引用计数有没有算错——快照删不干净、文件删了空间不释放,八成是引用计数出了问题。
2.3 预分配四种模式的真实取舍
qemu-img create -o preallocation=有四个取值,很多人随手写metadata但说不清为什么。我在下面这张表里把它们的实际行为写清楚:
| 预分配模式 | 元数据是否预分配 | 数据块是否预分配 | 创建耗时 | 适用场景 |
|---|---|---|---|---|
off | 否 | 否 | 秒级 | 临时测试机、开发环境 |
metadata | 是 | 否 | 秒级到十秒级 | 生产主力,兼顾创建速度和运行稳定性 |
falloc | 是 | 是(用 fallocate 快速占位,内容为空洞) | 秒级 | 需要固定占用、避免中途扩容抖动 |
full | 是 | 是(并且逐字节写零) | 分钟级,取决于盘大小 | 需要严格保证空间、审计场景 |
full和falloc的差别值得单独说:full会真把每个字节写成 0,创建 1T 的盘可能要十几分钟;falloc只是让文件系统把这些块"预留"下来,不写内容,速度快得多。除非有合规审计要求"磁盘必须写零",否则falloc就够了。
一个实际的经验:用metadata预分配之后,qcow2 在客户机第一次写某个簇的时候仍然要分配新簇并更新 L2 表,这段时间的写入延迟会有轻微抖动。如果你的业务对延迟敏感(比如数据库),用falloc更稳。我做过一组对比,同样跑随机写,metadata模式的前 30 秒写入吞吐波动比falloc明显大一些,之后就趋于一致了。
2.4 选型表:什么业务配什么格式
| 业务特征 | 推荐格式 | 理由 |
|---|---|---|
| 单机数据库、高 IOPS 场景 | RAW + LVM 快照 | 无间接寻址,I/O 路径最短 |
| 需要频繁做快照回滚的测试环境 | qcow2 + 外部快照 | 秒级创建,回滚成本低 |
| 需要在线迁移、增量备份 | qcow2 + 后端文件链 | 只传变更簇,带宽省得多 |
| 磁盘一次性交付、只读挂载 | RAW(或qemu-img convert后转出) | 格式通用,任何工具都能读 |
| 桌面虚拟化、镜像模板分发 | qcow2 + 压缩镜像 + 后端文件 | 模板共享,增量盘极小 |
我的默认建议是:没有特殊理由就上 qcow2。RAW 的性能优势在纯 SSD、virtio、cache=none的组合下,实际差距往往在个位数百分比,而 qcow2 带来的快照、克隆、迁移便利性,在运维层面的价值远超这点性能差。只有在明确测出瓶颈落在存储路径时,才值得为 RAW 放弃那些能力。
3. COW 和 ROW:被混淆最多的一对概念
这两个词是混乱的重灾区。很多人以为它们是 qcow2 和 RAW 的别称,其实不是。COW 和 ROW 描述的是"当你要修改一份被共享的数据时,怎么处理旧数据"这件事,它是文件系统、存储快照、甚至内存管理里通用的一套策略。
3.1 COW 的操作顺序:先抄旧的,再写新的
写时复制(Copy-On-Write)的流程是:数据块 A 被两个快照共享,现在要改 A。系统先把 A 的原始内容完整复制到新位置 B,然后对 B 做修改,最后把指针从 A 指向 B,A 的引用计数减一。全程 A 的内容始终不变,所以任何还指向 A 的快照都还能读到改动前的状态。
这个顺序决定了 COW 的代价:写操作变成"读旧 + 写新 + 改指针"三步。在随机小写入的场景下,这个放大效应很明显,而且长期运行会产生碎片,因为新块的位置是随机的。为什么很多文件系统在开了快照之后写入性能会明显下滑,原因就在这里——只要底层还有快照引用,每次写都得走这套流程。
放在 qcow2 里看,这个机制就体现在:客户机第一次写某个未分配的簇时,QEMU 要在宿主文件里找一个空闲簇,写入数据,更新对应的 L2 表项,同时更新引用计数。这不是"复制旧数据"那么严格,但逻辑内核是一致的——不改共享结构,改了就新建。
3.2 ROW 换了个思路:新数据写新地方,旧块直接还给分配器
重定向写(Redirect-On-Write)的动作顺序不一样:数据要改,直接把新内容写到全新的位置 C,把指针指过去,然后立刻把旧块 A 的引用计数减一;如果减到零,A 可以被回收。整个过程中,旧数据不需要被复制,只需要被保留到引用归零为止。
省掉"复制旧数据"这一步,就是 ROW 相对 COW 的核心优势:写放大更低,写路径更短,快照创建几乎是纯元数据操作,速度极快。代价是碎片化更严重——每次写都跑到新位置,数据在物理介质上的连续性会越来越差;而且空间回收依赖后台的垃圾回收机制,如果 GC 跟不上写入速度,就会出现"空间明明释放了但可用容量涨不回来"的现象。
3.3 两者对照
| 对比项 | COW(Copy-On-Write) | ROW(Redirect-On-Write) |
|---|---|---|
| 写操作顺序 | 先复制旧块到新位置,再改新块 | 直接写新位置,旧块引用减一 |
| 首次写延迟 | 较高,包含一次读取 | 较低,只写不回读 |
| 空间回收 | 相对及时,旧块立即可释放 | 依赖后台 GC,可能滞后 |
| 碎片倾向 | 中等 | 偏高 |
| 快照创建成本 | 低 | 极低 |
| 典型落地 | qcow2 簇分配、多数文件系统快照 | 部分存储阵列的快照、日志结构文件系统 |
回到最初的问题上:qcow2 名字里带 COW,指的是它采用写时复制的分配策略;而 ROW 是另一条技术路线,不是 qcow2 的子集,也不是 RAW 的别名。搞清这个,以后再看到"COW 和 ROW 哪个好"这种问法,就知道问题本身少了限定条件——得先问是哪一层的 COW。
3.4 qcow2 快照链就是 COW 的落地形态
qcow2 的实际用法里,COW 最直观的体现就是后端文件链(backing file chain)。做法是:先做一个只读的基础镜像,然后基于它派生一个可写的增量盘,增量盘只记录相对于基础镜像的变更。开十台测试机,共用一份 20G 的基础镜像,每台的增量盘可能只有几百兆。
创建方式很简单:
qemu-img create -f qcow2 -b base.qcow2 -F qcow2 vm01.qcow2-b指定后端文件,-F指明后端文件的格式。读的时候,QEMU 从最上层往下找,找到哪个簇在顶层有映射就用顶层,没有就往下层找。写的时候,只有被修改的簇才会分配在顶层文件里。
这套机制的坑在于链条不能太长。每多一层,一次未命中就要往下遍历一层,元数据缓存压力也会累加。我见过有人用脚本每天派生一层,跑了三个月叠了九十层,读性能掉到没法用。经验值是链条深度控制在三层以内,超过就做一次blockcommit把中间层合并掉。
virsh blockcommit vm01 vda --active --pivot --verbose--active表示合并当前活跃层,--pivot表示合并完成后把虚拟机切换到新文件上,不用关机,也不用重建虚拟机定义。
4. 在 Ubuntu 24.04 上从零建一台 KVM 虚拟机
前面讲原理,这一节全是操作。我用的环境是 Ubuntu 24.04 Server,物理机开了 VT-x,网络是普通千兆。整个过程大约十分钟。
4.1 装包:三个包解决 90% 需求
sudo apt update sudo apt install -y qemu-system-x86 qemu-utils libvirt-daemon-system \ libvirt-clients virtinst bridge-utils ovmf sudo usermod -aG libvirt,kvm $USER newgrp libvirt包的分工要说清:qemu-system-x86提供qemu-system-x86_64主程序,qemu-utils提供qemu-img、qemu-nbd这些工具,libvirt-daemon-system是守护进程,libvirt-clients提供virsh,virtinst提供virt-install,ovmf是 UEFI 固件。usermod那行是把自己加进 libvirt 和 kvm 两个组,否则每次virsh都得 sudo。
装完之后检查服务状态:
systemctl is-active libvirtd virsh version virsh net-list --allvirsh version会打印两行版本号,一行是 libvirt 库,一行是 QEMU 二进制。这两个版本号要记住,后面遇到兼容性问题经常要从这里对。
4.2 建磁盘:参数不是随便填的
sudo mkdir -p /var/lib/libvirt/images sudo qemu-img create -f qcow2 \ -o preallocation=metadata,cluster_size=64K,lazy_refcounts=on \ /var/lib/libvirt/images/ubuntu24.qcow2 40G三个参数我逐个解释。preallocation=metadata把 L1/L2 表提前建好,避免运行中第一次写的时候还要分配元数据;cluster_size=64K是默认值,如果你的客户机主要是大文件顺序读写(比如视频转码、大数据临时盘),可以调到1M减少 L2 表数量,但小文件随机写的放大也会变大;lazy_refcounts=on让引用计数延迟更新,配合cache=writeback能提高写入性能,代价是掉电时可能要做一次qemu-img check修复。
建完验证一下:
qemu-img info --output=json /var/lib/libvirt/images/ubuntu24.qcow2 qemu-img check /var/lib/libvirt/images/ubuntu24.qcow2check的输出里如果corruptions是 0,说明元数据是干净的。以后每次虚拟机异常关机之后都建议跑一遍。
4.3 建机:BIOS 还是 UEFI
老规矩用 SeaBIOS(传统 BIOS)能跑,但 Ubuntu 24.04 的安装镜像对 UEFI 支持更好,而且 UEFI 支持 Secure Boot。我这边直接用 UEFI:
sudo virt-install \ --name ubuntu24 \ --memory 4096 --vcpus 4 \ --cpu host-passthrough \ --machine q35 \ --disk path=/var/lib/libvirt/images/ubuntu24.qcow2,format=qcow2,bus=virtio,cache=none,io=native,discard=unmap \ --cdrom /data/iso/ubuntu-24.04-live-server-amd64.iso \ --network network=default,model=virtio \ --graphics vnc,listen=127.0.0.1,port=5901 \ --boot uefi \ --os-variant ubuntu24.04 \ --noautoconsole几个参数值得展开。--cpu host-passthrough把宿主 CPU 的指令集特性直接透传给客户机,性能最好,代价是这台虚拟机不能再迁移到 CPU 型号不同的宿主机上——如果你要做集群迁移,改用--cpu host-model。--machine q35是模拟现代芯片组(支持 PCIe),比默认的 i440fx 更贴近真实硬件,UEFI 场景下基本是必选项。
磁盘那一串里,bus=virtio用的是 virtio-blk 半虚拟化驱动,比模拟 IDE 快得多;cache=none让 I/O 绕开宿主页缓存直接走 O_DIRECT,避免双重缓存;io=native启用原生异步 I/O,只有cache=none时才生效;discard=unmap让客户机发来的 TRIM 指令能真正传导到宿主文件,这对 qcow2 瘦身至关重要,后面还会细说。
--graphics vnc,listen=127.0.0.1是只监听本地回环,装系统的界面通过 SSH 端口转发看。这样不用把 VNC 端口暴露出去,安全性更好。
首次启动之后,在另一个终端里把安装界面转出来:
ssh -L 5901:127.0.0.1:5901 user@host # 本地用任意 VNC 客户端连 127.0.0.1:59014.4 网络:默认 NAT 够用,桥接才进生产
libvirt 默认会创建一个叫default的网络,宿主上多出一块virbr0,客户机通过它做 NAT 上网,能出不能进。开发和测试完全够用,但生产环境通常要客户机和宿主机在同一个二层网络里。
桥接的配置在不同发行版和网络管理方式下差别很大。用 NetworkManager 的话,命令行是:
sudo nmcli connection add type bridge ifname br0 con-name br0 \ ipv4.method manual ipv4.addresses 192.168.1.50/24 \ ipv4.gateway 192.168.1.1 ipv4.dns 223.5.5.5 sudo nmcli connection add type ethernet ifname enp3s0 master br0 con-name br0-slave sudo nmcli connection up br0然后把 libvirt 的默认网络切到桥接,或者在virt-install时直接指定--network bridge=br0,model=virtio。注意桥接会把物理网卡的 IP 挪到桥上,中间会短暂断网,远程操作的话最好配个定时恢复脚本兜底,别问我怎么知道的。
如果不想动宿主机网络,还有一个折中方案是 macvtap,客户机直接借用物理网卡的 MAC 直通,配置量小,代价是宿主机和客户机之间不能直接通信(这是 macvtap 的固有特性),且部分交换机对多 MAC 场景有限制。
4.5 日常管理:几个我会背下来的命令
virsh list --all # 所有虚拟机状态 virsh dominfo ubuntu24 # 配置概览 virsh domblklist ubuntu24 # 磁盘列表 virsh domiflist ubuntu24 # 网卡列表 virsh vncdisplay ubuntu24 # 当前 VNC 端口 virsh console ubuntu24 # 串口控制台(需客户机配好 ttyS0) virsh autostart ubuntu24 # 开机自启 virsh domstats ubuntu24 --block # 块设备实时统计virsh domstats --block这个命令我强烈建议记住。它会打印每个磁盘的读写次数、字节数、延迟,做性能排查的时候,能一眼看出瓶颈是在客户机内部还是在存储路径上。
5. 不用 libvirt 的裸 QEMU,以及跨架构模拟
有时候就是需要裸跑 QEMU:排查 libvirt 层的问题、做一次性的系统安装、或者跑一个临时环境。这时候理解 QEMU 的参数结构就很有价值。
5.1 一条裸 QEMU 命令逐段拆
qemu-system-x86_64 \ -accel kvm \ -machine q35,accel=kvm \ -cpu host \ -smp 4,sockets=1,cores=2,threads=2 \ -m 4G \ -drive file=disk.qcow2,if=virtio,format=qcow2,cache=none,aio=io_uring \ -netdev user,id=n0,hostfwd=tcp::2222-:22 \ -device virtio-net-pci,netdev=n0 \ -vnc 127.0.0.1:1 \ -monitor stdio-accel kvm是加速开关,老教程里的-enable-kvm已经废弃了。-smp 4,sockets=1,cores=2,threads=2这种写法比单纯写-smp 4更明确,某些客户机操作系统会根据拓扑调整调度策略。-netdev user是 QEMU 内置的用户态网络,hostfwd把宿主的 2222 端口映射到客户机的 22 端口,做临时调试最方便,不需要任何 root 权限和网络配置改动。
-monitor stdio把 QEMU 的监视器接到当前终端,进去之后能敲info block、info network、snapshot这些命令。这个监视器不依赖客户机,哪怕系统已经卡死了,也能从宿主机侧看到状态,是排查"虚拟机毫无响应"这类问题的第一现场。
5.2 qemu 模拟 arm64:预期要放低
跨架构的场景很常见,比如在 x86 开发机上验证 arm64 的镜像构建。QEMU 能做到,但要走 TCG 软件模拟,性能大概只有原生的十分之一到五分之一。
qemu-system-aarch64 \ -machine virt \ -cpu cortex-a72 \ -smp 4 -m 4G \ -bios /usr/share/AAVMF/QEMU_EFI.fd \ -drive file=arm64-rootfs.qcow2,if=virtio,format=qcow2 \ -netdev user,id=n0,hostfwd=tcp::2223-:22 \ -device virtio-net-pci,netdev=n0 \ -nographic-machine virt是 arm64 下最通用的虚拟平台,-bios指向 AArch64 的 UEFI 固件(Debian/Ubuntu 系在qemu-efi-aarch64包里,路径通常是/usr/share/AAVMF/QEMU_EFI.fd)。注意-nographic会把串口输出直接接到终端,arm64 的virt平台默认会在串口上输出启动日志,比折腾图形界面省事得多。
这里有个必须强调的点:arm64 的 TCG 模拟无论怎么调参都不可能快。如果你的目的是大规模构建 arm64 镜像,正确做法是找一台真的 arm64 机器,或者用支持原生 arm64 的构建服务。QEMU 模拟适合做功能验证,不适合做性能测试。
5.3 qemu-user-static:另一条完全不同的路
上面说的是 full-system 模拟,还有一条路是用户态模拟,只模拟指令集不模拟整机。装qemu-user-static之后,配合 binfmt_misc,可以直接在 x86 上运行 arm64 的单个二进制:
sudo apt install -y qemu-user-static binfmt-support sudo systemctl restart systemd-binfmt # 验证 docker run --rm --platform linux/arm64 arm64v8/alpine uname -m这条路比整机模拟轻量得多,适合跑单进程、构建镜像、跑测试套件。代价是没有内核,涉及内核模块、io_uring、特定系统调用的程序可能跑不起来。判断标准很简单:只要能跑成容器镜像里的普通应用,用这条路;需要装系统、改内核参数、做网络实验,走 full-system 那条路。
6. 空间回收与性能调优:qcow2 的三个后续动作
虚拟机跑起来之后,真正让人头疼的是 qcow2 文件"只涨不跌"。客户机里删了 20G 数据,宿主的 qcow2 大小一点没变。这不是 bug,是 COW 机制的必然结果——被删掉的簇在宿主文件里仍然占着位置,只是客户机的文件系统不再引用它们了。
6.1 让 TRIM 指令真正传导下去
第一条要配的链路是discard。客户机的文件系统发出 TRIM,virtio-blk 传到 QEMU,QEMU 再通过unmap告诉宿主文件系统"这块可以还回去了"。任何一个环节断掉,空间都收不回来。
客户机侧确认挂载参数:
mount | grep discard如果没带discard,可以手动跑一次全盘 TRIM:
sudo fstrim -av我个人的习惯是不用挂载参数里的discard(它在每次删除时都触发,有额外开销),而是配一个定时任务每周跑一次fstrim。这样开销可控,空间也不会无限涨。
宿主机侧确认discard=unmap已经写进虚拟机定义:
virsh dumpxml ubuntu24 | grep -i discard如果没有,编辑一下:
virsh edit ubuntu24 # 在 <driver .../> 里加上 discard='unmap'改完之后需要重启虚拟机才生效。
6.2 压缩矩阵:三种压缩算法的实测取舍
qcow2 支持在转换时压缩,QEMU 5.0 之后还支持 zstd。压缩只在qemu-img convert时发生,运行中的虚拟机不会实时压缩。
# 转换成带压缩的 qcow2,用 zstd qemu-img convert -p -O qcow2 -c -o compression_type=zstd \ /var/lib/libvirt/images/ubuntu24.qcow2 \ /data/img/ubuntu24-zstd.qcow2三种压缩方式的取舍大致是这样:zlib 兼容性最好,任何版本的 QEMU 都能读,压缩率中等;zstd 压缩速度快、压缩率也不错,但要求 QEMU 5.0 以上且镜像 compat 版本为 1.1;lzo 压缩和解压都很快,压缩率略低。如果镜像要分发给不确定环境的同事,用 zlib 最稳妥。
注意 zstd 有个前置条件:镜像必须是 compat 1.1(qcow2 v3)。老镜像是 1.0 的话,先升一下:
qemu-img amend -o compat=1.1 disk.qcow26.3 快照链整理:别让层数无限长
用virsh snapshot-list能看到当前有哪些快照,用virsh snapshot-create-as建一个,virsh snapshot-revert回滚。qcow2 内部快照会直接生成一个新的层,快照多了链就长了。
整理的方式前面提过blockcommit,还有一个是blockpull,把后端文件的内容拉到当前盘里,这样可以把基础镜像独立出去:
virsh blockpull ubuntu24 vda --wait --verbose整理完成之后,用qemu-img map看一下实际分配的块分布,确认没有大量细碎的空洞:
qemu-img map --output=json /var/lib/libvirt/images/ubuntu24.qcow2 | head -20如果看到大量很短的 extent,说明碎片化比较严重,这时候做一次离线整理:
virt-sparsify --in-place /var/lib/libvirt/images/ubuntu24.qcow2--in-place是在原文件上操作,省空间但风险高,一定要先关机并备份。要更保险就用输出到新文件的方式:
virt-sparsify --compress --convert qcow2 \ /var/lib/libvirt/images/ubuntu24.qcow2 \ /data/img/ubuntu24-slim.qcow27. 常见问题与排查实录
前面讲的都是"正常路径",实际运维里遇到的多半是异常路径。这一节我把遇到过的问题整理成速查表,再挑三个典型场景展开说。
7.1 问题速查表
| 现象 | 大概率原因 | 排查动作 | 处理方式 |
|---|---|---|---|
虚拟机启动报Could not access KVM kernel module | /dev/kvm不可用或权限不足 | ls -l /dev/kvm,groups | 加载 kvm 模块,把用户加进 kvm 组 |
| qcow2 删除数据后体积不降 | discard 链路断掉 | virsh dumpxml查 discard 参数,客户机跑fstrim | 配discard=unmap,定期 fstrim |
| 写入延迟周期性抖动 | 元数据预分配不足或 cache 模式不当 | qemu-img info看 preallocation,virsh domstats看延迟 | 改preallocation=falloc,cache=none |
| 快照回滚后磁盘变得很大 | 内部快照层叠加过多 | virsh snapshot-list,qemu-img info --backing-chain | 做 blockcommit 合并 |
qemu-img check报引用计数错误 | 异常掉电导致 lazy refcount 未落盘 | qemu-img check输出详情 | qemu-img check -r all修复 |
| 迁移到另一台机器失败 | CPU 型号不兼容 | 对比两台宿主virsh capabilities | 改--cpu host-model并重启虚拟机 |
| 客户机网络时通时断 | MAC 地址冲突或网桥配置问题 | virsh domiflist看 MAC,brctl show看端口 | 重新生成 MAC,检查网桥成员 |
7.2 实录一:qcow2 从 40G 涨到 180G 的真相
有台跑日志分析的虚拟机,分配的盘只有 40G,跑了两周之后宿主上的 qcow2 文件涨到了 180G,看起来完全不合理。第一反应是"是不是被攻击了",但qemu-img info显示虚拟大小还是 40G,只是实际占用异常。
排查路径是这样的:先在客户机里df -h,看到根分区只用了 12G。再用du -sh逐层扫,发现/var/log/journal下面有个将近 100G 的目录——日志系统在磁盘满的时候反复写入失败、又反复重试,写进去了大量无效数据然后被删除,但 qcow2 里对应的簇已经分配出去了,客户机删除并不会自动还给宿主。
根因是discard链路没配。修复动作有三步:先限制 journal 的最大占用,避免再爆;然后在宿主的虚拟机定义里加上discard=unmap;最后在客户机里跑一次sudo fstrim -av。执行完之后,宿主上的 qcow2 从 180G 掉到了 26G。
经验教训是:只做客户机侧的容量监控是不够的,宿主文件的实际占用必须单独监控。我现在的告警规则里,qcow2 实际占用与虚拟大小的比值超过 1.5 就会触发提醒,这时候去看一眼基本都能提前发现问题。
7.3 实录二:快照链叠了 27 层之后
测试环境有个脚本,每天给虚拟机打一个内部快照。跑了将近一个月,有人反馈"这台机器启动要三分钟,跑起来也卡"。我用qemu-img info --backing-chain一看,链深度到了 27 层。
原理层面说,每读一个没在顶层命中的簇,都要一层一层往下查 L2 表;QEMU 会做缓存,但缓存容量有限,链一深,缓存命中率就掉下来,随机读的延迟会变得很难看。写入更麻烦,每次写都要定位到当前活跃层,元数据压力全堆在这一层上。
处理方式是blockcommit,但要注意顺序:不能一次性把 27 层全合掉,那样单次操作耗时太长,中途出错风险大。我的做法是分批合并,每次合并 5 到 8 层,中间观察virsh domstats --block的延迟指标。
virsh blockcommit testvm vda --active --pivot --verbose --wait之后我把脚本改成保留最近 3 个快照,更早的自动删除。这里有个细节:删除内部快照不会自动释放宿主空间,得配合一次fstrim或者qemu-img convert重写一遍文件,空间才会真正掉下来。
7.4 实录三:arm64 模拟环境里的诡异超时
有次在 x86 机器上用 QEMU 模拟 arm64 跑 CI 构建。单次构建本地跑 20 分钟没问题,但放进 CI 之后频繁超时。一开始怀疑是资源不足,加了 CPU 和内存,改善不明显。
后来把-smp拆成明确的拓扑-smp 4,sockets=1,cores=4,threads=1,并且关掉了宿主的 CPU 节能策略,把电源模式固定成性能模式,超时率明显下降。根本原因是 TCG 模拟对宿主的单核性能极其敏感,宿主一旦降频,模拟出来的客户机时间就直接被拉长,客户机里的超时检测很容易被触发。
顺带说清一个容易误解的点:QEMU 的 TCG 在多核场景下的扩展性并不好,加 vCPU 不等于线性提速,有时反而因为跨核同步开销导致更慢。跨架构模拟场景下,我一般建议 vCPU 数量不超过 4,把预算花在提高单核主频上。
8. 我踩过之后总结的几个判断口径
写到这里,前面所有内容其实都能压缩成几个判断口径,遇到类似问题时直接套用就行。
第一个口径是先分层再选型。看到一个名词,先问它在栈的哪一层。如果它属于内核层(KVM),那就去谈硬件支持;属于用户态程序层(QEMU),那就去谈参数和版本;属于格式层(RAW/qcow2),那就去谈元数据和预分配;属于策略层(COW/ROW),那就去谈写放大和空间回收。分完层,很多看似矛盾的说法就自动和解了。
第二个口径是格式跟着业务走,不要跟着习惯走。需要快照回滚和模板分发就用 qcow2,需要极限 IOPS 并且能接受运维复杂度就用 RAW,两者不是非此即彼,一台宿主机上混用完全正常——模板盘用 qcow2 方便克隆,核心数据库盘用 RAW 保性能。
第三个口径是空间问题永远先看回收链路。qcow2 涨了不降,十次里有八次是discard没配或者客户机没跑fstrim,剩下两次是快照层没合。先把这两件事确认掉,再去怀疑别的。
最后一个我自己用得很顺手的习惯:每次给虚拟机做重大变更之前(改格式、合快照、调预分配),先跑一次qemu-img check,把输出存下来。变更之后再跑一次,两份输出对比一下,元数据有没有被改坏一目了然。这个小动作救过我至少两次,比事后从备份里恢复省事得多。