在双路服务器上,线程固定到一个 CPU 后反而变慢,原因可能不是算力,而是数据仍留在另一个 NUMA 节点。NUMA 把处理器和内存组织成多个节点。CPU 访问本节点内存通常路径更短;跨节点访问需要经过处理器互连,还会占用远端内存控制器和链路带宽。
NUMA 优化的核心不是简单地“绑核”,而是同时回答两个问题:线程在哪里运行,物理页在哪里分配。
物理页何时决定归属
malloc() 成功往往只建立虚拟地址区间。匿名页通常在第一次实际写入时才通过缺页分配物理页。若没有显式内存策略,这个页倾向于落在执行首次触碰线程所在的节点,这就是常说的 first touch。
一个常见错误是主线程先把大数组全部清零,然后才启动分散在各节点的工作线程。所有页面可能先落到主线程所在节点,其他线程之后一直远程访问。并行初始化能让每个工作线程首次触碰自己负责的区间,但前提是后续计算仍由相近的 CPU 处理这些数据。
先看拓扑,再决定策略
1 | numactl --hardware |
numactl --hardware 给出节点 CPU、容量和距离矩阵。numastat -p 适合观察进程内存分布,numa_maps 能进一步显示 VMA 的策略与页位置。字段应结合内核版本解释,不能把 numastat 中任意一个累计值直接当作延迟。
做对照实验时,可以分别尝试:
1 | # CPU 与内存都限制在 node 0 |
--membind 比 --preferred 更严格,目标节点内存不足时可能让分配失败,因此不应未经容量和回退测试直接用于线上服务。--interleave 能分散带宽压力,但会让部分访问必然跨节点;它适合流式大数据,不一定适合共享热表或低延迟控制线程。
自动 NUMA balancing 在做什么
开启自动 NUMA balancing 后,内核会通过访问采样判断页与任务是否长期分离,再选择迁移页面或任务。它能修正部分首次放置错误,但采样、保护变化和迁移本身也有成本。工作集稳定、内存充足的长期服务可能受益;严格绑核、实时线程或访问模式快速变化的程序则需要单独测量。
1 | sysctl kernel.numa_balancing |
perf 事件是否存在、名称如何解释都依赖处理器 PMU。先用 perf list 确认,不要把不支持事件得到的空值当成零。
常见误判
- 线程绑到 node 0 不代表它访问的页也在 node 0。
- 远端访问增加不一定说明策略错了,共享数据本来就可能被多个节点读取。
- 平均 CPU 利用率正常不能排除互连或单个内存控制器已经饱和。
- 盲目迁移大页可能引入长尾停顿,收益要看数据复用周期。
- 容器的 cpuset CPU 与 memory 节点限制不一致,会制造隐蔽的远程访问。
证据边界
“本地更快”是一般规律,不是固定倍率。节点距离、互连、内存频率、缓存命中、页面大小和工作集都会改变结果。首次触碰也会被显式 mempolicy、共享页、页迁移和容量回退覆盖。结论应来自目标机器上的吞吐、尾延迟、内存带宽与页分布对照。
参考:NUMA Memory Policy · numactl(8) · numastat(8) · 不懂 NUMA,别再说你懂 Linux 内核了