首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >QEMU虚拟机优化

QEMU虚拟机优化

作者头像
姚华
发布2025-08-20 17:24:27
发布2025-08-20 17:24:27
2150
举报

原文链接

有很多方法可以微调QEMU虚拟机性能。这篇综合性文章包含了针对不同类型应用优化和提升虚拟机性能的各种方法。虽然本文专门针对Debian 9和QEMU,但它可以很容易地适配到其他Linux发行版和应用。

术语

  • 裸机(Bare-Metal)
    • 物理计算机系统及其上运行的基础操作系统。
  • 宿主机(Host)
    • 运行虚拟机的裸机。
  • 线程(Thread)
    • CPU的一个核心或SMP/超线程核心。
  • CPU线程(CPU thread)
    • 宿主机系统CPU上的一个线程。
  • vCPU线程(vCPU thread)
    • 虚拟机虚拟化CPU上的一个线程。

CPU隔离

将虚拟机的vCPU线程与宿主机系统隔离可以确保宿主机上的进程不会影响被隔离虚拟机的性能。这可以通过在启动时使用静态内核参数isolcpus或在运行时使用cpuset或cset shield(cpuset的接口)来设置。

使用"isolcpus"将QEMU线程与宿主机进程隔离

如果要求虚拟机或其他延迟敏感应用具有绝对最低的延迟,使用isolcpus来屏蔽CPU线程免受Linux进程调度器的影响是最合适的方法。虽然这种方法可能实现最低延迟,但它也是最不灵活的。这个内核参数在启动时设置,将阻止Linux进程调度器在所有被隔离的线程上运行任务。需要手动将每个应用的PID绑定到被隔离的CPU线程,并且需要重启才能将被隔离的CPU线程返回到Linux进程调度器。

编辑/etc/default/grub,将isolcpus=列表内核参数添加到GRUB_CMDLINE_LINUX_DEFAULT变量中。列表是要从Linux进程调度器中隔离的宿主机CPU线程的列表或范围。使用,分隔选择单个线程,或使用-分隔选择线程范围。

代码语言:shell
复制
/etc/default/grub
# 如果更改此文件,请之后运行'update-grub'来更新
# /boot/grub/grub.cfg。
# 有关此文件中选项的完整文档,请参阅:
#   info -f grub -n 'Simple configuration'

GRUB_DEFAULT=0
GRUB_TIMEOUT=0
GRUB_RECORDFAIL_TIMEOUT=$GRUB_TIMEOUT
GRUB_DISTRIBUTOR=`lsb_release -i -s 2> /dev/null || echo Debian`
GRUB_CMDLINE_LINUX=""

# 指定单个线程
GRUB_CMDLINE_LINUX_DEFAULT="quiet intel_iommu=on isolcpus=8,9,10,11,12,13,14,15,24,25,26,27,28,29,30,31"
# 或指定线程范围
GRUB_CMDLINE_LINUX_DEFAULT="quiet intel_iommu=on isolcpus=8-15,24-31"

保存并关闭文件,然后运行update-grub来更新grub。

重启并使用htop或gnome-system-monitor等系统监视器来确保正确的CPU线程已被隔离且没有进程在它们上面运行。使用stress -c nproc 命令对宿主机CPU施加压力。被隔离的CPU线程上应该没有负载。

每个QEMU vCPU线程都需要手动绑定到被隔离的CPU线程,使用taskset或QEMU CPU亲和性补丁来完成。这两种方法在本文后面都有解释。

使用"cset shield"动态将QEMU线程与宿主机进程隔离

cset shield比isolcpus更加灵活,可以在不重启宿主机或停止虚拟机的情况下即时创建/删除和修改称为shield的隔离CPU线程组。性能与isolcpus几乎相同,但由于一些底层内核进程无法移出shield,会有轻微的延迟损失。

首先创建一个shield,通过运行以下命令来完成。

代码语言:shell
复制
# 指定单个线程
cset shield -c 8,9,10,11,12,13,14,15,24,25,26,27,28,29,30,31
# 或指定线程范围
cset shield -c 8-15,24-31
# 将所有可移动的内核线程移出shield
cset shield --kthread on

