☰
Apache Ignite NUMA 感知内存分配器(ignite-numa-allocator)实战指南:原理、构建与四种分配策略配置
2026/10/15 6:39:38 网站建设 项目流程
  • 分布式数据库
  • 缓存

【免费下载链接】ignite

Apache Ignite

项目地址:https://gitcode.com/gh_mirrors/ignite4/ignite
点击查看免费下载

本指南以 Apache Ignite 仓库中的 modules/numa-allocator/README.md 为主体,系统讲解 NUMA 感知内存分配器的适用场景(多路 CPU / 非一致性内存架构下的 Ignite 数据区域堆外内存分配)、前置依赖与源码构建流程、Java/C++/libnuma 三层实现原理,以及SimpleNumaAllocationStrategy、InterleavedNumaAllocationStrategy、LocalNumaAllocationStrategy四种分配策略的 XML 配置实战。读完本指南,你可以在 Linux 环境下从零编译该模块,并将 NUMA 感知分配器接入 Ignite 数据存储配置,实现按节点亲和的堆外内存分配。

一、为什么需要 NUMA 感知的内存分配

在现代多路服务器上,CPU 与内存之间并非对称访问:每个 NUMA 节点(NUMA node)由一组 CPU 核和一段本地内存构成,跨节点访问的内存延迟显著高于本地访问。Apache Ignite 的持久化与堆外内存架构大量使用直接内存(off-heap),默认情况下,Ignite 通过UnsafeMemoryAllocator(见 modules/core/src/main/java/org/apache/ignite/internal/mem/unsafe/UnsafeMemoryAllocator.java)调用GridUnsafe.allocateMemory分配内存,此时内存页可能被操作系统随机分布到任意 NUMA 节点,导致 Ignite 线程访问"远端"内存,放大访问延迟、加剧内存总线争用。

ignite-numa-allocator模块提供了一种替代方案:基于 Linux 的libnuma,将 Ignite 数据区域的堆外内存显式分配到指定 NUMA 节点。它实现了 Ignite 核心定义的MemoryAllocator接口(见 modules/core/src/main/java/org/apache/ignite/mem/MemoryAllocator.java),接口只包含两个方法:

  • long allocateMemory(long size):分配size字节的堆外内存,返回内存地址(失败返回0);
  • void freeMemory(long addr):释放指定地址的内存。

因此,NUMA 感知分配器可以无缝替换默认的UnsafeMemoryAllocator,对上层数据区域透明。需要强调的是:该模块只支持 Linux 发行版,且要求libnuma >= 2.0.x,这是本模块所有使用场景的前提。

二、环境要求与依赖安装

2.1 运行时依赖

运行ignite-numa-allocator需要 Linux 机器上安装libnuma 2.0.x,官方文档同时建议安装numactl工具——它既能用于验证节点布局(如numactl --hardware),也能在启动 JVM 时通过numactl设置进程/线程的 NUMA 内存策略(详见本文第四部分对默认策略的说明)。

RHEL / CentOS 系:

sudo yum install numactl

Ubuntu / Debian 系:

sudo apt install numactl

2.2 构建依赖

从源码构建该模块需要:

  • JDK(用于生成 JNI 头文件与编译 Java 代码);
  • C++11 兼容编译器(gcc >= 4.8.5);
  • libnuma头文件。

RHEL / CentOS 系:

sudo yum groupinstall 'Development Tools' sudo yum install java-17-openjdk numactl-devel libstdc++-static

Ubuntu / Debian 系:

sudo apt install build-essential libnuma-dev openjdk-17-jdk

2.3 通过 Maven 完成原生库构建

原生共享库libnuma_alloc.so的编译由cmake-maven-plugin集成到 Maven 构建中,相关配置位于 modules/numa-allocator/pom.xml。需要注意:

  • 模块坐标是org.apache.ignite:ignite-numa-allocator,其父工程为parent-internal;
  • 原生构建只在linux-buildprofile 中生效,该 profile 的激活条件是操作系统为 Linux 且架构为amd64(<os><name>Linux</name><arch>amd64</arch></os>),并将cmake.classifier设置为linux-x86_64;
  • 构建流程分为两步:cmake-generate阶段以modules/numa-allocator/src/main/cpp为源码目录生成 Unix Makefiles(CMAKE_BUILD_TYPE=Release),随后cmake-build阶段执行编译;
  • 编译产物*.so会被maven-resources-plugin复制到target/classes/org/apache/ignite/internal/mem/linux/${os.arch}/目录,打包进 JAR 的类路径中。

