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实现如下流程:
- 通过
sysconf(_SC_NPROCESSORS_ONLN)获取系统在线 CPU 数量; - 通过
syscall(SYS_gettid)获取当前线程的 TID(注意是线程 ID,而非进程 ID——亲和性是线程粒度的属性); - 在 Java 侧创建
java.util.BitSet实例(通过 JNIFindClass/GetMethodID/NewObject反射式构造); - 调用
sched_getaffinity(tid, sizeof(cpu_set_t), &cpumask)读取当前线程的 CPU 掩码; - 遍历每个 CPU 编号,用
CPU_ISSET(i, &cpumask)判断该 CPU 是否在掩码中,若是则调用 JavaBitSet#set(i)置位; - 返回填充完成的
BitSet对象给 Java 侧。
代码中每一处 JNI 查找都做了空指针检查,失败时通过throw_runtime_exception抛出带详细信息的java.lang.RuntimeException,便于排查。
设置亲和性:setAffinity0
affinity_helper.c 中的setAffinity0是反向流程:
- 获取系统在线 CPU 数量与当前线程 TID;
- 通过 JNI 调用 Java
BitSet#size()获取位集大小,用BitSet#get(i)逐个读取每一位; - 用
CPU_ZERO(&cpumask)清空本地掩码,对置位的位调用CPU_SET(i, &cpumask); - 调用
sched_setaffinity(tid, sizeof(cpumask), &cpumask)应用新的亲和性; - 失败时抛出
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 中,事件循环线程启动时:
- 从
ThreadAffinity对象获取当前线程被允许绑定的 CPU 集合allowedCpus; - 调用
ThreadAffinityHelper.setAffinity(allowedCpus)绑定; - 再调用
ThreadAffinityHelper.getAffinity()回读实际掩码并比对; - 若不一致,记录 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