可以使用cset shield启动QEMU,或者在之后将PID/s添加到shield中。使用以下命令使用cset shield启动QEMU。

代码语言:shell
复制
cset shield --exec qemu-system-x86_64 -- -$OPTS

或者,将已在运行的PID/s添加到shield中,如下所示。

代码语言:shell
复制
cset shield --shield --threads --pid $(pidof qemu-system-x86_64)

在某些时候,可能需要禁用shield以允许宿主机再次与虚拟机共享CPU资源。这不会/不应该导致虚拟机暂停或终止。使用以下命令来完成。

代码语言:shell
复制
cset shield --reset

使用"cpuset/cgroups"动态将QEMU线程与宿主机进程隔离

cpuset是cgroups的一个子系统,可以在不重启宿主机系统或停止虚拟机的情况下即时创建/删除和修改隔离CPU线程组。这是cset使用的底层内核驱动,因此两种方法的性能相同。然而,直接与内核驱动交互允许更大的灵活性。通过使用cpuset,为不同类型的QEMU线程分配单独的cpusets允许进一步的隔离。此示例将QEMU vCPU线程设置为与宿主机系统的第二个CPU节点1/1匹配,然后将I/O线程设置为第一个CPU节点的最后2个线程,以便大量I/O访问对虚拟机CPU性能的影响最小。

首先使用以下命令创建以下cpusets。

代码语言:shell
复制
# 为宿主机系统上运行的所有其他进程创建一个名为"system"的cpuset
mkdir /sys/fs/cgroup/cpuset/system
# 设置cpuset.mems为"system" cpuset中的进程应该有权访问的内存节点
echo 0-1 > /sys/fs/cgroup/cpuset/system/cpuset.mems
# 指定"system" cpuset可用的核心(第一个numa节点上的前7个核心及其SMP线程)
echo 0-6,16-22 > /sys/fs/cgroup/cpuset/system/cpuset.cpus

# 为QEMU父进程及其所有工作进程创建一个名为"qemu-virt"的cpuset
mkdir /sys/fs/cgroup/cpuset/qemu-virt
# 设置cpuset.mems为"qemu-virt" cpuset中的进程应该有权访问的内存节点
echo 0-1 > /sys/fs/cgroup/cpuset/qemu-virt/cpuset.mems
# 指定"qemu-virt" cpuset可用的核心(第一个numa节点上的最后一个核心及其SMP线程)
echo 7,23 > /sys/fs/cgroup/cpuset/qemu-virt/cpuset.cpus

# 为QEMU vCPU进程创建一个名为"qemu-vcpu"的cpuset
mkdir /sys/fs/cgroup/cpuset/qemu-vcpu
# 设置cpuset.mems为"qemu-vcpu" cpuset中的进程应该有权访问的内存节点
echo 0-1 > /sys/fs/cgroup/cpuset/qemu-vcpu/cpuset.mems
# 指定"qemu-vcpu" cpuset可用的核心(整个第二个节点)
echo 8-15,24-31 > /sys/fs/cgroup/cpuset/qemu-vcpu/cpuset.cpus

接下来使用以下命令将所有进程从根cpuset移动到system cpuset中。

代码语言:shell
复制
# 将"root" cpuset中的所有PID复制到"system" cpuset中
cat /sys/fs/cgroup/cpuset/tasks | xargs -n1 -i echo {} > /sys/fs/cgroup/cpuset/system/tasks
# 清除"root" cpuset中的所有PID
echo > /sys/fs/cgroup/cpuset/tasks

现在将QEMU父PID移动到qemu-virt cpuset中。

代码语言:shell
复制
# 注意:
# "$NAME"是我们设置的一个变量,用于在宿主机系统上区分不同的虚拟机。它应该与qemu参数中的"-name"相同
# 为了使"for"循环工作并区分QEMU vCPU线程与其他线程,有必要添加'-name $NAME,debug-threads=on' qemu参数