CMake 构建脚本位于 modules/numa-allocator/src/main/cpp/CMakeLists.txt,其关键点:

  • 以find_package(JNI REQUIRED)与find_package(Threads REQUIRED)定位 JNI 头文件与线程库;
  • 通过find_library(NUMA_LIBRARIES NAMES libnuma.so)查找libnuma,找不到时直接以FATAL_ERROR终止构建,并输出 "not found numa library";
  • 编译选项包含-shared -D_FORTIFY_SOURCE=2 -z noexecstack -z,relro -z,now -fPIC等安全加固参数,链接时使用-static-libgcc -static-libstdc++静态链接 GCC/标准库运行库,降低对目标机器运行环境的依赖。

构建成功后,可直接用标准 Maven 命令编译安装该模块(在仓库根目录执行):

./mvnw -pl modules/numa-allocator -am clean install

2.4 原生库的加载机制

NumaAllocUtil的类初始化(static块)负责加载原生库,逻辑见 modules/numa-allocator/src/main/java/org/apache/ignite/internal/mem/NumaAllocUtil.java:

  • 库路径按os.name与os.arch决定,非 Linux 系统会直接抛出UnsupportedOperationException("Operating System is not supported");
  • 库文件按/org/apache/ignite/internal/mem/linux/<arch>/libnuma_alloc.so从 JAR 类路径中读取,写入临时文件后通过System.load加载,退出时自动清理;
  • 加载成功后立即调用nodesCount(),将系统可用 NUMA 节点数保存到静态常量NUMA_NODES_CNT中,供各分配策略做参数合法性校验;
  • 若原生库缺失或加载失败,会抛出ExceptionInInitializerError(如 "Failed to load native numa_alloc library ... not found")。

因此在实际部署时,必须保证 JAR 中包含对应 Linux 架构的libnuma_alloc.so(即通过linux-buildprofile 构建,或将构建产物按上述路径放入类路径),并保证系统已安装libnuma。

三、实现原理:Java 策略 → JNI → libnuma 的调用链

3.1 三层结构总览

该模块的代码结构清晰分为三层,从仓库目录modules/numa-allocator/src即可看出:

src/main/java —— 分配器与策略(org.apache.ignite.mem.*)+ JNI 声明(internal.mem.NumaAllocUtil) src/main/cpp —— C++ 原生实现(numa_alloc 封装 + JNI 桥接) src/test/java —— 单元测试与集群级集成测试

Java 层定义策略接口NumaAllocationStrategy(见 modules/numa-allocator/src/main/java/org/apache/ignite/mem/NumaAllocationStrategy.java),仅含long allocateMemory(long size)一个方法;NumaAllocator实现MemoryAllocator,持有策略对象并转发调用(见 modules/numa-allocator/src/main/java/org/apache/ignite/mem/NumaAllocator.java):

  • allocateMemory(long size)直接委托给allocStrategy.allocateMemory(size);
  • freeMemory(long addr)统一调用NumaAllocUtil.free(addr),无论哪种策略分配的内存都以相同方式释放。

所有真正与libnuma交互的入口集中在NumaAllocUtil的六个native方法:allocate、allocateOnNode、allocateLocal、allocateInterleaved、chunkSize、free,分别对应libnuma的numa_alloc/numa_alloc_onnode/numa_alloc_local/numa_alloc_interleaved(_subset)/numa_size/numa_free系列函数。

JNI 桥接层位于 modules/numa-allocator/src/main/cpp/src/org_apache_ignite_internal_mem_NumaAllocUtil.cpp,它将jlong size转为size_t、将jintArray(节点数组)包装为 RAII 守卫JIntArrayGuard后逐位写入numa::BitSet(底层为numa_bitmask),再调用 C++ 命名空间numa的封装函数。

3.2 C++ 侧的内存布局与大小追踪

C++ 核心封装位于 modules/numa-allocator/src/main/cpp/src/numa/numa_alloc.cpp。为了让NumaAllocUtil.chunkSize(long addr)能返回实际应用内存大小,分配时会在用户请求的size前额外预留一个头部,内存布局如下:

