本文梳理了当前主流 JVM 查询引擎(Presto、Spark)集成 C++ 向量化执行引擎的三种架构模式,从通信机制、Fallback 策略、线程模型、语义兼容性等维度进行对比分析,帮助读者建立选型的思维框架。
为什么 JVM 引擎需要 Native 执行?
大规模数据处理领域的主流引擎——Presto、Spark、Flink——都运行在 JVM 上。JVM 提供了跨平台能力和丰富的生态,但在执行层面临几个根本性的性能限制:
内存布局不可控。JVM 的对象模型带来 header 开销和不可预测的内存布局,难以实现对 CPU cache 友好的 columnar 数据排列。手动管理 off-heap 内存虽然可以绕过这个问题,但增加了工程复杂度。
难以利用硬件指令。SIMD(如 AVX-256/512)是向量化执行的关键加速手段,但在 JVM 中使用 SIMD 需要深入了解 JIT 编译器内部细节,且效果不稳定。正如 Photon 论文所指出的,实现使用 SIMD 指令或 prefetch 数据的 operator,如果不深入了解 JVM 编译器内部实现几乎不可能做到 [1] 。
JIT 编译器的隐性限制。Spark 2.0 引入的 Whole Stage Code Generation 通过将多个 operator 融合成单个函数来消除虚函数调用开销。但 JVM JIT 编译器对方法大小存在内部限制,当 codegen 生成的方法过大时,JVM 优化会 bail out,造成难以诊断的性能悬崖 [1] 。
性能可预测性差。GC 停顿、JIT 编译的 warmup、以及 JVM 多层抽象使得性能分析和调优困难。相比之下,C++ native code 的内存管理和执行行为都在显式控制之下,性能更容易解释和优化 [1] 。
但 JVM 引擎的价值显然不在执行层。Presto 和 Spark 的核心竞争力是:成熟的分布式调度、经过大规模生产验证的 SQL 优化器(如 Spark 的 Catalyst)、庞大的生态系统和用户基础。完全抛弃 JVM 重写一个引擎的成本和风险都太高。
因此,核心问题变成了:如何在保留 JVM 引擎上层能力的同时,把数据密集的执行层下沉到 C++ native 代码? 不同的集成方式本质上是在选择 JVM 与 native 之间的边界画在哪里、以什么方式通信。
三种集成架构模式
模式 A:整体进程替换
代表项目:Prestissimo(Presto C++ Worker)
这是最"彻底"的方案。Presto 的分布式架构天然分为 Coordinator(负责 SQL 解析、优化、调度)和 Worker(负责实际数据处理)两个角色,它们之间通过 REST HTTP 通信。Prestissimo 的思路很直接——用 C++ 重写整个 Worker 进程。
graph TB
subgraph "Java Coordinator"
SQL[SQL 解析 / 优化 / 调度]
end
subgraph "C++ Worker(Prestissimo)"
REST[REST API
Proxygen HTTP]
Translate[Plan Fragment → Velox Plan]
subgraph "Velox Runtime"
D1[Driver 1] --> Pipeline1[Operator Pipeline]
D2[Driver 2] --> Pipeline2[Operator Pipeline]
D3[Driver N] --> PipelineN[Operator Pipeline]
end
end
SQL -- "HTTP: Plan Fragment" --> REST
REST --> Translate
Translate --> D1
Translate --> D2
Translate --> D3
style SQL fill:#e8d5b7
style REST fill:#b7d5e8
style Translate fill:#b7d5e8
style D1 fill:#b7e8c4
style D2 fill:#b7e8c4
style D3 fill:#b7e8c4
图 1:模式 A 架构。Java Coordinator 通过 HTTP 发送 plan fragment 给 C++ Worker。Worker 内部完全由 Velox 接管,多个 Driver 并行执行。JVM-Native 边界在进程间。
Prestissimo 通过 Proxygen C++ HTTP 框架实现了与 Java Worker 完全相同的 REST endpoints。由于 Coordinator 和 Worker 之间的通信协议不变,C++ Worker 对 Coordinator 完全透明——这是一个真正的 drop-in replacement [2] 。
这种架构的一个重要特征是 Worker 节点上不需要 JVM。所有数据处理都在纯 C++ 环境下进行,Velox 可以完全掌控内存管理、线程调度和 CPU 指令的使用。Worker 接收到 Presto plan fragment 后,直接翻译为 Velox plan node tree,创建多个 Driver 并行执行 operator pipeline [3] 。
这里的"并行"不是指多个 Spark task 的并行,而是 Velox 原生的 morsel-driven parallelism——同一个 pipeline 内,多个 Driver 线程可以并行消费不同的数据分片(morsel),并通过 work stealing 实现动态负载均衡。这一调度能力是这种架构独有的优势。
值得注意的是,Prestissimo 方案之所以可行,很大程度上是因为 Presto 的 Coordinator-Worker 架构本身就通过 REST API 做了清晰的进程间隔离。如果原始引擎的组件间耦合更紧密(比如 Spark 的 driver-executor 之间不仅通过网络通信,还共享大量 JVM 层面的抽象),这种整体替换就不那么容易了。
模式 B:JNI In-Process 算子替换
代表项目:Databricks Photon、Apache Gluten + Velox/ClickHouse
与整体替换不同,这种模式选择在 Spark JVM 进程内部 引入 native 执行。Native library 被加载进 JVM 进程,通过 JNI(Java Native Interface)与 Spark 交互。Spark 的 Catalyst optimizer 正常工作不变,但在 physical plan 执行阶段,runtime 会判断哪些 operator 可以在 native 引擎中执行,不支持的 operator 则回退到 JVM。
graph TB
subgraph "Spark JVM 进程"
subgraph "JVM 侧"
Catalyst[Catalyst Optimizer]
PhysPlan[Physical Plan]
Fallback[JVM Fallback
Row-based 执行]
end
subgraph "Native 侧(通过 JNI 加载)"
NativeExec[Native Vectorized
Execution]
OffHeap[(Off-Heap Memory)]
end
Catalyst --> PhysPlan
PhysPlan -- "支持的 Operator" --> NativeExec
PhysPlan -- "不支持的 Operator" --> Fallback
NativeExec <--> |"JNI + 指针"| OffHeap
Fallback <-.-> |"columnar ↔ row 转换"| NativeExec
end
style Catalyst fill:#e8d5b7
style PhysPlan fill:#e8d5b7
style Fallback fill:#e8d5b7
style NativeExec fill:#b7e8c4
style OffHeap fill:#b7e8c4
图 2:模式 B 架构。Native 引擎加载进 JVM 进程,通过 JNI 通信。支持的 operator 走 native 向量化执行,不支持的回退到 JVM。Fallback 边界处发生 columnar ↔ row 格式转换。
Photon 是这种模式的先行者。它作为 native library 被加载进 JVM 进程,Spark 和 Photon 之间通过 JNI 传递指向 off-heap memory 的数据指针。Photon 还与 Spark 的 memory manager 集成,在混合执行计划中协调 spilling [4] 。
但这种模式下的 native 引擎在 线程模型上受到 Spark 的约束。Spark 的 task 模型是 one task = one thread = one partition——Spark 自己决定并行度,通过启动大量 task 来处理不同的 partition。这意味着 native 引擎(无论是 Photon 还是 Velox)在每个 Spark task 内部以单线程运行,Velox 原生的 morsel-driven 并行调度能力在这种模式下基本无法发挥。
紧耦合 vs 松耦合:Photon 和 Gluten 的路线分歧
虽然 Photon 和 Gluten 在架构模式上相同,但它们在耦合策略上做了不同的选择。
graph LR
subgraph "Photon(紧耦合)"
CP1[Catalyst
Physical Plan] --> |直接映射| P[Photon
Operators]
end
subgraph "Gluten(松耦合)"
CP2[Catalyst
Physical Plan] --> |翻译| S[Substrait IR] --> |翻译| V[Velox / ClickHouse]
end
style CP1 fill:#e8d5b7
style P fill:#d5b7e8
style CP2 fill:#e8d5b7
style S fill:#e8e8b7
style V fill:#b7e8c4
图 3:两种耦合策略对比。Photon 直接映射 Catalyst plan 到自有 operator;Gluten 通过 Substrait IR 解耦,支持可插拔后端。
Photon 走紧耦合路线。 Catalyst physical plan 直接映射到 Photon 自有的 operator 实现,无中间表示层。Photon 就是为 Databricks Runtime(DBR,Databricks 的 Spark fork)量身定做的,这让它可以做更深的优化——比如将 sort merge join 替换为 hash join [1] 。但代价是与 DBR 完全绑定,闭源,用户只能在 Databricks 平台上使用。
Gluten 走松耦合路线。 它在 Spark 和 native 引擎之间引入了 Substrait——一种跨语言的标准化查询计划表示——作为中间层。Spark physical plan 先翻译为 Substrait plan,再翻译为底层引擎(Velox 或 ClickHouse)的 native plan [5] 。这带来了 backend 可插拔的灵活性,也让 Gluten 成为一个开源的、多方参与的项目(Apache 顶级项目,Intel 主导,IBM、Meta、ByteDance、Microsoft 等参与贡献)。
Microsoft 的 Fabric 平台已经在用 Gluten + Velox 作为 Spark 的 native execution engine,并进入 public preview [6] 。
这两种策略的取舍可以用下表总结:
| 维度 | Photon(紧耦合) | Gluten + Velox/CH(松耦合) |
|---|---|---|
| Plan 翻译路径 | Physical Plan → Photon Operators | Physical Plan → Substrait → Velox/CH |
| 优化空间 | 更大,可改写 plan 结构 | 受 Substrait 表达能力限制 |
| 与 Spark 的关系 | 与 DBR fork 深度绑定 | 通过 IR 解耦,支持开源 Spark |
| Backend | 仅 Photon | 可插拔(Velox、ClickHouse) |
| 开源性 | 闭源 | Apache Incubating |
| 核心取舍 | 专用优化深度 | 通用可组合性 |
这本质上是系统设计中一个经典的 trade-off:专用系统 vs 通用平台。Photon 可以看作是一个为 Spark 生态深度定制的 native 引擎;Gluten + Velox 则试图建立一个引擎无关的、可复用的加速层。
模式 C:外部进程 Offload
代表项目:Spruce / SparkCpp(Meta 内部)
这是一种工程上最轻量的方案。Meta 内部的 Spruce 项目利用 Spark 已有的 script transform 接口,将执行 offload 到 Spark executor 旁的独立 C++ 进程 [7] 。
graph LR
subgraph "Spark JVM Executor"
Task[Spark Task] --> |"序列化 Plan Fragment"| Pipe[stdin pipe]
end
subgraph "SparkCpp 进程"
Pipe --> Deser[反序列化 +
翻译为 Velox Plan]
Deser --> VeloxRT[Velox Runtime
多 Driver 并行]
VeloxRT --> |"结果序列化"| PipeOut[stdout pipe]
end
PipeOut --> Task
style Task fill:#e8d5b7
style Pipe fill:#e8e8b7
style Deser fill:#b7d5e8
style VeloxRT fill:#b7e8c4
style PipeOut fill:#e8e8b7
图 4:模式 C 架构。Spark executor 通过 pipe 将 plan fragment 序列化传给外部 C++ 进程,C++ 进程用 Velox 执行后返回结果。JVM-Native 边界在进程间,通过 pipe IPC 通信。
Spark 的 script transform 接口原本允许用户在 Spark 中执行任意外部程序。Spruce 利用这个已有接口,将 plan fragment 序列化后通过 pipe(stdin/stdout)传给外部的 SparkCpp 进程。SparkCpp 反序列化后翻译为 Velox plan 并执行 [7] 。
这种方式的优势在于:对 Spark 代码几乎零侵入、C++ 进程拥有独立的地址空间和线程模型、可以完全利用 Velox 的 morsel-driven 并行调度。但代价是 pipe 通信带来的序列化/反序列化开销,以及接口能力的上限——script transform 并非为高性能查询执行设计的。
需要说明的是,Spruce 是 Meta 的内部项目,公开信息主要来源于 Velox 的 VLDB 论文 [7] ,关于其生产规模和具体实现细节的信息有限。
多维度对比
三种模式的差异不仅体现在架构图上,更体现在一系列设计决策的 trade-off 中。
通信机制与数据传输开销
| 模式 | 边界位置 | 通信机制 | 数据传输开销 |
|---|---|---|---|
| A: 进程替换 | 进程间 | REST HTTP | Worker 间本来就有网络通信,C++ 替换的是 Worker 内部执行而非通信层,不增加额外开销 |
| B: JNI 算子替换 | 进程内 | JNI + off-heap pointer | 近似 zero-copy,开销最低 |
| C: 外部进程 | 进程间 | pipe (stdin/stdout) | 序列化/反序列化,有可测量开销 |
模式 B 在数据传输上有明确优势——JNI 调用本身的开销约为几十纳秒量级,且通过传递 off-heap memory 指针可以避免数据拷贝。但需要注意,这个优势建立在 native 侧和 JVM 侧共享同一进程地址空间的基础上,由此带来的内存管理复杂度是隐性成本。
Fallback 策略与粒度
Fallback 策略决定了在 native 引擎尚未完全覆盖所有 operator 时,系统如何优雅地处理不支持的操作。
模式 A 不提供 fallback。 整个 Worker 要么是 C++,要么是 Java,不存在混合执行。如果某个 query 使用了 C++ Worker 不支持的函数或 operator,query 会直接报错。这意味着模式 A 需要较高的 operator coverage 才能投入生产。Prestissimo 目前已经支持了 Presto SQL dialect 的大部分功能,Meta 和 IBM 已将其用于生产环境 [3] 。
模式 B 支持 operator 级别的 fallback。 同一个 query plan 中,支持的 operator 走 native 路径,不支持的回退到 JVM。这让用户可以在 operator coverage 尚不完整时就开始使用 native 加速——query 不会报错,只是部分 operator 没有被加速。
但 fallback 有其隐性代价:每次 native → JVM 或 JVM → native 的切换都涉及 columnar ↔ row 格式的转换。Native 引擎使用 columnar batch(Arrow 格式或类似格式),而 Spark JVM 的 operator 使用 InternalRow。这种格式转换不是免费的,尤其当 fallback 频繁发生时,转换开销可能显著侵蚀 native 执行带来的性能收益。
这也是为什么 Photon 和 Gluten 都在持续扩大 operator coverage——不只是为了功能完整性,更是为了减少 fallback 带来的格式转换开销。
模式 C 的 fallback 粒度介于两者之间——fragment 级别。 整个 plan fragment 要么在 C++ 进程执行,要么不走 offload。
线程模型与 Native 调度能力
这是一个容易被忽视但对性能影响深远的维度。
Velox 内部采用 Driver loop 执行模型:operator 被拍平成一个数组,由 Driver 在扁平循环中依次驱动——而不是 Volcano 模型的递归调用栈。这种设计配合 morsel-driven parallelism,可以实现多 Driver 线程并行消费数据、work stealing、动态负载均衡 [7] 。
但这些调度能力能否发挥,取决于集成模式:
模式 A 和 C(独立进程) 可以完全释放 Velox 的调度能力。C++ 进程拥有独立的线程池,一个 Velox Task 可以创建多个 Driver 线程并行处理 morsel。
模式 B(JNI in-process) 则受到 Spark task 模型的约束。Spark 的并行模型是 one task = one thread = one partition:Spark 自己决定并行度,通过启动大量 task 来处理不同 partition。Native 引擎(Photon 或 Velox)嵌在每个 Spark task 的线程里运行,在每个 task 内部以单线程执行。
这意味着:当某些 partition 存在数据倾斜时,模式 A/C 可以在 Velox 层面通过 work stealing 来缓解;模式 B 只能依赖 Spark 的 AQE(Adaptive Query Execution)来重新划分 partition,native 引擎自身的调度能力发挥不出来。
核心洞察:集成越深入 Spark runtime(JNI in-process),越受其 task 模型约束,native 引擎自身的调度优势越难释放;集成越独立(独立进程),越能发挥 native 调度能力,但集成成本和通信开销更高。
语义兼容性
语义兼容性是所有集成方案面临的共同挑战,但程度因场景而异。
模式 A(Presto 场景) 的兼容性问题最小。Velox 的函数库最初就是按 Presto 语义实现的——类型系统、函数行为、NULL 处理规则都与 Presto 保持一致。很多为 Prestissimo 实现的组件(如 Presto wire protocol、Presto 函数包)今天已经成为 Velox 核心的一部分 [7] 。
模式 B 和 C(Spark 场景) 的挑战更大。Spark 和 Presto 在很多看似相同的函数上存在微妙的语义差异。Velox 官方博客明确指出:LIKE 函数和 Spark 的 split 函数在使用列值(而非常量)作为参数时,可能悄悄产生错误的结果 [8] 。这类 subtle bugs 在日常使用中很容易被忽略,但在大规模数据处理中可能导致严重的正确性问题。
Photon 通过构建大量的交叉测试框架来验证语义一致性:单元测试验证每个 SQL expression 在 Photon 和 Spark 中行为一致,还有专门的 native code 测试框架对 column vectors 进行正确性校验 [1] 。这本身就是巨大且持续的工程投入。
内存管理
| 模式 | 内存管理方式 | 与 JVM GC 的关系 | Spilling 协调 |
|---|---|---|---|
| A: 进程替换 | 完全 native 管理(Velox memory arena) | 无关,Worker 无 JVM | Velox 自主决定 |
| B: JNI 算子替换 | native 侧使用 off-heap,与 Spark memory manager 集成 | 共享进程,需协调 | Photon 深度集成;Gluten 通过 memory registration API 注册 |
| C: 外部进程 | 独立地址空间 | 无关 | 无法跨进程协调 |
模式 B 的内存管理最复杂。Photon 与 Spark memory manager 深度集成,共享 off-heap 内存池,在内存压力下协调 spilling [4] 。Gluten 的做法稍有不同——它通过调用 Spark 的 memory registration API,为每次 native 内存分配/释放动作在 Spark 侧注册,让 Spark 的统一内存管理机制也能感知和管控 native 内存使用 [5] 。
参考资料
- [1] A. Behm et al., “Photon: A Fast Query Engine for Lakehouse Systems,” SIGMOD 2022.
- [2] Presto Documentation, “Presto C++.”
- [3] Presto Blog, “Diving into the Presto Native C++ Query Engine (Presto 2.0),” Jun 2024.
- [4] Databricks Blog, “Announcing Photon Public Preview,” Jun 2021.
- [5] Apache Gluten.
- [6] Microsoft Fabric Blog, “Public Preview of Native Execution Engine for Apache Spark,” Jun 2024.
- [7] P. Pedreira et al., “Velox: Meta’s Unified Execution Engine,” VLDB 2022.
- [8] Velox Blog, “The hidden traps of regex in LIKE and split,” Mar 2026.
- [9] P. Pedreira et al., “The Composable Data Management System Manifesto,” VLDB 2023.
- [10] NVIDIA Developer Blog, “Accelerating Large-Scale Data Analytics with GPU-Native Velox and NVIDIA cuDF,” Oct 2025.