# 将QEMU父PID移动到"qemu-virt" cpuset中
echo $(pstree -pa $(pidof qemu-system-x86_64) | grep $NAME | awk -F',' '{print $2}' | awk '{print $1}') > /sys/fs/cgroup/cpuset/qemu-virt/tasks ;
# 创建一个计数器并将其设置为0
HOST_THREAD=0
# 对于每个QEMU vCPU PID
for PID in $(pstree -pa $(pstree -pa $(pidof qemu-system-x86_64) | grep $NAME | awk -F',' '{print $2}' | awk '{print $1}') | grep CPU |  pstree -pa $(pstree -pa $(pidof qemu-system-x86_64) | grep $NAME | cut -d',' -f2 | cut -d' ' -f1) | grep CPU | sort | awk -F',' '{print $2}')
    do let HOST_THREAD+=1
    # 将QEMU vCPU PID添加到"qemu-vcpu" cpuset中
    echo $PID > /sys/fs/cgroup/cpuset/qemu-vcpu/tasks
done

删除cpusets并将系统恢复到正常状态就像之前一样,只是反过来。

代码语言:shell
复制
# 将"system" cpuset中的所有PID复制到"root" cpuset中
cat /sys/fs/cgroup/cpuset/system/tasks | xargs -n1 -i echo {} > /sys/fs/cgroup/cpuset/tasks
# 从"system" cpuset中移除PID
echo > /sys/fs/cgroup/cpuset/tasks
# 移除"system" cpuset
rmdir /sys/fs/cgroup/cpuset/system

# 将"qemu-virt" cpuset中的所有PID复制到"root" cpuset中
cat /sys/fs/cgroup/cpuset/qemu-virt/tasks | xargs -n1 -i echo {} > /sys/fs/cgroup/cpuset/tasks
# 从"qemu-virt" cpuset中移除PID
echo > /sys/fs/cgroup/cpuset/tasks
# 移除"qemu-virt" cpuset
rmdir /sys/fs/cgroup/cpuset/qemu-virt

# 将"qemu-vcpu" cpuset中的所有PID复制到"root" cpuset中
cat /sys/fs/cgroup/cpuset/qemu-vcpu/tasks | xargs -n1 -i echo {} > /sys/fs/cgroup/cpuset/tasks
# 从"qemu-vcpu" cpuset中移除PID
echo > /sys/fs/cgroup/cpuset/tasks
# 移除"qemu-vcpu" cpuset
rmdir /sys/fs/cgroup/cpuset/qemu-vcpu

CPU亲和性

在创建高性能、低延迟虚拟机时,考虑系统架构非常重要。优化虚拟机使用的CPU线程与宿主机系统架构的关系可以带来显著改进。确保虚拟机被分配与宿主机同一节点或同一计算模块中的CPU线程可以显著减少延迟峰值并提升整体性能。

一个完美的例子是AMD FX系列CPU,针对特定CPU架构优化虚拟机可以带来显著的性能提升。它们具有独特的架构,这也是它们的主要缺点之一。AMD FX 8xxx/9xxx系列处理器由4个计算模块组成,每个模块有2个整数核心,它们拥有自己的一级(L1)缓存。虽然每个整数核心都有自己的L1缓存,但它们必须共享每个周期的获取和解码阶段、单个L2缓存模块和单个浮点单元(FPU)。这些共享资源意味着在某些情况下,一个整数核心可能必须等待另一个。通过将虚拟机的CPU亲和性设置为交错排列,每个vCPU将位于宿主机CPU的不同计算模块中。这意味着每个vCPU都可以访问宿主机CPU上自己的L2缓存和FPU,使虚拟机能够访问的L2缓存和FPU数量是顺序CPU亲和性的两倍。

以下是使用5 GHz AMD FX 8350 CPU运行Debian 9.1的宿主机系统上的4核虚拟机的测试结果。客户机是禁用了网络的Windows 10虚拟机,以便Windows更新不会影响测试结果。您可以看到,使用核心(0,2,4,6)而不是(0,1,2,3)可以获得13.5%的改进。

使用"taskset"设置CPU亲和性