+-------------------------------+------------------------+ | Header (sizeof(max_align_t)) | Application memory | +------------------------------ +------------------------+ ^ | Result pointer(返回给上层)

头部使用union region_size { size_t size; max_align_t a; }实现:既按max_align_t保证最大对齐,又复用该空间存储应用内存块大小。NumaAllocHelper模板统一完成"多分配一个头部、写入大小、返回头部之后地址"的动作;Free则在释放前通过ConvertPointer回退到头部,用numa_free(ptr, ptr->size + sizeof(region_size))释放完整区域。这意味着上层拿到的是偏移后的指针,free时无需再关心块大小,由封装自动换算。

另外,JNI 桥接层对allocateOnNode做了防御处理:当传入的node不在[0, numa::NumaNodesCount())范围时,会自动退化为普通numa::Alloc(size)(即默认策略),避免越界调用底层 API。

四、四种分配策略与配置实战

策略对象通过构造函数注入NumaAllocator,再挂接到 Ignite 配置的memoryAllocator属性上。配置有两个层级:

  • DataStorageConfiguration.setMemoryAllocator(...):为所有数据区域设置默认分配器;若未设置,Ignite 默认使用基于Unsafe的分配器(见 modules/core/src/main/java/org/apache/ignite/configuration/DataStorageConfiguration.java);
  • DataRegionConfiguration.setMemoryAllocator(...):覆盖单个数据区域的分配器(见 modules/core/src/main/java/org/apache/ignite/configuration/DataRegionConfiguration.java)。

NUMA 分配器与这两种方式均兼容(这一点由集成测试验证,见本文第五部分)。以下 XML 配置均以defaultDataRegionConfiguration(默认数据区域)为例,实际使用时按需放置在上述任一配置层级。

4.1 SimpleNumaAllocationStrategy:简单分配策略

默认策略形式:按当前线程/进程的默认 NUMA 内存策略分配,底层调用void *numa_alloc(size_t)。所谓"默认策略"可通过numactl工具预先设定(例如numactl --interleave=all java ...会让进程采用交错策略),因此该配置适合"由外部环境决定放置策略"的场景:

<property name="dataStorageConfiguration"> <bean class="org.apache.ignite.configuration.DataStorageConfiguration"> <property name="defaultDataRegionConfiguration"> <bean class="org.apache.ignite.configuration.DataRegionConfiguration"> <property name="name" value="Default_Region"/> .... <property name="memoryAllocator"> <bean class="org.apache.ignite.mem.NumaAllocator"> <constructor-arg> <bean class="org.apache.ignite.mem.SimpleNumaAllocationStrategy"/> </constructor-arg> </bean> </property> </bean> </property> </bean> </property>

指定节点形式:显式在编号为node的 NUMA 节点上分配,底层调用void *numa_alloc_onnode(size_t, int):

<property name="dataStorageConfiguration"> <bean class="org.apache.ignite.configuration.DataStorageConfiguration"> <property name="defaultDataRegionConfiguration"> <bean class="org.apache.ignite.configuration.DataRegionConfiguration"> <property name="name" value="Default_Region"/> .... <property name="memoryAllocator"> <bean class="org.apache.ignite.mem.NumaAllocator"> <constructor-arg> <bean class="org.apache.ignite.mem.SimpleNumaAllocationStrategy"> <constructor-arg name="node" value="0"/> </bean> </constructor-arg> </bean> </property> </bean> </property> </bean> </property>

参数校验(见 modules/numa-allocator/src/main/java/org/apache/ignite/mem/SimpleNumaAllocationStrategy.java):无参构造时内部使用哨兵值NODE_NOT_SET = Integer.MAX_VALUE,allocateMemory走NumaAllocUtil.allocate(size);带参构造则通过A.ensure断言node >= 0且node < NumaAllocUtil.NUMA_NODES_CNT,越界或负数会直接抛出IllegalArgumentException。节点编号以numactl --hardware输出为准。

4.2 InterleavedNumaAllocationStrategy:交错分配策略

全部节点交错:在所有可用 NUMA 节点之间均匀交错分配,底层调用void *numa_alloc_interleaved(size_t)。交错分配能均摊各节点内存带宽,适合计算压力分散、无法保证节点亲和的场景:

<property name="dataStorageConfiguration"> <bean class="org.apache.ignite.configuration.DataStorageConfiguration"> <property name="defaultDataRegionConfiguration"> <bean class="org.apache.ignite.configuration.DataRegionConfiguration"> <property name="name" value="Default_Region"/> .... <property name="memoryAllocator"> <bean class="org.apache.ignite.mem.NumaAllocator"> <constructor-arg> <bean class="org.apache.ignite.mem.InterleavedNumaAllocationStrategy"/> </constructor-arg> </bean> </property> </bean> </property> </bean> </property>

指定节点子集交错:仅在给定节点集合上交错分配,底层调用void *numa_alloc_interleaved_subset(size_t, struct bitmask*)。Java 层的int[] nodes在 JNI 层被转换为numa_bitmask位掩码:

<property name="dataStorageConfiguration"> <bean class="org.apache.ignite.configuration.DataStorageConfiguration"> <property name="defaultDataRegionConfiguration"> <bean class="org.apache.ignite.configuration.DataRegionConfiguration"> <property name="name" value="Default_Region"/> .... <property name="memoryAllocator"> <bean class="org.apache.ignite.mem.NumaAllocator"> <constructor-arg> <bean class="org.apache.ignite.mem.InterleavedNumaAllocationStrategy"> <constructor-arg name="nodes"> <array> <value>0</value> <value>1</value> </array> </constructor-arg> </bean> </constructor-arg> </bean> </property> </bean> </property> </bean> </property>

参数语义(见 modules/numa-allocator/src/main/java/org/apache/ignite/mem/InterleavedNumaAllocationStrategy.java):

  • 传入null或空数组等价于"全部节点交错",内部将nodes置为null,JNI 层走AllocInterleaved(size)分支;
  • 传入非空数组时,内部先Arrays.copyOf拷贝并排序,再校验首元素>= 0、末元素< NUMA_NODES_CNT,校验失败抛IllegalArgumentException;
  • JNI 层对节点索引本身不做二次校验,直接bs.Set(nodes[i])写入位掩码,因此 Java 层的合法性校验是关键防线。

4.3 LocalNumaAllocationStrategy:本地节点分配策略

在当前进程所在的本地 NUMA 节点上分配,底层调用void *numa_alloc_local(size_t)(注意:README 中写为numa_alloc_onnode(size_t),实际源码 LocalNumaAllocationStrategy.java 与 numa_alloc.cpp 使用的是numa_alloc_local)。"本地"由进程当前运行的 CPU 所在节点决定,因此通常与"将 Ignite 线程绑定到某组 CPU"搭配使用,能获得最优的本地访问延迟:

<property name="dataStorageConfiguration"> <bean class="org.apache.ignite.configuration.DataStorageConfiguration"> <property name="defaultDataRegionConfiguration"> <bean class="org.apache.ignite.configuration.DataRegionConfiguration"> <property name="name" value="Default_Region"/> .... <property name="memoryAllocator"> <bean class="org.apache.ignite.mem.NumaAllocator"> <constructor-arg> <constructor-arg> <bean class="org.apache.ignite.mem.LocalNumaAllocationStrategy"/> </constructor-arg> </constructor-arg> </bean> </property> </bean> </property> </bean> </property>

注意:README 原文在该示例中嵌套了两层<constructor-arg>。从源码看NumaAllocator只有一个构造参数(NumaAllocationStrategy),Spring 通常能容许多余的 constructor-arg,但更严谨的写法是只保留一层(与 4.1 / 4.2 示例一致)。如果你的配置解析报错,优先去掉外层多余的<constructor-arg>。

该策略类没有任何成员变量,是无状态实现,allocateMemory直接转发NumaAllocUtil.allocateLocal(size)。

4.4 策略选择小结

策略类分配位置底层 libnuma 函数适用场景
SimpleNumaAllocationStrategy()线程/进程默认策略numa_alloc(size_t)由numactl等外部工具决定策略
SimpleNumaAllocationStrategy(int node)指定节点numa_alloc_onnode(size_t, int)明确单节点亲和
InterleavedNumaAllocationStrategy()全部节点交错numa_alloc_interleaved(size_t)均衡带宽、分散放置
InterleavedNumaAllocationStrategy(int[] nodes)指定节点集合交错numa_alloc_interleaved_subset(size_t, struct bitmask*)指定多节点均衡
LocalNumaAllocationStrategy()进程本地节点numa_alloc_local(size_t)配合 CPU 绑定追求最低延迟

