virt-report虚拟化社区动态

vCPU 与 NUMA 内存放置

本文由 OpenAI Codex (GPT-6-Astra) 辅助编写。

一、vCPU 放置与内存访问局部性

一台双路服务器上,虚拟机的 4 个 vCPU 都在 NUMA 节点 1 的物理 CPU 上运行,客户机 RAM 却大部分分配在节点 0。每次访问这些页,都可能跨越处理器互连。绑定线程解决了“在哪执行”,内存位置仍需单独安排。

NUMA 的含义是不同 CPU 访问不同内存位置时,延迟和可用带宽可能不同。虚拟机又增加了客户机拓扑:客户机认为自己的 CPU 和内存属于哪个节点,与真实页位于哪个宿主节点,是两套独立关系。本文沿“发现远端页 → 分别配置线程与 RAM → 核对实际放置 → 比较业务结果”展开,使用同一台 4 vCPU、8 GiB RAM 的 VM。

源码版本:QEMU 10.2.0、Linux 6.18、Libvirt 12.0.0。

二、把虚拟拓扑与物理放置分开

配置对象配置作用仍需单独安排的事项
客户机 NUMA 拓扑客户机看到哪些 vCPU 与 RAM 属于同一节点、节点之间的距离宿主线程与物理页实际位于对应节点
vCPU 线程亲和性宿主调度器允许该线程在哪些逻辑 CPU 上运行独占物理核、稳定调度时间、内存自动迁移
RAM 后端内存策略该内存对象的页优先或限定从哪些宿主节点分配客户机应用将数据分给恰当的工作线程
IOThread 与其他后端线程处理 I/O 的 CPU 位置及其与队列、内存、设备的关系后端线程自身的亲和性,与 vCPU 亲和性分别配置

客户机节点编号不必等于宿主节点编号。客户机节点 0 完全可以放到宿主节点 1;迁移到另一台机器后,也可以保持客户机拓扑而采用新的物理映射。vCPU 的 socket/core/thread 拓扑同样是客户机看到的组织方式,不能当作宿主绑核配置。

一个客户机节点对应三条放置关系
客户机节点 0 的 vCPU、RAM 和处理其请求的后端需要分别安排。示例将 vCPU 与 RAM 放在宿主节点 1;远端后端或误放的物理页仍可能引入跨节点访问。
客户机节点 0 的 vCPU、RAM 和处理其请求的后端需要分别安排。示例将 vCPU 与 RAM 放在宿主节点 1;远端后端或误放的物理页仍可能引入跨节点访问。 打开原图
Mermaid 源码
flowchart TB
  accTitle: 客户机 NUMA 与宿主线程、内存及后端的放置
  accDescr: 客户机节点 0 的 vCPU 线程可以绑定到宿主节点 1,RAM 后端通过独立内存策略分配到该节点。后端线程必须另行安排;如果位于节点 0,访问共享缓冲区仍会跨节点。
  G["客户机节点 0<br/>4 vCPU · 8 GiB RAM"] --> V["vCPU 线程<br/>CPU 亲和性"]
  G --> M["RAM 后端<br/>host-nodes 与 policy"]
  V --> P["宿主节点 1 的 CPU<br/>执行客户机"]
  M --> R["宿主节点 1 的物理页<br/>数据与队列缓冲区"]
  P <-->|局部访问| R
  B["I/O 后端线程<br/>独立配置亲和性"] --> L["放在宿主节点 1"]
  B --> X["放在宿主节点 0"]
  L -->|局部访问| R
  X -.->|跨节点访问| R
  classDef guest fill:#e4f1ec,stroke:#438575,color:#173e34;
  classDef host fill:#eaf0f6,stroke:#8198ac,color:#263c4c;
  classDef remote fill:#fff4db,stroke:#b89a55,color:#59491e;
  class G,V,M guest;
  class P,R,B,L host;
  class X remote;

三、4 个 vCPU 与 8 GiB RAM 的放置例子

假设宿主节点 1 包含逻辑 CPU 8—15,有足够空闲内存;这些编号只是例子,实际应先在宿主机执行 lscpu -e=CPU,NODE,SOCKET,CORE 和 numactl --hardware 确认。客户机只暴露一个节点,包含 4 个 vCPU 和 8 GiB RAM。在客户机内运行这些命令,得到的是虚拟拓扑,无法据此确定宿主物理放置。

QEMU 内存与虚拟拓扑部分可以表示为:

-m 8G -smp 4,sockets=1,cores=4,threads=1
-object memory-backend-ram,id=ram0,size=8G,host-nodes=1,policy=bind,prealloc=on
-numa node,nodeid=0,cpus=0-3,memdev=ram0

nodeid=0 是客户机节点,host-nodes=1 是宿主节点。这段配置将 RAM 约束到宿主节点 1,但尚未设置 vCPU 亲和性。由 Libvirt 管理时,可以在既有 domain XML 中同时表达客户机 CPU/NUMA 拓扑、vcpupin 和 numatune,例如以下相关片段:

<cputune>
  <vcpupin vcpu='0' cpuset='8'/>
  <vcpupin vcpu='1' cpuset='9'/>
  <vcpupin vcpu='2' cpuset='10'/>
  <vcpupin vcpu='3' cpuset='11'/>
  <emulatorpin cpuset='12-13'/>
</cputune>
<numatune>
  <memory mode='strict' nodeset='1'/>
  <memnode cellid='0' mode='strict' nodeset='1'/>
</numatune>

这里假设客户机节点 0 已在 CPU 的 numa 配置中定义。若使用 IOThread,还要为已创建的线程配置 iothreadpin;若使用独立 vhost-user 后端,则在那个进程及其工作线程上配置。CPU 8—11 是否属于不同物理核、是否与其他任务争抢,仍需检查,四个逻辑 CPU 不保证有四份独立核心资源。