这种方法不需要对QEMU本身进行任何修改,如果您想从发行版的软件仓库获取QEMU更新,而不是每次决定更新时都从源代码修补和重新编译QEMU,这可能很好。这是一个简单的shell脚本,它使用debug-threads QEMU参数和taskset来查找vCPU线程并将它们绑定到脚本中其他地方设置的亲和性变量。

代码语言:shell
复制
#!/bin/bash

# 清除选项
OPTS=""

# 设置虚拟机名称
NAME="PARASITE"

# 宿主机亲和性列表
THREAD_LIST="8,9,10,11,12,13,14,15,24,25,26,27,28,29,30,31"

# qemu选项
OPTS="$OPTS -name $NAME,debug-threads=on"
OPTS="$OPTS -enable-kvm"
OPTS="$OPTS -cpu host"
OPTS="$OPTS -smp 16,cores=8,sockets=1,threads=2"
OPTS="$OPTS -m 32G"
OPTS="$OPTS -drive if=virtio,format=raw,aio=threads,file=/vms/disk-images/windows-10.img"

function run-vm {
# 指定在哪个宿主机线程上运行QEMU父进程和工作进程
taskset -c 0-7,16-32 qemu-system-x86_64 $OPTS
}

function set-affinity {
# 休眠20秒,等待QEMU虚拟机启动并创建vCPU线程
sleep 20 &&
HOST_THREAD=0
# 对于每个vCPU线程PID
for PID in $(pstree -pa $(pstree -pa $(pidof qemu-system-x86_64) | grep $NAME | awk -F',' '{print $2}' | awk '{print $1}') | grep CPU |  pstree -pa $(pstree -pa $(pidof qemu-system-x86_64) | grep $NAME | cut -d',' -f2 | cut -d' ' -f1) | grep CPU | sort | awk -F',' '{print $2}')
do
    let HOST_THREAD+=1
    # 将每个vCPU线程PID设置为THREAD_LIST中的下一个宿主机CPU线程
    echo "taskset -pc $(echo $THREAD_LIST | cut -d',' -f$HOST_THREAD) $PID" | bash
done
}

set-affinity &
run-vm

内存调优

内存或RAM可能对虚拟机性能产生很大影响,特别是对于某些对内存速度和延迟敏感的系统架构。为虚拟机预分配内存并增加内存页大小将有助于减少内存访问延迟并提高整体虚拟机CPU性能。

预分配内存

预分配内存是提高虚拟机性能的最简单方法之一。这个设置正如其名称所示,它专门分配一块内存区域,其大小为客机虚拟机的全部内存,这样当客机请求内存时,宿主机就不必分配新的内存块。然而,这将使分配给虚拟机的所有内存在客机停止之前都无法被宿主机使用。

可以使用一个QEMU参数启用预分配内存:-mem-prealloc

大页内存

页是系统内存的一块(通常为2或4KB大小),CPU分配和索引它以便在需要时可以再次访问。由于操作系统和CPU需要保留此索引,记录每个页在物理内存中的位置,因此拥有许多小页会增加索引中的总条目数,从而增加查找每个条目所需的时间。大页内存用于增加大小并减少总页数,这减少了在内存中查找页所需的时间。

为您的QEMU虚拟机启用和使用大页内存有几个步骤。首先确定宿主机CPU支持的页大小。通过运行以下脚本来完成:

代码语言:shell
复制
if [ "$(cat /proc/cpuinfo | grep -oh pse | uniq)" = "pse" ]
then echo "2048K = OK"
else echo "2048K = NO"
fi
if [ "$(cat /proc/cpuinfo | grep -oh pdpe1gb | uniq)" = "pdpe1gb" ]
then echo "1G = OK"
else echo "1G = NO"
fi

建议使用支持的最大大页大小以获得最佳性能。编辑/etc/default/grub并添加hugepagesz=大小和default_hugepagesz=大小内核参数。这些参数设置每个大页的大小。

代码语言:shell
复制
# 如果更改此文件,请之后运行'update-grub'来更新
# /boot/grub/grub.cfg。
# 有关此文件中选项的完整文档,请参阅:
#   info -f grub -n 'Simple configuration'

