☰
Hazelcast Linux CPU 亲和性辅助库(libaffinity_helper)编译与使用指南
2026/10/9 1:48:44 网站建设 项目流程

Hazelcast 为 Linux x86_64 平台提供了一套自研的 CPU 亲和性(CPU Affinity)JNI 共享库libaffinity_helper.so,用于在 TPC(Thread-Per-Core)引擎中把事件循环线程绑定到指定 CPU 核心,从而减少缓存抖动、提升实时数据处理性能。本文以仓库中 affinity_helper.md 的编译步骤为核心,结合 affinity_helper.c 源码与 ThreadAffinityHelper.java 集成实现,完整讲解如何编译、打包、加载并使用这套亲和性辅助库,读完即可独立完成构建并理解其底层工作原理。

为什么需要自研亲和性辅助库

在开启 TPC(Thread-Per-Core)模式的 Hazelcast 中,每个 Reactor 线程负责独立的事件循环,最佳实践是让这些线程与物理 CPU 核心一一对应。业界常用的 OpenHFT Affinity 库虽然功能完整,但要求用户自行加载共享库、并额外把 OpenHFT 的 jar 加入 classpath,部署成本较高。

从 ThreadAffinityHelper.java 的类注释可以看出,Hazelcast 开发这套辅助库的目的正是简化 Linux x86_64 系统上的 CPU 亲和性配置:它把一个小体积的共享库直接打进 Hazelcast 的 jar 包,运行时自动解压加载,用户无需手动干预。在非 Linux x86_64 平台或共享库加载失败时,ThreadAffinityHelper.isAffinityAvailable() 会自动回退到 OpenHFT 实现。

编译共享库(核心步骤)

仓库中的 affinity_helper.md 给出了 Linux 下编译该共享库的标准步骤。编译前请先确认:

  • 已安装 GCC 工具链;
  • 已安装对应 JDK(用于提供jni.h);
  • PATH_TO_JDK_INCLUDE_DIR:你的 JDK 安装目录下 include 目录的完整路径,通常形如/usr/lib/jvm/java-17-openjdk-amd64/include(不同发行版路径不同,以实际安装位置为准)。

按以下两步依次执行编译命令:

# 第一步:编译目标文件(-c 只编译不链接) gcc -c -I ${PATH_TO_JDK_INCLUDE_DIR} -fPIC -Os -o affinity_helper.o affinity_helper.c # 第二步:链接生成共享库(-shared 生成 .so) gcc -shared -fPIC -Wl,-soname,libaffinity_helper.so -o affinity_helper.so affinity_helper.o -lc

各编译选项的含义如下:

选项作用
-c只编译为目标文件,不执行链接,便于分步构建
-I ${PATH_TO_JDK_INCLUDE_DIR}指定头文件搜索路径,让编译器找到jni.h及其平台子目录(如linux/下的jni_md.h)
-fPIC生成位置无关代码(Position Independent Code),共享库必须开启
-Os优化生成代码体积,使打进 jar 的库尽可能小
-o affinity_helper.o/-o affinity_helper.so指定输出文件名
-Wl,-soname,libaffinity_helper.so设置共享库的 SONAME,用于运行时动态链接标识
-lc链接 C 标准库(libc),提供sched_*等系统调用封装所需符号

编译成功后得到affinity_helper.so。需要注意的是,-I只指定了 JDK include 目录;由于源码中#include "affinity_helper.h",编译时需要把仓库中的 affinity_helper.h 放在同一目录下(或通过额外-I指定其所在目录)。

关于头文件的说明

affinity_helper.h 是标准 JNI 工具(如javah)生成的机器头文件,声明了两个导出给 Java 侧调用的 native 方法:

  • Java_com_hazelcast_internal_util_ThreadAffinityHelper_getAffinity0:无参数,返回java/util/BitSet,用于读取当前线程的 CPU 亲和性掩码;
  • Java_com_hazelcast_internal_util_ThreadAffinityHelper_setAffinity0:接收一个java/util/BitSet参数,用于设置当前线程的 CPU 亲和性掩码。

方法名中的com_hazelcast_internal_util_ThreadAffinityHelper对应 Java 侧的类全限定名com.hazelcast.internal.util.ThreadAffinityHelper,这是 JNI 符号导出规则(包名中的点替换为下划线)决定的。

源码级原理解析

读取亲和性:getAffinity0

affinity_helper.c 中的getAffinity0实现如下流程:

  1. 通过sysconf(_SC_NPROCESSORS_ONLN)获取系统在线 CPU 数量;
  2. 通过syscall(SYS_gettid)获取当前线程的 TID(注意是线程 ID,而非进程 ID——亲和性是线程粒度的属性);
  3. 在 Java 侧创建java.util.BitSet实例(通过 JNIFindClass/GetMethodID/NewObject反射式构造);
  4. 调用sched_getaffinity(tid, sizeof(cpu_set_t), &cpumask)读取当前线程的 CPU 掩码;
  5. 遍历每个 CPU 编号,用CPU_ISSET(i, &cpumask)判断该 CPU 是否在掩码中,若是则调用 JavaBitSet#set(i)置位;
  6. 返回填充完成的BitSet对象给 Java 侧。

代码中每一处 JNI 查找都做了空指针检查,失败时通过throw_runtime_exception抛出带详细信息的java.lang.RuntimeException,便于排查。

设置亲和性:setAffinity0

