Chat with us, powered by LiveChat
Varidata 新闻资讯
知识库 | 问答 | 最新技术 | IDC 行业新闻
Varidata 官方博客

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

发布日期:2026-08-03
NUMA内存访问延迟优化示意图

NUMA 内存访问延迟,指的是处理器从 DRAM 读取数据所消耗的时间。本地插槽的内存请求通常可在 60 到 100 纳秒内完成;一旦需要跨越 UPI 或 QPI 互连访问远程插槽,延迟则会升高到 200 或 300 纳秒。这种跨插槽访问会给工作负载带来最高可达 3 倍的延迟惩罚。

要优化内存访问延迟,核心在于最大化内存本地性,并消除远程插槽流量。通过强制 CPU 线程绑定、控制页分配以及实施拓扑感知的内核调优,你可以确保系统性能达到峰值。

拓扑发现与延迟分析

映射本地性距离

在进行内存访问调优之前,必须先梳理系统的硬件拓扑。ACPI 的系统本地性距离信息表(SLIT)提供了 NUMA 节点之间相对访问成本的比率。Linux 内核会读取该矩阵,并以无量纲整数的形式显示距离值。通常,本地访问基准值为 10;如果某个远程节点的距离值为 21,则表示访问该远程插槽上的内存,其请求延迟大约是本地访问的两倍。

你可以运行 numactl 查看这些相对本地性距离:

numactl --hardware

该输出会显示节点清单以及相对距离矩阵。

使用 Perf 分析访问延迟

你可以借助标准 Linux 性能分析工具,测量实时内存流量。运行 numastat,可比较系统各节点上的内存分配计数器。如果 numa_missother_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 映射插槽间的相对距离。

  • 通过 tasksetnumactl 绑定,强制实施严格的 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 设备的本地插槽上。

您的免费试用从这里开始!
联系我们的团队申请物理服务器服务!
注册成为会员,尊享专属礼遇!
您的免费试用从这里开始!
联系我们的团队申请物理服务器服务!
注册成为会员,尊享专属礼遇!
Telegram Teams