GRUB_DEFAULT=0
GRUB_TIMEOUT=0
GRUB_RECORDFAIL_TIMEOUT=$GRUB_TIMEOUT
GRUB_DISTRIBUTOR=`lsb_release -i -s 2> /dev/null || echo Debian`
GRUB_CMDLINE_LINUX=""
GRUB_CMDLINE_LINUX_DEFAULT="quiet hugepagesz=1G default_hugepagesz=1G"

保存并关闭文件,然后运行update-grub来更新grub。需要重启才能使更改生效。

可以通过echo数字到文件来在运行时动态分配大页内存。要为所有节点分配大页内存,将大页数echo到/sys/kernel/mm/hugepages/hugepages-大小/nr_hugepages。要为特定节点分配大页内存,将大页数echo到/sys/devices/system/node/node编号/hugepages/hugepages-大小/nr_hugepages。

代码语言:shell
复制
# 为整个系统内存池分配大页内存
echo 16 > /sys/kernel/mm/hugepages/hugepages-1048576kB/nr_hugepages

# 为特定节点分配大页内存
echo 16 > /sys/devices/system/node/node1/hugepages/hugepages-1048576kB/nr_hugepages

通过echo 0到同一文件来移除大页内存。

在启动QEMU时使用-mem-path /dev/hugepages参数来启用大页内存的使用。

Linux进程调度器调优

有几种不同的方法可以调整Linux进程调度器以减少延迟峰值并提高整体CPU性能。大多数这些都需要使用实时内核或编译自定义内核来启用这些功能。

SCHED_FIFO实时进程优先级

SCHED_FIFO(先进先出)策略是一种实时策略,优先级范围为1-99,99为最高。使用此策略的进程将无限期运行或直到完成,并且只能被具有相同或更高优先级的进程中断。

使用chrt将QEMU设置为SCHED_FIFO策略

代码语言:shell
复制
# 注意:
# "$NAME"是我们设置的一个变量,用于在宿主机系统上区分不同的虚拟机。它应该与qemu参数中的"-name"相同
# 有必要等待QEMU完成操作系统启动后再更改为实时进程优先级,否则会停止虚拟磁盘访问

# 获取QEMU虚拟机的父PID
PARENT_PID=$(pstree -pa $(pidof qemu-system-x86_64) | grep $NAME | cut -d','  -f2 | cut -d' ' -f1)
# 将父PID的所有线程设置为SCHED_FIFO 99优先级
pstree -pa $PARENT_PID | cut -d','  -f2 | cut -d' ' -f1 | xargs -L1 echo "chrt -f -p 99" | bash

sched_rt_runtime_us

sched_rt_runtime_us是一个内核参数,用于管理实时任务可以消耗的CPU时间(以微秒为单位)。默认值950000仅允许实时任务使用95%的CPU时间,增加此值或完全禁用它可以改善延迟并增加可用于实时任务的CPU时间。然而,增加此值或禁用它也可能对系统产生负面影响。如果实时任务没有隔离到它们自己专用的CPU线程,它们将被允许使用所有可用的CPU时间并阻止其他(有时是关键)进程运行,并可能无限期地锁定宿主机系统。只有在您了解风险并能够正确配置系统以解决这些问题时,才更改此参数。

使用sysctl命令在运行时设置此参数。

代码语言:shell
复制
# 设置为-1以禁用
sysctl kernel.sched_rt_runtime_us=-1
# 或增加值
sysctl kernel.sched_rt_runtime_us=980000

Linux内核调优

如果发行版软件仓库中的实时内核对您的应用不够好,可能需要配置和编译自定义Linux内核以实现低延迟虚拟化。

CONFIG_PREEMPT_RT实时内核补丁

CONFIG_PREEMPT_RT Linux内核补丁允许系统上具有实时进程优先级的进程抢占大部分内核。这意味着实时进程首先执行,不必等待系统上运行的大多数进程。