affinity_helper.c 中的setAffinity0是反向流程:

  1. 获取系统在线 CPU 数量与当前线程 TID;
  2. 通过 JNI 调用 JavaBitSet#size()获取位集大小,用BitSet#get(i)逐个读取每一位;
  3. 用CPU_ZERO(&cpumask)清空本地掩码,对置位的位调用CPU_SET(i, &cpumask);
  4. 调用sched_setaffinity(tid, sizeof(cpumask), &cpumask)应用新的亲和性;
  5. 失败时抛出RuntimeException。

整体实现非常轻量:不依赖任何第三方 C 库,只使用 glibc 的sched.h与 Linux 系统调用,这正是它能以极小体积随 Hazelcast jar 分发的原因。

Java 侧封装:自动解压与加载

Java 侧的 ThreadAffinityHelper.java 在静态初始化块中完成库的加载决策(见 ThreadAffinityHelper.java#L123-L142):

  • 检查系统属性hazelcast.affinity.lib.disabled是否为true,若为true则完全跳过 Hazelcast 自有库,一律使用 OpenHFT 实现;
  • 否则在OS.isLinux()且 JVM 为 64 位(!JVM.is32bit())的前提下,尝试从 jar 资源lib/linux-x86_64/libaffinity_helper.so中读取共享库;
  • extractBundledLib() 把 jar 内的.so资源复制为临时文件(文件名形如hazelcast-libaffinity-helper-*.so),再通过System.load()加载;
  • 任一环节失败都会记录fine级日志并回退到 OpenHFT,保证亲和性功能不会成为阻塞性故障。

对外暴露的 getAffinity() 与 setAffinity(BitSet) 会根据USE_HZ_LIB标志自动在自有库与 OpenHFT 之间切换,异常时记录 warning 日志并返回空BitSet,保持接口稳定。

在 TPC 引擎中使用亲和性

共享库的实际消费方是 TPC 引擎的 Reactor 线程。在 Reactor.java 的 StartEventloopTask#configureThreadAffinity 中,事件循环线程启动时:

  1. 从ThreadAffinity对象获取当前线程被允许绑定的 CPU 集合allowedCpus;
  2. 调用ThreadAffinityHelper.setAffinity(allowedCpus)绑定;
  3. 再调用ThreadAffinityHelper.getAffinity()回读实际掩码并比对;
  4. 若不一致,记录 warning 日志提示亲和性未生效;一致则记录fine级日志确认绑定成功。

亲和性规则本身通过系统属性配置。ReactorBuilder.java#L40 定义了属性名hazelcast.tpc.reactor.affinity,其取值语法由 ThreadAffinity.java 中的AffinityParser解析,支持 CPU 分组(每个分组指定允许的 CPU 编号与重复线程数),例如用形如0,1,2,3或分组语法2x0-3的表达方式为多个 Reactor 线程分配不同的 CPU 集合。若未设置该属性,亲和性默认禁用(ThreadAffinity.DISABLED,见 ThreadAffinity.java#L38)。

典型使用场景与注意事项

  • 开发者自行编译替换:默认情况下用户无需编译——Hazelcast jar 中已内置编译好的lib/linux-x86_64/libaffinity_helper.so。本文的编译步骤主要用于:修改 C 源码后的调试、为其他 Linux 发行版重建、或研究 JNI 交互细节。若你重新编译了affinity_helper.so,可参照 extractBundledLib() 读取的资源路径lib/linux-x86_64/libaffinity_helper.so将其替换进自己的打包流程。
  • 启用亲和性的前提:仅 Linux 64 位 JVM 生效(源码判断条件见 ThreadAffinityHelper.java#L132);其他平台自动回退到 OpenHFT,若 OpenHFT 也不可用(如缺少 JNA),则isAffinityAvailable()返回false,使用亲和性配置时会抛出RuntimeException提示 "Thread affinity support is not available"。
  • 关闭自有库:如遇与自研库相关的兼容性问题,可通过 JVM 参数-Dhazelcast.affinity.lib.disabled=true强制回退到 OpenHFT 实现。
  • 验证亲和性是否生效:观察 Reactor 线程启动时的日志——绑定成功打印has affinity for CPUs:...(fine 级),失败打印affinity was not applied successfully(warning 级);也可在运行期用taskset -pc <tid>核对实际掩码。

小结

本文完整继承了 affinity_helper.md 中两条核心编译命令,并在此基础上补充了每个 gcc 选项的作用、JNI 头文件的符号约定、C 源码中sched_getaffinity/sched_setaffinity的实现细节,以及 Java 侧自动解压加载与 TPC 引擎调用链路。掌握这些内容后,你既能独立完成libaffinity_helper.so的构建,也能在线上环境通过系统属性精确控制 Hazelcast 事件循环线程的 CPU 绑定,为实时数据平台的性能调优打下基础。

  • 缓存
  • KV存储
  • 消息队列
  • 流处理
  • 后端

【免费下载链接】hazelcast

Hazelcast is a unified real-time data platform combining stream processing with a fast data store, allowing customers to act instantly on>项目地址:https://gitcode.com/gh_mirrors/ha/hazelcast

点击查看免费下载
上一篇:GetQzonehistory 使用指南:一键把 QQ 空间全部历史说说归档到本地
下一篇:@microsoft/fast-colors lchToRGB() 函数解析:从 CIELCH 到 RGBA 的色彩转换实战

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

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

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

立即咨询