在大数据计算领域,Spark 和 Flink 已经成为事实上的标准。然而,随着数据量的爆炸式增长,基于 JVM 的原生算子在计算密集型任务(如 Shuffle、Join、Aggregation)中逐渐显露出性能瓶颈。华为鲲鹏 BoostKit 推出的 OmniAdaptor 项目,旨在作为"胶水层",将上层计算框架与底层的 Native 加速引擎(OmniOperator)无缝连接。本文将结合源码,深入剖析 OmniAdaptor 如何通过 JNI、向量化计算和内存管理优化,实现大数据性能的跨越式提升。
官方资源 :鲲鹏 BoostKit 开源开放地图

今天这篇文章就带大家一起来分析一下BoostKit里面的OmniAdaptor 项目吧,下载源码的话直接使用Git就能快速下载到:

1. 引言
在传统的 Spark 执行模型中,数据处理主要依赖于 Java 对象模型。这种方式虽然通用性强,但在处理海量数据时面临两大挑战:
- GC(垃圾回收)开销:海量小对象的创建与销毁导致频繁 GC,严重影响吞吐量。
- CPU 流水线效率低:基于行的处理方式无法利用现代 CPU 的 SIMD(单指令多数据)指令集。
OmniAdaptor 的出现正是为了解决这些问题。它作为一个中间件,拦截 Spark 的物理执行计划,将计算密集型算子替换为基于 C++ 实现的 OmniOperator 算子。
这个过程需要 JVM 与 Native 之间建立稳定的交互环境,因此 OmniAdaptor 在启动时会通过 JNI 完成一系列初始化:创建 Native 上下文、加载 C++ 计算库、建立算子执行环境等。
下面这张图展示的正是 OmniAdaptor 启动阶段的 JNI 初始化流程,也就是 Spark 执行计划真正"接上"原生加速引擎的关键步骤:

2. 架构解析
OmniAdaptor 的核心在于打通 JVM 与 Native(C++)的边界。这主要通过 spark-extension-shims(Java/Scala 端)和 cpp/src/jni(C++ 端)来实现。
2.1 Native 入口:SparkJniWrapper
在源码 cpp/src/jni/SparkJniWrapper.cpp 中,我们可以看到 Java 调用 C++ 的入口函数。例如,nativeMake 函数负责初始化 Shuffle 过程:
// cpp/src/jni/SparkJniWrapper.cpp
JNIEXPORT jlong JNICALL Java_com_huawei_boostkit_spark_jni_SparkJniWrapper_nativeMake(
JNIEnv* env, jobject, jstring partitioning_name_jstr, jint num_partitions,
// ... 其他参数
jint spill_batch_row, jlong task_spill_memory_threshold)
{
JNI_FUNC_START
// 1. 参数校验与转换
if (partitioning_name_jstr == nullptr) {
env->ThrowNew(runtimeExceptionClass, "Short partitioning name can't be null");
return 0;
}
// 2. 配置 SplitOptions
auto splitOptions = SplitOptions::Defaults();
if (buffer_size > 0) splitOptions.buffer_size = buffer_size;
// 3. 创建 Native Splitter 实例
// ...
}
这段代码展示了 OmniAdaptor 如何接收来自 Spark 的配置(如分区数、内存阈值),并将其转化为 C++ 结构体,为后续的高性能计算做准备。
3. 核心突破:Native Shuffle 机制
Shuffle 是 Spark 任务中最耗时、最复杂的阶段之一,它需要完成大规模数据的 分区、序列化、落盘、网络传输、再读回 等一系列重操作。传统 JVM Shuffle 在执行这些步骤时,会频繁创建对象、触发 GC,并伴随大量随机内存访问,性能压力非常大。
OmniAdaptor 在这一环节做出的核心创新,就是将 Shuffle 整条链路下沉到 Native 层。通过 Splitter 类,它接管了 Spark ShuffleWriter 的大部分职责:从哈希分区计算、批次切分,到缓冲管理、spill 到磁盘,都在 C++ 中完成。由于绕过了 JVM 对象模型,Shuffle 不再受 GC 限制,同时可以利用连续内存、SIMD 指令和更精细的内存管理,使得整个 Shuffle 阶段的延迟大幅下降。
3.1 向量化分区计算
在 cpp/src/shuffle/splitter.cpp 中,ComputeAndCountPartitionId 函数是性能优化的关键:
// cpp/src/shuffle/splitter.cpp
int Splitter::ComputeAndCountPartitionId(VectorBatch& vb) {
auto num_rows = vb.GetRowCount();
// 清空计数器
memset_s(partition_id_cnt_cur_, ...);
if (singlePartitionFlag) {
// 单分区优化路径
// ...
} else {
// 获取哈希向量(Vectorized Access)
auto hash_vct = reinterpret_cast<Vector<int32_t> *>(vb.Get(0));
for (auto i = 0; i < num_rows; ++i) {
// 直接读取连续内存,计算 Partition ID
int32_t pid = hash_vct->GetValue(i);
partition_id_[i] = pid;
partition_id_cnt_cur_[pid]++;
}
}
return 0;
}
代码解读:
- VectorBatch:OmniAdaptor 不再处理单行记录,而是处理列式的数据批(Batch)。
- 连续内存访问 :
hash_vct->GetValue(i)访问的是连续的 C++ 内存,极大地提高了 CPU 缓存命中率。 - 无 GC:整个过程不涉及任何 Java 对象的创建。
Native Shuffle 的分裂过程:

Native Shuffle Splitter 在处理一个 VectorBatch 时的整体数据流。可以看到分区计算、批次拆分和 SIMD 加速的整体过程,其耗时通常在 微秒级
3.2 内存管理与零拷贝
为了避免 JVM 与 Native 之间的数据拷贝,OmniAdaptor 采用了堆外内存(Off-Heap Memory)管理。
在 cpp/src/io/wrap/zero_copy_stream_wrapper.h 中,我们可以看到对 Google Protobuf ZeroCopyStream 的封装。这意味着数据在序列化和网络传输过程中,尽可能减少了内核态与用户态之间的拷贝。
// 内存分配示意 (cpp/src/shuffle/splitter.cpp)
void *ptr_tmp = static_cast<void *>(options_.allocator->Alloc(new_size * (1 << column_type_id_[i])));
std::shared_ptr<Buffer> value_buffer (new Buffer((uint8_t *)ptr_tmp, ...));
OmniAdaptor 使用自定义的 allocator 直接向操作系统申请大页内存,不仅绕过了 JVM 堆内存限制,还减少了 TLB Miss,进一步提升性能。
在 Shuffle 数据溢写过程中,Splitter 会将超出内存的数据以列式格式写入大页内存,并顺序溢写到磁盘,同时充分利用连续内存和 SIMD 优化,提升吞吐和效率。
下图展示了这一 Native Spill 过程,可以直观看到列式存储和大页内存优化带来的性能优势:

4. 性能收益
为了验证 OmniAdaptor 的真实性能,我们在鲲鹏 920 集群上进行了 TPC-DS 标准测试。接下来将带大家看一下如何在实际开发环境中部署 OmniAdaptor 并复现其性能收益。
4.1 部署与配置
OmniAdaptor 以 Spark 插件的形式存在。部署时,我们需要将编译好的 Jar 包放入 Spark 的 jars 目录,并更新 spark-defaults.conf。
# 1. 替换 Jar 包
cp boostkit-omniop-spark-3.1.1-1.0.0.jar $SPARK_HOME/jars/
cp boostkit-omniop-bindings-1.0.0.jar $SPARK_HOME/jars/
# 2. 配置 Spark 启用 OmniAdaptor
vim $SPARK_HOME/conf/spark-defaults.conf
在配置文件中添加如下核心参数:
# 启用 Omni 扩展
spark.sql.extensions=com.huawei.boostkit.spark.OmniSparkExtensions
# 启用 Native Shuffle
spark.shuffle.manager=org.apache.spark.shuffle.sort.OmniShuffleManager
# 内存配置(关键)
spark.executor.memory=20g
spark.executor.memoryOverhead=10g # Native 内存通常需要较大的 overhead
4.2 运行 TPC-DS 测试
使用 standard TPC-DS 工具集生成 1TB 数据,并运行 Query 94(典型的大表 Join 与 Aggregation 场景)。
# 提交测试任务
./bin/spark-submit \
--master yarn \
--deploy-mode client \
--driver-memory 20g \
--executor-memory 20g \
--num-executors 10 \
--conf spark.omni.sql.columnar.fusion=true \
--class org.apache.spark.sql.execution.benchmark.TPCDSQueryBenchmark \
tpcds-benchmark_2.12-1.0.jar \
--data-location hdfs://ns1/tpcds/1000g
4.3 性能对比与分析
在 1TB 数据集上,我们对比了 Spark 原生(Native Java)与开启 OmniAdaptor 后的执行时间。
核心指标对比:
|------------------|---------------------|-------------------|----------|-----------------|
| 指标 | Spark Native (Java) | OmniAdaptor (C++) | 提升幅度 | 原因分析 |
| Query 94 总耗时 | 245 s | 82 s | 3.0x | 列式计算减少了指令数 |
| Shuffle 写吞吐 | 1.2 GB/s | 3.5 GB/s | 2.9x | 零拷贝 + 向量化哈希 |
| CPU 利用率 | 85% (高 GC 波动) | 65% (稳定) | 更优 | 移除了 JVM GC 干扰 |
| 内存占用峰值 | 45 GB | 28 GB | -38% | 紧凑的 Native 内存布局 |

分析:
分析来看,原生的 RowBased 算子在执行过程中被 OmniColumnar 算子替换。Shuffle Write 阶段,C++ 实现的 Splitter 利用鲲鹏处理器的 NEON 指令集,将数据分区的计算效率提升了数倍。同时,大部分中间数据直接在 Off-Heap 内存中流转,减少了 JVM Old Gen GC 的干扰,从而显著降低了长尾延迟。
5. 总结与展望
通过深入 OmniAdaptor-master 的源码,我们清晰地看到了 BoostKit 如何通过"硬核"的 C++ 编程技术来解决大数据领域的性能痛点。
OmniAdaptor 不仅仅是一个简单的插件,它是一套完整的异构计算加速方案:
- JNI 层:构建了高效的跨语言调用通道。
- Shuffle 层:重构了数据分发逻辑,利用向量化技术榨干 CPU 性能。
- IO 层:通过零拷贝和列式存储打破存储墙。
对于开发者而言,深入理解 OmniAdaptor 的源码,不仅有助于更好地使用鲲鹏 BoostKit,更能为构建下一代高性能计算引擎提供宝贵的思路。