尝试从发行版的软件仓库下载最新的实时内核及其源代码。如果您的发行版不提供实时内核或需要更新版本,则需要修补vanilla内核。

首先下载Linux内核源代码和匹配的实时内核补丁,本例中为linux-4.16.18。

cd到包含Linux内核源代码和实时补丁存档的目录并解压它们。

代码语言:shell
复制
Downloads/4.16.18/
gunzip linux-4.16.18.tar.gz
tar xvf linux-4.16.18.tar

gunzip patch-4.16.18-rt12.patch.gz

将补丁移动到Linux内核源代码目录,然后cd到该目录。

代码语言:shell
复制
Downloads/4.16.18/
mv patch-4.16.18-rt12.patch linux-4.16.18/
cd linux-4.16.18

使用patch命令应用补丁。

代码语言:shell
复制
Downloads/4.16.18/linux-4.16.18/
patch -p1 < patch-4.16.18-rt12.patch

将现有内核的配置复制到内核源代码目录。

代码语言:shell
复制
Downloads/4.16.18/linux-4.16.18/
cp /boot/config-\`uname -r\` .config

使用菜单配置实用程序启用完全可抢占内核(RT)抢占模型,然后保存并退出。如果系统在使用完全可抢占内核选项时不稳定,请尝试每个可抢占内核选项。

代码语言:shell
复制
Downloads/4.16.18/linux-4.16.18/
make menuconfig

这些选项可以在以下菜单树中找到

Processor type and features > Preemption Model

Use the next three commands to compile and build a deb package.

代码语言:shell
复制
Downloads/4.16.18/linux-4.16.18/
make -j`nproc`
make modules -j`nproc`
make -j`nproc` bindeb-pkg

Finally install the packages using dpkg, they will be located up one directory.

代码语言:shell
复制
Downloads/4.16.18/linux-4.16.18/
dpkg -i \
../linux-headers-4.16.18-rt12_4.16.18-rt12-1_amd64.deb \
../linux-image-4.16.18-rt12_4.16.18-rt12-1_amd64.deb \
../linux-libc-dev_4.16.18-rt12-1_amd64.deb

Timer Frequency

将中断定时器频率提高到 1000Hz 可以改善系统的整体延迟和响应速度,但缺点是会略微降低吞吐量并增加功耗。要实现这一点,还需要更改内核配置并重新编译内核。

此选项可在以下内核配置菜单树中找到:

Processor type and features > Timer Frequency

References

hugetlbpage.txt

https://www.kernel.org/doc/Documentation/vm/hugetlbpage.txt

The Kernel Development Community. The kernel’s command-line parameters.

https://www.kernel.org/doc/html/v4.14/admin-guide/kernel-parameters.html

Robert M. Love. (2014). taskset - set or retrieve a process's CPU affinity.

https://manpages.debian.org/testing/util-linux/taskset.1.en.html

Robert M. Love & Karel Zak. (2016). chrt - manipulate the real-time attributes of a process.

https://manpages.debian.org/testing/util-linux/chrt.1.en.html

SUSE. (2018). Tuning the Task Scheduler.

https://doc.opensuse.org/documentation/leap/tuning/html/book.sle.tuning/cha.tuning.taskscheduler.html

Alex Tsariounov. (2011). cset-shield - cpuset supercommand which implements cpu shielding.

https://manpages.debian.org/jessie/cpuset/cset-shield.1.en.html

本文系外文翻译,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文系外文翻译前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 原文链接
    • 术语
  • CPU隔离
    • 使用"isolcpus"将QEMU线程与宿主机进程隔离
    • 使用"cset shield"动态将QEMU线程与宿主机进程隔离
    • 使用"cpuset/cgroups"动态将QEMU线程与宿主机进程隔离
  • CPU亲和性
    • 使用"taskset"设置CPU亲和性
  • 内存调优
    • 预分配内存
    • 大页内存
  • Linux进程调度器调优
    • SCHED_FIFO实时进程优先级
    • sched_rt_runtime_us
  • Linux内核调优
    • CONFIG_PREEMPT_RT实时内核补丁
    • Timer Frequency
  • References
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档