所有策略类均实现Serializable并支持GridToStringBuilder的字符串表示,便于在日志与诊断中查看当前策略。

五、测试验证:从单元级到集群级

仓库在 modules/numa-allocator/src/test/java/org/apache/ignite/internal/mem 下提供了两层测试,可作为"配置是否正确、分配器是否生效"的验证模板:

单元测试NumaAllocatorUnitTest.java:

  • PositiveScenarioTest:对Local、Interleaved(含空数组、偶数节点、全部节点)与Simple(无参、末节点)共 7 种策略,分配32MB缓冲后断言NumaAllocUtil.chunkSize(ptr)等于请求大小,并通过GridUnsafe.setMemory/getByte做全量读写回环验证,最后freeMemory释放;
  • ErrorScenarioTest:验证非法参数——Interleaved传入含负数({-3,-4,0})或节点数超过NUMA_NODES_CNT的数组、Simple传入负数或等于NUMA_NODES_CNT的节点号,均断言抛出IllegalArgumentException。

集群级集成测试NumaAllocatorBasicTest.java:

  • 参数化组合了 5 种策略 × 两种挂载方式(DataStorageConfiguration.setMemoryAllocator全局默认 /DataRegionConfiguration.setMemoryAllocator单区域覆盖);
  • 启动 3 个服务端节点 + 1 个客户端,通过IgniteDataStreamer向test缓存持续灌入 4KB 随机数据,直到各节点默认数据区域用量达到0.9 * MAX_SIZE(MAX_SIZE = 100MB),验证分配器在真实数据装载压力下工作正常;
  • 最后断言所有服务端节点默认数据区域的分配器实例均为NumaAllocator,确认配置确实生效。

完整的测试套件入口为 NumaAllocatorTestSuite.java(实际路径modules/numa-allocator/src/test/java/org/apache/ignite/testsuites/NumaAllocatorTestSuite.java)。运行测试前请确保当前机器已安装libnuma且 JAR/类路径中包含编译好的libnuma_alloc.so,否则测试会在NumaAllocUtil类初始化阶段失败。

六、使用限制与注意事项

  1. 平台限制:仅支持 Linux(NumaAllocUtil对非 Linux 直接抛UnsupportedOperationException),且 Maven 原生构建 profile 目前仅覆盖amd64架构;在未启用linux-buildprofile 的环境中运行会因缺少原生库而报ExceptionInInitializerError。
  2. 系统依赖:运行与构建均要求libnuma 2.0.x;构建还需要numactl-devel/libnuma-dev头文件包,缺少时 CMake 以FATAL_ERROR终止。
  3. 节点编号合法性:SimpleNumaAllocationStrategy与InterleavedNumaAllocationStrategy在构造时即校验节点编号范围(0 <= node < NUMA_NODES_CNT),非法参数抛IllegalArgumentException;NUMA_NODES_CNT来自底层numa_max_node() + 1,随机器拓扑动态确定。
  4. 策略语义差异:SimpleNumaAllocationStrategy()的无参形式取决于线程/进程的默认 NUMA 策略,若要按计划放置内存,请用numactl显式设置;LocalNumaAllocationStrategy的"本地"随进程当前 CPU 迁移而变化,结合 CPU 绑定(如 taskset / cpuset)才能保证亲和稳定。
  5. 内存释放一致性:无论采用哪种策略分配,释放统一走NumaAllocUtil.free(内部numa_free),上层无需也不能混用其他释放路径;C++ 层会自动把指针回退到头部并按实际总大小释放,因此请勿对分配结果做非法的指针偏移后释放。

按上述步骤完成依赖安装、原生库构建与策略配置后,即可在 NUMA 多路服务器上让 Ignite 数据区域的内存分配贴合硬件拓扑,为后续的内存访问延迟优化打下基础。

  • 分布式数据库
  • 缓存

【免费下载链接】ignite

Apache Ignite

项目地址:https://gitcode.com/gh_mirrors/ignite4/ignite
点击查看免费下载

相关推荐

上一篇:chproxy配置文件详解:从simple.yml到wildcarded.yml的实用示例
下一篇:sway-farm游戏周边生态:NFT市场与第三方工具集成

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询