这是单节点内存密集型负载的放置例子。超过单节点容量的大 VM,可能更适合暴露多个客户机节点,让每组 vCPU 靠近自己的 RAM;此时应用也要理解 NUMA,避免把全部数据集中到一个节点后由其他节点反复远程访问。

四、从内存映射到物理页分配

QEMU 创建 RAM 后端时,先获得一段宿主虚拟地址映射。使用按需分配的匿名内存时,许多物理页在后续缺页时才真正分配。分配位置取决于适用的 NUMA 内存策略、允许的节点集合以及当时的资源状况;默认局部分配下,触发分配的线程所在节点会影响结果。

这解释了为什么“启动完成后再绑核”可能晚了一步:预分配线程或启动过程中运行的线程已经让页落在别的节点。单纯调整线程亲和性不会把这些页一起搬走。共享内存、文件后端和大页还有各自的分配约束,也不宜用一个 first touch 规则概括所有情况。

HostMemoryBackend 保存 RAM 区域、host_nodes、policy 和 prealloc 等属性。QEMU 的 Linux 路径先通过 mbind() 为后端应用节点策略,再执行预分配,避免预分配先发生而策略尚未生效。内存策略与预分配的顺序

常见策略适用思路取舍
bind / Libvirt strict把分配约束在指定节点集合内,维持明确的局部性节点内存不足时可能分配失败,不能假定其他节点有空闲就一定能启动
preferred优先使用指定节点,必要时允许回退更容易满足容量,但运行时页分布可能偏离理想位置
interleave将页分散到一组节点,利用多节点带宽和容量单个 CPU 访问时包含远端页,不等于最低访问延迟

cpuset/cgroup 的允许节点约束也参与决定最终放置。预分配能在启动阶段更早暴露容量问题并减少运行时首次缺页,代价是更长启动时间和更早占用内存;大页还需要检查目标节点的大页池,而非只检查全机空闲内存。

五、I/O 路径也要计入局部性

一次 virtio 请求可能先由 vCPU 准备描述符和缓冲区,再由 QEMU IOThread、内核 vhost 或独立用户态后端读取,最后交给网卡或存储设备。共享缓冲区没有变,读取它的 CPU 却可能跨了节点。后端线程放得很远时,绑好 vCPU 和 RAM 仍会留下远端访问及缓存交接成本。

物理设备也有所在节点。把后端靠近网卡可以缩短设备侧路径,但若客户机 RAM 在另一节点,缓冲区访问又会跨节点。合理选择取决于数据量、批大小、共享方式和队列分配;不能用“所有线程都绑到同一个 CPU”解决局部性,否则可能增加执行排队。

多队列可以把请求分配给多组工作线程,使处理并行化,也可能提高跨节点共享和通知数量。评价时要把每个队列的消费者、缓冲区位置和设备位置对应起来,再决定是否拆分队列或调整线程亲和性。

六、验证配置是否变成了实际放置

以下命令在运行虚拟机的宿主机上执行,只读取状态。DOMAIN 替换为 Libvirt 域名,PID 替换为该虚拟机的 QEMU 进程号;访问进程映射和 Libvirt 可能需要相应权限。前三项读取放置策略,后三项检查内存分布与线程运行位置。

virsh vcpupin DOMAIN
virsh emulatorpin DOMAIN
virsh numatune DOMAIN
numastat -p PID
less /proc/PID/numa_maps
ps -L -p PID -o pid,tid,psr,comm

psr 是采样时看到的处理器,不是允许 CPU 集合;线程亲和性还应结合 /proc/PID/task/TID/status 的 Cpus_allowed_list。numastat -p 汇总进程内存,包含 QEMU 自身开销,不能把所有页都直接算成客户机 RAM;numa_maps 可进一步按映射区间核对。

对前面的例子,预期应同时看到三项结果:四个 vCPU 的允许 CPU 集合符合配置;RAM 对应映射的物理页主要位于节点 1;需要绑定的后端线程也在预期位置。若只满足第一项,就仍可能重复开头的远端访问。三项都满足后,才进入相同负载下的延迟和吞吐比较。

先固定业务负载,再比较放置调整前后的吞吐、延迟分布、CPU 消耗和内存节点分布。节点间缓存共享、带宽竞争与调度排队可能同时存在,不能仅凭远端页数推导业务会快多少。迁移后应重新验证目的端放置;客户机拓扑保持不变,不代表目的端自动沿用正确的物理资源映射。

七、从默认分配到按内存对象管理

默认调度与局部分配无需细致配置,适合资源动态共享;当虚拟机跨多个节点、使用大页或接入高吞吐设备后,默认策略可能难以保持稳定局部性。显式 NUMA 拓扑和每节点内存后端把客户机视图与宿主分配策略分开,使控制更精确,也增加了容量规划和迁移时重建映射的责任。

Linux 的自动 NUMA 平衡可以根据访问情况调整部分工作和页的位置,适用效果受策略、页类型和运行环境影响;严格绑定、大页或固定高吞吐后端不能期待它自动修正全部放置。进一步出现的异构内存描述也让“节点距离”超出了单纯双路服务器:延迟、带宽和容量可能不同,需要让客户机了解适合其负载的信息。

回到这台 4 vCPU、8 GiB VM:将线程和 RAM 放在节点 1,减少的是它们之间的远端访问;若主要流量来自节点 0 的网卡,还要权衡设备侧的数据路径。容量充足的单节点放置、多节点分区以及跨节点交错,分别适合不同的数据访问方式,最终以相同业务负载下的结果选择。

八、参考资料