NUMA (Non-uniform memory access,非一致性内存访问)架构是近代计算机常见的一种架构。随着 CPU 近年来性能提升速度的逐渐放缓,开始出现通过多个 CPU 共同工作来提升性能的方式,而 NUMA 则是伴随这种方式出现的一种优化方案。
1. 硬件角度
从硬件角度来看,如果按照以前的计算机架构,多个 CPU 之间访问同一块内存,会共用一个 bus:
但是这样一来,bus 就很可能出现性能瓶颈,导致整体性能的降低,因此就出现了 NUMA 架构:
每个 CPU 都拥有自己的一块 “本地” 内存,其他 CPU 的内存则可以看做是 “远程” 内存。这种架构下,CPU 访问自己的本地内存会有更低的延迟以及更高的性能表现。
2. Linux 角度
从 Linux 角度来看,基于这种硬件结构,一个 CPU 会被抽象成一个 NUMA Node, 并且尽可能将 CPU 所需要的内存分配在它的本地内存中。
但是在一个 NUMA Node 的内存即将耗尽的时候,Linux 采取的默认策略是 swap/淘汰 内存页,这样对于大内存应用(比如一个应用内存大于一个 NUMA Node 的所有内存)来说,一旦某个后续需要用到那部分内存页,就会出现明显的性能下降。
3. 解决方案
目前有几个比较主流的方法如下:
- 使用
numactl --interleave=all
指定程序运行时随机分配在多个 NUMA Node 上。 - 设置
vm.zone_reclaim_mode=0
,当本地内存不够时优先去远程内存分配。 - 使用 numad 自动管理 NUMA 内存分配。
numactl --interleave=all
咋看之下好像失去了 NUMA 的优势,但是大部分情况下,一个应用可以分为稳定的线程(系统线程)以及经常变化的线程(用户创建的线程),对于稳定的线程来说,固定在一个 NUMA Node 下可以获得最好的性能,而对于随机分配在不同 CPU 下的用户线程来说,随机分配反而可以产生奇效,借用网上的一个性能测试结果:
从上面的图可以看出,绝大部分场景下的性能表现,NUMA 默认 < 手动绑定 Numa Node < interleave 方式。因此,绝大部分场景下,NUMA 的性能瓶颈不在于远程内存访问,而是在于 NUMA 导致的内存页 swap/淘汰。
numad,是属于提供 NUMA 自动内存管理的守护线程,官方介绍如下:
numad is an automatic NUMA affinity management daemon. It monitors NUMA topology and resource usage within a system in order to dynamically improve NUMA resource allocation and management (and therefore system performance). Depending on system workload, numad can provide up to 50 percent improvements in performance benchmarks. It also provides a pre-placement advice service that can be queried by various job management systems to provide assistance with the initial binding of CPU and memory resources for their processes.
numad 会周期性地地监控 NUMA 的资源使用情况,必要的时候会在 NUMA Node 之间移动进程以求达到最佳性能效果。
numad 主要的受益对象有:
- 消耗大量系统资源且长时间运行的进程
- 同时消耗多个 NUMA Node 资源的进程。
对于那些只运行数分钟,或者只消耗少量资源的进程,numad 并不能带来性能上的提升,此外,那些具有连续性、不可预测的内存访问的系统,比如大内存的数据,也无法从 numad 获得提升。
对于 numad 的使用,可以参照官方文档。
4. 使用方式
我们可以很简单的借助 numad 享受到 numa 机制带来的提升,但 numa 机制也不是适用于所有类型的程序,如果服务器需要运行多种类型的程序(比如只消耗少量资源,只运行短暂时间),那么 numad 往往不能为其带来太大的提升,反而可能会带来调度上的开销。
针对上面这种场景,在不适用 numad 方式下,可以使用 numactl 方式指定程序运行的 numa node,同时各个软件(如 JDK、Yarn)也有对 numa 提供了一定程度上的支持。
4.1 numactl
numactl 可以指定进程在哪个 numa node 节点上运行,同时指定在哪个 numa node 节点上分配内存,格式如下:
numactl -N 0-1 -m 0-1 [命令]
numactl 的参数含义如下:
N 0-1
:指定该进程在 0、1 号 numa node 上运行,也可以单独指定一个 numa nodem 0-1
:指定该进程在 0、1 号 numa node 上分配内存,也可以单独指定一个 numa node
验证过程如下:
# 指定休眠进程在 0 号 numa node 上运行
[root@dn22 ~]# nohup numactl -N 0 -m 0 sleep 60 &
[3] 3811
# 使用 taskset 查看进程所在节点
[root@dn22 ~]# taskset -cp 3811
pid 3811's current affinity list: 0-7,16-23
# 可以看到, 0-7,16-23 核心属于 0 号 numa node
[root@dn22 ~]# numactl -H
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 4 5 6 7 16 17 18 19 20 21 22 23
node 0 size: 130946 MB
node 0 free: 1003 MB
node 1 cpus: 8 9 10 11 12 13 14 15 24 25 26 27 28 29 30 31
node 1 size: 131072 MB
node 1 free: 1234 MB
node distances:
node 0 1
0: 10 21
1: 21 10
4.2 Yarn 方式
Yarn 从 3.1.0 版本开始支持 numa 调度,开启方式如下:
- 设置
yarn.nodemanager.numa-awareness.enabled
为 true - 对
yarn.nodemanager.numa-awareness.read-topology
进行设置,分两种情况:- true:Yarn 会自动通过
numactl --hardware
命令获取机器上的 numa 拓扑结构信息 - false:Yarn 不会自动获取 numa 拓扑结构信息,而是依赖于以下配置获取 numa 拓扑结构信息:
- yarn.nodemanager.numa-awareness.node-ids:指定可用的 numa 节点,即
NODE_ID
,多个 numa 节点使用,
分隔符隔开,如0,1,2
- yarn.nodemanager.numa-awareness.<NODE_ID>.memory:指定各个 numa 节点可用的内存,多个 numa 节点则根据其
NODE_ID
配置多项 - yarn.nodemanager.numa-awareness.<NODE_ID>.cpus:指定各个 numa 节点可用的 cpu 核数,多个 numa 节点则根据其
NODE_ID
配置多项
- yarn.nodemanager.numa-awareness.node-ids:指定可用的 numa 节点,即
- true:Yarn 会自动通过
为了支持该特性,节点上还需要预先安装 numactl 工具。
4.3 JVM 方式
JVM 也提供了对 NUMA 的支持,但对 GC 算法以及 JDK 版本有要求:
- ParallelGC 支持 NUMA(JDK 6)
- ZGC 支持 NUMA(JDK 11)
- G1 GC 在 JDK 14 开始支持 NUMA
在 JVM 启动参数上添加 -XX:+UseNUMA
开启 NUMA 感知,这里简述下 ParallelGC 是如何利用 NUMA 的:
JVM 实现了 NUMA 感知分配内存的 allocator,该 allocator 基于一个假说:对于一个对象来说,创建该对象的线程是最有可能会使用该对象的线程。因此该 allocator 主要针对频繁生成新对象的 eden 区域,优先在线程所在的 numa node 上分配对象,以保证该线程后续能以最快的速度去访问对象,其他区域的内存部分则是通过交织的方式,在各个 numa node 上均衡分配。
其余 GC 算法的 numa 感知实现机制可以自行查阅资料。
5. 测试效果
目前有看到比较好的测试案例:https://indico.cern.ch/event/304944/contributions/1672535/attachments/578723/796898/numa.pdf 。
在自己集群测试中,运行一个 Spark 流式作业,开启 numad 前后对比如下:
- 开启前:1853.0 h (123.0 h),GC 占比: 123 / 1853 ~= 5.47%
- 开启后: 2792.5 h (139.3 h),GC 占比:139.3 / 2792.5 ~= 4.99%
集群 CPU 整体利用率变化如下:
从中可以看出, CPU 峰值 43% 下降到了 37%,CPU 负载也有一定的减轻。
参考: