前阵子在一台ARM服务器上折腾KVM虚拟化,从确认硬件支持、装好libvirt,到第一个ARM虚拟机正常联网跑业务,前前后后花了两天时间。整个过程和x86平台上做KVM的思路高度相似,但坑点完全不一样——固件、镜像、设备模型、网卡驱动,稍不留意就会卡住。这篇就把ARM服务器虚拟化从KVM安装到虚拟机创建的完整流程、关键参数和排错经验整理出来,给刚接触ARM平台的运维、虚拟化开发,以及想用ARM服务器多开测试环境的同学做个参考。
1. 为什么要在ARM服务器上做KVM虚拟化
1.1 先搞清楚ARM和x86的虚拟化差异
很多人一听到ARM服务器第一反应是“这玩意能不能跑虚拟机”,其实ARMv8-A架构原生就支持虚拟化扩展,KVM在ARM64平台上已经是主流方案。核心差异在于:x86的虚拟化依赖Intel VT-x或AMD-V,ARM则靠异常级别EL2,也就是hypervisor运行层。KVM作为Linux内核模块运行在内核态,正好利用EL2把虚拟机的指令执行和资源访问隔离起来,性能和裸机差距已经控制得很小。
另一个差异是设备模型。x86上有Legacy BIOS/UEFI、PCIe拓扑、传统IDE/SATA控制器,ARM平台上更常见的是QEMU virt机型和UEFI固件AAVMF。ARM虚拟机一般跑的也是ARM64内核,所以在虚拟设备层面大量使用virtio——半虚拟化的virtio-net、virtio-blk、virtio-scsi几乎成了标配。如果你拿x86的镜像或x86的配置习惯去套ARM环境,最容易踩的就是固件不对、机器类型不对、virtio驱动没进initramfs。
1.2 为什么选KVM而不是容器或纯模拟器
ARM服务器上的业务隔离方案无非几种:Docker容器、QEMU纯软件模拟、KVM虚拟化。容器的隔离靠内核namespace和cgroup,共享宿主机内核,密度高、启动快,但遇到需要独立内核、独立内核参数、独立驱动栈的场景就不够用了。纯QEMU模拟器比如Linux系统下的TTC、或者说纯粹用TCG模式跑不同架构镜像,能模拟但速度感人,不适合作为生产虚拟化方案。
KVM选择的是“内核虚拟化+用户态设备模拟”的组合:KVM模块负责CPU和内存的虚拟化,QEMU负责设备模拟和生命周期管理。这个组合的好处是稳定、成熟、生态大,libvirt管理工具也齐全。我在实际项目里最终使用KVM,因为我需要跑多个不同发行版的ARM系统镜像,要有独立的网络栈、独立的系统服务、独立的崩溃恢复边界,这些容器给不了。
1.3 这套东西适合谁、能解决什么问题
如果你是运维工程师,手头有ARM服务器,想在一台物理机上分出多个业务虚拟机;如果你是嵌入式或底层开发者,需要反复测试ARM64的发行版镜像,又不想频繁折腾物理机;或者你只是好奇ARM平台上虚拟化到底怎么做,这篇文章的流程都可以直接抄作业。ARM服务器虚拟化的收益也很明确:硬件利用率更高、环境隔离更干净、测试环境可以任意销毁重建。实践中最常见的几个用途包括:跑ARM版云镜像做CI构建节点、验证ARM架构下的数据库和中间件兼容性、给没有图形环境的ARM服务器提供独立调试环境。
2. 硬件准备与KVM依赖安装
2.1 检查CPU虚拟化支持是不是白折腾的开始
无论宿主机是什么发行版,第一步永远是确认硬件和内核有没有准备好。最直接的检查方式:
lscpu | grep -i virtualization cat /proc/cpuinfo | grep -i virt ls -l /dev/kvmARM64平台上lscpu输出里会看到Virtualisation字段标成KVM,/proc/cpuinfo里能看到virt特性关键字。最关键的是/dev/kvm这个设备文件,libvirt和QEMU都要靠它和内核交互。如果/dev/kvm不存在,大概率是内核没有加载kvm模块:
lsmod | grep kvm modprobe kvmARM平台常见的KVM模块名是kvm,部分内核会按机器类型细分,比如kvm_vhe或者kvm_nvhe,但统一入口还是kvm。加载不了时先检查固件里有没有开启虚拟化扩展,不少ARM服务器在BIOS/固件里默认关掉,需要进UEFI设置打开才能看到/dev/kvm。
内存和磁盘也提前算好。一个2GB内存的虚拟机配合20GB虚拟磁盘,宿主机至少要留出4GB以上空闲内存,磁盘分区建议用LVM或者预留独立卷组,方便后期扩容。这个环节看起来基础,但实际有相当比例的人前面全对,最后倒在没有/dev/kvm上。
2.2 KVM、QEMU和libvirt组件的安装细节
以Ubuntu/Debian系ARM服务器为例,安装命令并不复杂:
sudo apt update sudo apt install qemu-system-arm qemu-utils libvirt-daemon-system libvirt-clients virtinst -y这里有个容易忽略的点:qemu-system-arm包提供的是QEMU arm和arm64系统模拟程序,qemu-system-aarch64也在其中。如果只装了qemu-kvm或者x86的包,后面启动ARM虚拟机经常会提示找不到可用的machine type。安装完成后启动libvirtd:
sudo systemctl enable --now libvirtd sudo systemctl status libvirtdlibvirtd正常后验证工具链是否就绪:
virsh version virsh list --all第一次执行virsh list如果提示连接失败,看一下libvirt-daemon-system的服务是否挂在systemd下,以及当前用户是否在libvirt组里。建议把常用账号加进libvirt和kvm两个组:
sudo usermod -aG libvirt,kvm $USER重新登录后日常操作就不用总加sudo了。ARM平台这一步和x86几乎一致,差异主要在qemu包名和机器类型上。
2.3 ARM平台安装阶段就要注意的两个细节
第一,发行版软件包要选对架构。ARM服务器一般跑aarch64版系统,APT会自动安装arm64的包,这点一般不会错。但如果你是从x86上把整个根文件系统迁移过来,或者用了一些奇怪的第三方源,可能出现包架构混装,表现为QEMU启动时报exec format error或者无法识别PE格式。用file命令快速确认:
file /usr/bin/qemu-system-aarch64第二,确认机器类型是否完整。QEMU的机器类型决定了虚拟总线拓扑,ARM平台最常用的是virt-4.2、virt-5.2这些带版本号的机型,它们通过ACPI和GPEX桥模拟PCIe设备。安装后可以用下面命令看看当前QEMU支持哪些:
qemu-system-aarch64 -machine help | grep virt实际生产环境中我基本只用virt机型,它和KVM虚拟机配合最稳、virtio设备的支持也最完整。
3. ARM虚拟机镜像的准备与格式处理
3.1 从哪获取可靠的ARM镜像
创建ARM虚拟机最关键的一步是拿到能启动的ARM64系统镜像。官方渠道首推各大发行版的cloud镜像,比如Ubuntu Cloud Images里带arm64标识的qcow2文件,Debian官网的generic/cloud arm64镜像也是直接可用的qcow2格式。这类镜像已经预装cloud-init、打过virtio驱动、可以调整根分区大小,是KVM环境下最省事的起点。
下载时留心三点:文件名里是否明确写了arm64、aarch64,别下成amd64;qcow2镜像的SHA256校验值要核对;下载后先用qemu-img info确认格式再创建虚拟机。部分镜像网站还会给出具体的虚拟机类型和最小内存建议,也值得大致扫一眼。
如果你需要的是老版本系统,比如要跑一个CentOS 7 ARM兼容环境,官方镜像链接可能已经下线,此时需要本地手工构建或者寻找社区长期归档的镜像。社区镜像质量参差不齐,我的原则是优先选维护时间久、更新频繁的项目,下载后多跑几步自检,避免带着脏镜像排错浪费半天。
3.2 镜像格式:qcow2和raw到底怎么选
ARM虚拟化里img和qcow2两种格式都很常见。img通常是裸格式raw,磁盘内容完全按偏移写入,性能直接但占空间大,快照和稀疏分配能力也很弱。qcow2是QEMU的写时复制格式,支持稀疏文件、快照、压缩、加密,日常使用更推荐。如果手里只有raw格式或者别的虚拟化格式,可以转换:
qemu-img convert -f raw -O qcow2 input.img output.qcow2反过来也是同样的思路,conver命令的-f和-O对调即可。转换完成后务必再看一眼:
qemu-img info output.qcow2重点关注disk size和virtual size。disk size是实际占用宿主机磁盘的空间,virtual size是虚拟机的逻辑磁盘大小。如果你在宿主机看到的磁盘占用远小于预期文件大小,说明稀疏特性生效了,这是正常现象。
3.3 seed镜像:用cloud-init完成首次登录准备
ARM cloud镜像默认不允许root直接登录,也没有预设密码,直接启动会卡在不知道该用什么账号进去。解决办法是准备一个cloud-init的seed镜像挂给虚拟机,或者叫nocloud ISO。最常用的工具是cloud-localds:
sudo apt install cloud-image-utils -y cat > seed.cfg << 'EOF' #cloud-config password: your-password chpasswd: { expire: False } ssh_pwauth: True EOF cloud-localds seed.iso seed.cfgcloud-localds会把配置写成一个ISO镜像,启动时作为虚拟光驱挂载给虚拟机。cloud-init会在首次启动时读取它,完成账号密码设置和SSH key写入。如果你不需要密码登录而是用密钥,也可以在seed.cfg里加ssh_authorized_keys字段。这里还有个小细节:不同发行版cloud-init读取的label不太一样,Ubuntu要求ISO卷标为cidata,cloud-localds生成的镜像默认就是cidata,直接用就好。某个版本如果遇到启动后没有执行cloud-init,先确认虚拟机的光驱总线是不是被识别为virtio,而不是IDE。
3.4 磁盘扩容和分区调整
ARM cloud镜像默认虚拟磁盘一般只有2到10GB,明显不够用。创建虚拟机前先给一个定制大小:
qemu-img create -f qcow2 arm-test.qcow2 30G把官方基础镜像的根分区整体倒进去,再动态扩容:
qemu-img convert -O qcow2 base-arm64.img arm-test.qcow2 qemu-img resize arm-test.qcow2 30G光改虚拟尺寸还不够,虚拟机启动后分区表和文件系统还是原来的大小,进入虚拟机执行growpart和resize2fs或者更简单的方式是直接依赖cloud-init的growpart模块,它会在首次启动自动扩展根分区。cloud-init配置里确认有growpart、resizefs这两个模块存在即可。如果用的是非cloud镜像,才需要手动fdisk和resize2fs,步骤会繁琐一些。
4. 虚拟机创建完整实操流程
4.1 virt-install一行命令创建ARM虚拟机
当你已经准备好qcow2磁盘镜像和seed.iso之后,用virt-install创建虚拟机比手写XML快得多,而且参数语义非常清晰。下面是我实际用过的创建命令:
sudo virt-install \ --name arm-test \ --virt-type kvm \ --ram 2048 \ --vcpus 2 \ --disk path=/var/lib/libvirt/images/arm-test.qcow2,format=qcow2,bus=virtio \ --disk path=/var/lib/libvirt/images/seed.iso,device=cdrom \ --os-variant ubuntu22.04 \ --network network=default,model=virtio \ --graphics none \ --console pty,target_type=serial \ --import逐参数解释一下,方便你按需调整:
- --virt-type kvm指定走KVM加速而不是纯QEMU模拟,如果写成qemu,性能断崖下跌
- --ram和--vcpus按宿主机资源余量给,ARM服务器核心多,常见配置是4核8G起步
- --disk path指定磁盘路径,format改成qcow2,bus指定virtio,这是ARM平台性能关键
- --disk device=cdrom把seed镜像挂成光驱,cloud-init才能读到
- --network network=default走libvirt默认NAT网络,外部再映射端口
- --graphics none配合--console pty,纯命令行环境没有窗口也能进控制台
- --import告诉virt-install直接从已有磁盘启动,不再进行系统安装流程
执行完成后终端会显示虚拟机的域ID和状态。ARM平台下加--console pty,target_type=serial非常必要,否则一旦网络没通,你连备用的字符终端入口都可能没有。
创建成功后用virsh管理:
virsh list --all virsh start arm-test4.2 为什么ARM平台要特别留意机型UEFI固件
x86上默认BIOS就能启动绝大多数系统,ARM却不行。ARM云镜像普遍带EFI分区,引导流程依赖UEFI固件AAVMF。virt-install在ARM平台一般会自动探测并使用/usr/share/AAVMF/AAVMF_CODE.fd,但如果你跳过virt-install,手动virsh define一个手写XML,忘了loader字段会直接启动失败。
判断固件有没有配好的办法是看启动日志:如果虚拟机一直停在黑屏卡死、或virsh start后立刻回到offline状态,大概率是UEFI缺失。可以在virt-install命令里显式加上:
--boot uefi或者在XML中手动指定loader。这个细节直接决定镜像能不能引导,我在ARM虚拟化里踩过的最深坑就是这里。
4.3 网络拓扑:NAT还是桥接
ARM虚拟化里的网络配置和x86逻辑一致,但实际用法有讲究。libvirt默认提供一个名为default的NAT网络,虚拟机可以上网,宿主机也能访问到虚拟机,但外部主机无法直接连进来。如果虚拟机只用于内部测试,用default最省事:
virsh net-list --all virsh net-start default如果虚拟机要对外提供服务,建议改成桥接。宿主机配一个bridge接口,虚拟机直接挂在物理网络里。以netplan为例,可以新建一个bridge接口,再用libvirt的virbr0之外的桥接方式。常见配置是把物理网卡加入br0:
network: version: 2 renderer: networkd ethernets: enp3s0: dhcp4: no bridges: br0: interfaces: [enp3s0] dhcp4: yes然后告诉libvirt使用系统已有桥接:
sudo virsh net-list --all # 使用bridge模式时直接在virt-install里写: --network bridge=br0,model=virtio这里注意ARM平台虚拟机内网卡默认叫ens命名,和硬件直通无关。网络模型强烈建议virtio,模拟的e1000或rtl8139虽然启动兼容性高,但性能差很多,对ARM这种注重能效的平台不划算。
4.4 创建后如何快速验证虚拟机状态
创建流程结束不代表虚拟机真的可用,我一般用下面几个命令依次验证:
virsh list --all virsh domifaddr arm-test virsh console arm-testvirsh domifaddr会列出虚拟机的接口IP,前提是虚拟机内DHCP已经拿到地址。如果这里空白的,大概率cloud-init没跑通或者网卡模型没配对。virsh console则是直接进入串行控制台,登录后在guest内检查:
ip a df -h cat /proc/cpuinfo | grep -i model看到处理器是CPU implementer等ARM标识,同时根分区已经扩容到目标大小,这套ARM虚拟机就算真正创建成功且可以投入使用了。到这里流程基本走完,但后面还有一堆实际使用中的坑值得专门写一写。
5. 常见问题读取与排查技巧实录
5.1 启动失败:黑屏、立刻离线、固件不匹配
ARM虚拟化最典型的启动问题就是黑屏或virsh start后虚拟机立刻回到关闭状态。排查顺序固定为:先看libvirt日志,再看QEMU输出。
sudo tail -f /var/log/libvirt/qemu/arm-test.log日志里经常出现的关键词有:“failed to initialize KVM”“Could not open '/dev/kvm'”“Unable to find any firmware”。三种情况应对完全不同:
- /dev/kvm缺失:按第2章检查内核模块和固件虚拟化开关
- firmware找不到:安装AAVMF包,并给virt-install补--boot uefi参数
- machine type不匹配:在virt-install加--machine virt,或者在XML中写明machine='virt-4.2'
有一次排查半天发现是模板里的machine写成pc了,那是x86的机型,ARM平台上直接起不来。建议创建前用qemu-system-aarch64 -machine help确认你用的机型在列表内。
5.2 虚拟机起来了但IP拿不到,网卡配置玄学
另一个高频问题:虚拟机状态是running,但virsh domifaddr看不到IP。先别急着查DHCP,先看网卡模型对不对:
virsh domiflist arm-test确认interface model是virtio。如果在boot阶段就加载不了virtio-net驱动,虚拟机内部只会看到一个不存在的网卡。路径不对时,通常虚拟机里ls /sys/class/net什么都看不到。解决办法是换一块非virtio网卡先启动,进入系统把virtio相关内核模块放进initramfs,重建后再切回virtio。
还有一种情况是cloud-init的seed没挂载。检查虚拟机光驱是不是还在:
virsh domblklist arm-test如果光驱设备消失或没有挂载,重新添加seed.iso并重启,或者直接在控制台里手动cloud-init clean && cloud-init init。实测中seed.iso文件权限也要注意,如果属主不是root导致libvirt无法读取,XML定义没问题但启动会挂载失败。
5.3 磁盘性能差,CPU大量浪费在IO等待上
创建虚拟机时磁盘bus没写,或者写了默认的ide/sata,ARM平台上性能会很难看。务必将磁盘总线指定为virtio,也就是--disk path=xxx,format=qcow2,bus=virtio。同时cache模式可以按场景调:
- cache=none配合写回策略,适合大多数数据库类应用
- cache=writeback适合IO频繁但能容忍掉电丢失业务的场景
- cache=directsync最安全但性能最差
我在实际项目里用cache=none加virtio-blk,配合图片存储在NVMe盘上,吞吐量接近宿主机裸盘性能。还可以给网络开多队列:
sudo virsh destroy arm-test sudo virsh edit arm-test在interface节点里加上driver多队列属性,ARM平台多核性能优势才能发挥出来。
5.4 ARM虚拟化常见问题速查表
| 现象 | 常见原因 | 处理建议 |
|---|---|---|
| 创建后立刻offline | 未指定UEFI固件 | 加--boot uefi或加载AAVMF |
| 黑屏无输出 | machine type不对或缺少串口控制台 | 使用virt机型,加--console pty,target_type=serial |
| 虚拟机内看不到网卡 | 网卡模型不是virtio且内核缺驱动 | 改用model=virtio,重建initramfs |
| cloud-init没生效 | seed.iso未挂载或卷标不对 | 检查domblklist,用cloud-localds生成cidata卷标 |
| 分辨率/显示异常 | 使用了--graphics vnc但没有VNC环境 | 改用--graphics none,串口访问 |
| 性能不达标 | 磁盘总线和缓存模式不对 | bus=virtio,cache=none |
| 扩容不生效 | cloud-init没有growpart模块 | 安装cloud-growpart或手动resize2fs |
这个表基本覆盖了我这两天踩到的全部问题。其中印象最深的还是固件问题,ARM虚拟化确实比x86多了这一层门槛,但只要理解UEFI和machine type这两个关键点,整个流程就通顺得多。
6. 一点经验延伸:ARM虚拟化还能深入玩些什么
个人实际体会到,ARM服务器虚拟化最大的价值不只是“把虚拟机跑起来”,而是服务体系逐渐完善。现在很多中间件都出了ARM版本,比如JDK 11就有arm64版本,不少数据库、微服务中间件也能直接跑在ARM虚拟机上。如果你要在ARM虚拟环境里搭建业务栈,第一步不是急着装服务,而是先把基础镜像和网络模型稳定下来,再逐层加组件。
我在这个项目上最后悔的就是最开始没有先把UEFI固件和virtio驱动验证完就批量创建虚拟机,结果一次性制造了几台无法启动的废域。建议新接触的同学每一层都先跑一台最小虚拟机验证,再把镜像模板固化成快照,后续克隆就很快了。
另外,ARM虚拟化后续可以研究的主线还有不少:virtiofs文件共享可以让宿主机和虚拟机共享目录,PCI直通可以把GPU或网卡直接给到虚拟机,甚至可以在ARM服务器上结合用户态网络模拟做一些边缘场景。早一点把这些能力用起来,ARM服务器就不再是只能跑几个容器的“特殊硬件”,而是能承载完整虚拟化业务的基础设施。