VFIO 虚拟机启动缓慢排查
May 8, 2023 00:00 · 1179 words · 3 minute read
一句话描述问题现象:通过 VFIO 透传 GPU 的虚拟机(libvirt domain)在启动后长时间(二十分钟以上)暂停(paused
状态)后才进入 running
状态。
$ virsh list | grep paused
88 cpcs-8fc4ed15f11840afaac40424078542dd_ecs-test0 paused
宿主机负载并不高,资源充足;libvirtd 负载也不高,排除 libvirtd 响应 API 调用不及时。
在 libvirt domain paused 时,qemu 进程已由 libvirt 拉起:
$ ps -ef | grep ecs-test0
qemu 318004 1 99 10:25 ? 00:00:08 /usr/libexec/qemu-kvm -name guest=cpcs-8fc4ed15f11840afaac40424078542dd_ecs-test0,debug-threads=on -S balabala
qemu 进程像是阻塞了,接下来需要确定 qemu 的确切状态。
通过 qmp 协议与 qemu 进程通信来查询其状态。首先 libvirt 的 virsh 客户端支持 qmp,尝试使用 virsh qemu-monitor-command 与 domain 通信:
$ virsh qemu-monitor-command cpcs-8fc4ed15f11840afaac40424078542dd_ecs-test0 '{"execute": "query-status"}'
error: Timed out during operation: cannot acquire state change lock (held by monitor=remoteDispatchDomainCreateWithFlags)
因为 qemu 启动后直接阻塞,libvirt 启动 API 调用也阻塞,此时 domain 被锁定,无法对其进行操作,所以要另找办法与 qemu 进程通信。
通过 libvirt 拉起的 qemu 进程,默认通过 /var/lib/libvirt/qemu/ 路径下的 monitor usock 文件暴露 qemu 监控 API,我们自行连接该 usock 来与 qemu 进程通信时,要先关闭 libvirtd 进程,否则无应答:
$ nc -U /var/lib/libvirt/qemu/domain-88-cpcs-8fc4ed15f11840a/monitor.sock
{"QMP": {"version": {"qemu": {"micro": 0, "minor": 2, "major": 4}, "package": "qemu-kvm-4.2.0-59.module+oc8.5.0+46+33f0d227"}, "capabilities": ["oob"]}}
{"execute" : "qmp_capabilities"}
{"return": {}}
{"execute": "query-status"}
{"return": {"status": "prelaunch", "singlestep": false, "running": false}}
通过查看 qemu 文档得知,prelaunch 表示 qemu 进程启动时带有 -S
选项,这时 Guest OS 并未启动;-S
表示 qemu 进程运行后 Guest OS 启动前这段时间冻结 CPU。
用 perf 对 qemu-kvm 进程采样并输出报告:
$ perf record -F 99 -a -g -p 318004 -- sleep 60
Warning:
PID/TID switch overriding SYSTEM
[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 1.575 MB perf.data (6001 samples) ]
$ $ perf report -n --stdio
# To display the perf.data header info, please use --header/--header-only options.
#
#
# Total Lost Samples: 0
#
# Samples: 6K of event 'cycles'
# Event count (approx.): 211618809815
#
# Children Self Samples Command Shared Object Symbol
# ........ ........ ............ ............. ................... ..............................................
#
99.69% 0.00% 0 qemu-kvm libc-2.28.so [.] __GI___ioctl
|
---__GI___ioctl
entry_SYSCALL_64_after_hwframe
do_syscall_64
__x64_sys_ioctl
ksys_ioctl
do_vfs_ioctl
vfio_fops_unl_ioctl
vfio_iommu_type1_ioctl
|
--99.66%--vfio_pin_pages_remote
|
--99.60%--vaddr_get_pfn
get_user_pages
__gup_longterm_locked
__get_user_pages
|
--99.50%--handle_mm_fault
|
--99.45%--__handle_mm_fault
|
--99.00%--do_huge_pmd_anonymous_page
alloc_pages_vma
__alloc_pages_nodemask
__alloc_pages_slowpath
|
|--92.42%--__alloc_pages_direct_compact
| try_to_compact_pages
| |
| --92.40%--compact_zone_order
| |
| |--88.32%--compact_zone
| | |
| | |--69.67%--__reset_isolation_suitable
| | | |
| | | |--67.11%--__reset_isolation_pfn
很明显 qemu-kvm 99%+ 的 CPU 时间都在运行 vfio_pin_pages_remote
函数。
因为 qemu-kvm 利用 Linux VFIO 实现 PCI 设备(这里是 GPU)透传,从函数名中也能看出来。
VFIO 驱动本身就是一个 IOMMU 框架,用于在安全的 IOMMU 保护环境中向用户空间暴露对设备的直接访问。而 IOMMU 可以把设备的 IO 地址映射成虚拟地址,为设备提供页表映射,设备通过 IOMMU 将数据直接 DMA 写到用户空间(不经过 CPU),必须使用连续的物理内存。
vfio_pin_pages_remote
就是用于固定住一段连续物理内存的。调用 do_huge_pmd_anonymous_page
函数来申请透明大页(THP)处理缺页异常说明当前系统已经没有连续的 2M 物理内存。再顺着调用链来到 try_to_compact_pages
函数,这是 Linux 内核中内存碎片整理(内存规整)的核心函数。
看一眼宿主机内存使用情况:
$ free -h
total used free shared buff/cache available
Mem: 1.0Ti 855Gi 143Gi 20Mi 7.9Gi 144Gi
Swap: 0B 0B 0B
虽然系统空闲内存足够,但由于 IOMMU 需要大段连续物理内存,看上去无法找到造成内存分配失败。此时就要整理内存碎片,原理暂且不表。qemu-kvm 进程启动后“阻塞”半小时才进入 running 状态,由申请 THP 触发的内存规整耗费了相当多的时间。
查看透明大页 THP 是否开启:
$ cat /sys/kernel/mm/transparent_hugepage/enabled
[always] madvise never
发现处于开启状态,临时关闭透明大页 THP:
$ echo never > /sys/kernel/mm/transparent_hugepage/enabled
$ cat /sys/kernel/mm/transparent_hugepage/enabled
always madvise [never]
重新拉起虚拟机,domain paused
状态时长缩短至 30 秒内,之后转换至 running
状态,开始初始化 Guest OS。
透明大页往往会给系统带来副作用而非优化,如非必要建议关闭或设置为 madvise(在程序中通过 madvise 系统调用来分配 THP)。