如何优化 NUMA 架构中的内存访问延迟

NUMA 内存访问延迟,指的是处理器从 DRAM 读取数据所消耗的时间。本地插槽的内存请求通常可在 60 到 100 纳秒内完成;一旦需要跨越 UPI 或 QPI 互连访问远程插槽,延迟则会升高到 200 或 300 纳秒。这种跨插槽访问会给工作负载带来最高可达 3 倍的延迟惩罚。
要优化内存访问延迟,核心在于最大化内存本地性,并消除远程插槽流量。通过强制 CPU 线程绑定、控制页分配以及实施拓扑感知的内核调优,你可以确保系统性能达到峰值。
拓扑发现与延迟分析
映射本地性距离
在进行内存访问调优之前,必须先梳理系统的硬件拓扑。ACPI 的系统本地性距离信息表(SLIT)提供了 NUMA 节点之间相对访问成本的比率。Linux 内核会读取该矩阵,并以无量纲整数的形式显示距离值。通常,本地访问基准值为 10;如果某个远程节点的距离值为 21,则表示访问该远程插槽上的内存,其请求延迟大约是本地访问的两倍。
你可以运行 numactl 查看这些相对本地性距离:
numactl --hardware该输出会显示节点清单以及相对距离矩阵。
使用 Perf 分析访问延迟
你可以借助标准 Linux 性能分析工具,测量实时内存流量。运行 numastat,可比较系统各节点上的内存分配计数器。如果 numa_miss 或 other_node 行中的数值较高,就说明远程访问惩罚频繁发生。
若需进行更精确的硬件采样,可使用带内存访问计数器的 perf stat:
perf stat -e numa:numa_hit,numa:numa_miss ./your_application该命令会在程序运行期间,对本地节点命中与远程节点未命中进行性能分析。
评估硬件互连流量
大量跨插槽流量会使 Intel UPI 或 AMD Infinity Fabric 等硬件互连链路趋于饱和。你可以使用 perf c2c(cache-to-cache)分析,评估插槽间带宽与延迟瓶颈。该工具能够定位伪共享问题,并跟踪跨插槽的远程缓存行命中情况。
提示: 监控
numastat中的numa_foreign指标,可帮助你发现进程是否正在从其所分配本地插槽域之外获取数据。
线程绑定与内存本地性
操作系统调度器通常会为了负载均衡,频繁地在不同 CPU 核心之间迁移活跃线程。在 NUMA 系统中,这种默认行为会破坏内存本地性。当线程迁移到远程插槽上的核心后,它必须通过互连链路访问原先节点上的数据,从而引入额外延迟。要消除这一惩罚,你需要将线程与内存分配都严格绑定到本地硬件资源。
通过 Taskset 绑定线程
默认的 Linux 调度器优先考虑整体系统吞吐量,而非线程级内存本地性。你可以使用 taskset 工具直接控制 CPU 亲和性,从而覆盖这种默认行为。
当你把执行线程绑定到特定 CPU 核心时,就能保持核心的 L1、L2 和 L3 缓存处于“热”状态。这种做法可防止操作系统将进程迁移到其他插槽。
执行以下命令,可将一个进程绑定到 NUMA 节点 0 的前 8 个核心:
taskset -c 0-7 ./your_application你也可以通过进程 ID(PID),将一个已经在运行的进程绑定到指定核心:
taskset -p -c 0-7 12345线程绑定能够确保指令在靠近本地缓存行的位置执行。不过,taskset 仅控制 CPU 执行亲和性。若要真正优化跨节点内存访问模式,你还必须管理物理内存分配。
使用 Numactl 限制分配
仅仅绑定线程,并不能保证应用程序一定会在本地节点分配内存。一个运行在 Socket 0 上的进程,在默认策略下,仍有可能把 RAM 分配到 Socket 1。因此,你必须使用 numactl 同时强制执行“运行位置”和“分配位置”两类规则。
运行工作负载时,可显式指定 CPU 与内存节点约束:
numactl --cpunodebind=0 --membind=0 ./your_application--membind 策略是一种严格的分配约束,它会把分配操作限制在指定 NUMA 节点上。如果这些节点无法提供足够内存,分配会立即失败,而不会回退到其他未指定节点。
如果你的应用需要更高可用性,严格分配失败可能会影响业务连续性。此时可使用 --preferred 作为更灵活的策略:
numactl --cpunodebind=0 --preferred=0 ./your_application--preferred 参数会指示内核优先在节点 0 上分配 RAM;只有当节点 0 的物理内存不足时,内核才会回退到远程节点。
如何通过迁移优化内存访问
在高峰负载期间,工作负载常常会超出单一插槽的资源上限。当应用跨越多个 NUMA 域运行时,初始的静态绑定就不再足够。此时,你必须实施动态内存页迁移策略,才能维持峰值性能。
提示: 在运行环境发生变化时,可以使用
migratepages工具,在不中断生产服务的情况下,将完整内存占用迁移到其他节点。
如果某个进程的主要计算负载转移到了节点 1,你可以手动将其现有页从节点 0 迁移到节点 1:
migratepages 12345 0 1该命令会扫描进程 ID 为 12345 的进程,找出位于节点 0 上的物理页,并将其实时迁移到节点 1。
你也可以依赖自动化内核机制,或借助自定义应用逻辑来完成迁移:
内核 AutoNUMA 迁移: Linux 内核会持续扫描活跃进程的内存页,识别远程访问,并自动将物理页移动到更靠近访问线程的位置。
显式用户态迁移: C/C++ 应用可以调用
move_pages()系统调用,根据运行时指标,直接迁移特定内存地址。
动态页迁移会在迁移阶段带来即时处理开销。但对于长时间运行的进程而言,把活跃数据集迁移到本地插槽内存,收益往往立竿见影。你可以显著减少跨插槽 UPI 流量,并持续优化访问延迟。
内核分配策略与页面调优
Linux 通过系统调用提供了多种内存策略,帮助你控制页在 NUMA 节点之间的放置方式。你可以根据应用行为配置这些内核机制,以优化内存访问性能。
本地分配与交错分配
Linux 内核支持多种策略标志,用于指定内存节点选择方式:
MPOL_BIND:严格将分配限制在指定节点集合内。MPOL_PREFERRED:优先选择单一节点;当首选 NUMA 节点内存耗尽时,策略会自动回退到其他可用 NUMA 节点,而不是返回错误。MPOL_INTERLEAVE:以轮询方式,将分配均匀分布到多个节点上。
尽管 MPOL_BIND 能提供较低的本地延迟,但在某些内存访问模式下,MPOL_INTERLEAVE 的表现反而更优:
大内存占用: 分配区域较大,通常为 1 MB 或以上。
均匀访问模式: 对已分配内存区域的访问请求分布较为均衡。
顺序或流式访问: 访问模式旨在通过将页面分配和并发访问分散到多个 NUMA 节点,而非限制在单一节点,以最大化内存带宽。
管理透明大页
透明大页(Transparent Huge Pages,THP)通过分配 2 MB 或 1 GB 的内存页,而不是标准的 4 KB 页面,来减少转换后备缓冲区(TLB)未命中。然而,对于实时工作负载,当内核需要在远程插槽间分配或拆分这些大页时,常常会引发延迟尖峰。因此,建议你将 THP 设置为 madvise 模式,以便由应用程序显式控制是否使用大页。
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled精细调整 AutoNUMA 平衡
AutoNUMA 会周期性扫描活跃线程的内存位置,以优化跨插槽的内存访问。内核会把放错位置的页面迁移到更靠近执行线程的节点。
但这种自动页面扫描会引入 CPU 开销,并带来不可预测的延迟抖动。对于实时系统,应该关闭 AutoNUMA 平衡,转而依赖显式的静态内存放置策略:
sysctl -w kernel.numa_balancing=0应用程序与共享内存调优
设置 Pthread 的 NUMA 亲和性
你可以通过编程方式,将 C/C++ 线程绑定到特定 NUMA 节点,从而优化应用的内存访问。标准运行时系统通常依赖外部脚本,而直接在 C 层进行线程绑定,则能提供更精细的硬件控制。
使用 POSIX 线程库并结合 libnuma 开发包,你可以在应用程序内部实现显式亲和性控制:
#include <pthread.h>
#include <numa.h>
void bind_thread_to_node(pthread_t thread, int node_id) {
struct bitmask *cpus = numa_allocate_cpumask();
numa_node_to_cpus(node_id, cpus);
pthread_setaffinity_np(thread, sizeof(cpu_set_t), (cpu_set_t *)cpus->maskp);
numa_free_cpumask(cpus);
}在初始化阶段绑定工作线程,可防止线程迁移带来的性能损失。将线程绑定与 numa_alloc_onnode() 分配结合使用,能够确保本地缓存亲和性。
优化共享内存与进程间通信
通过 POSIX 或 System V 共享内存实现的进程间通信(IPC),同样需要显式放置策略。默认情况下,共享内存段的物理页会分配到“首次写入该内存的进程所在 NUMA 节点”上。当多个工作进程跨插槽读取这些数据时,这种默认行为会造成严重的延迟瓶颈。
你可以通过显式设置 mbind() 策略,来平衡共享数据结构:
#include <numaif.h>
mbind(shared_memory_ptr, size, MPOL_INTERLEAVE, nodemask, maxnode, MPOL_MF_MOVE);该系统调用会将共享缓存行均匀分布到参与处理的各个硬件插槽中。
配置容器运行时亲和性
现代云环境通常通过 Docker 和 Kubernetes 等容器运行时来承载应用。如果不做配置,容器工作负载会在宿主机所有插槽间动态漂移,从而引入较高的互连访问延迟。
提示: 为 Kubernetes Kubelet 设置
--topology-manager-policy=single-numa-node参数,可以为延迟敏感型 Pod 保证可对齐的 CPU 与内存分配。
你也可以通过静态 CPU 集合配置 Docker 容器:
docker run -d --cpuset-cpus="0-7" --cpuset-mems="0" your_image该命令会将容器化进程严格绑定到节点 0 的 CPU 核心和本地 RAM。
要消除跨节点内存延迟,关键在于控制“数据放在哪里”以及“线程跑在哪里”。请按以下技术清单优化系统中的内存访问:
使用
numactl --hardware映射插槽间的相对距离。通过
taskset和numactl绑定,强制实施严格的 CPU 与内存亲和性。选择显式的运行时页分配策略,并调优 AutoNUMA 设置。
提示: 持续使用
perf跟踪硬件性能计数器,有助于你快速发现新的跨节点互连瓶颈。
常见问题
如何判断你的工作负载是否受到 NUMA 延迟影响?
在终端中运行 numastat -c,即可查看系统内存分配计数器。如果 other_node 列的数值较高,说明存在跨插槽内存访问。
numastat -c你也可以运行 perf stat -e numa:numa_miss,在执行期间实时测量远程内存访问事件。
taskset 和 numactl 的主要区别是什么?
taskset 工具只负责将执行线程绑定到特定 CPU 核心;而 numactl 则能更深入地同时控制 CPU 亲和性和物理 RAM 的节点放置。若要在固定执行位置的同时,强制实施本地分配或交错分配策略,应优先使用 numactl。
对于实时应用,是否应该始终禁用 AutoNUMA?
关键结论: 实时系统追求的是可预测性,而不是动态平衡。
是的,对于实时工作负载,应禁用 AutoNUMA。内核的自动页面扫描机制会引入 CPU 开销和不可预测的延迟抖动。采用静态线程绑定并配合显式策略,通常能获得更稳定一致的访问时延。
NUMA 架构是否会影响 PCIe 设备和 NVMe 性能?
PCIe 插槽通常直接连接到特定处理器插槽。当线程访问位于远程插槽上的网卡或 NVMe 设备时,I/O 流量必须跨越插槽间互连链路。要优化设备吞吐量,应将 I/O 密集型线程绑定到承载该 PCIe 设备的本地插槽上。

