Spark Tungsten 是 Spark 自 1.4 版本引入的革命性执行引擎优化项目,旨在突破 JVM 对象模型和垃圾回收(GC)的限制,充分利用现代 CPU 和内存的特性,显著提升 Spark 应用程序的执行效率。它是 Spark SQL、DataFrames、Datasets 以及 RDDs 计算后端性能飞跃的关键。
Tungsten 的核心目标:
- 减少内存占用和 GC 开销: 传统 JVM 对象(如
String
, 包装类型Integer
,Long
,复杂数据结构)本身有巨大的内存开销(对象头、对齐填充等),且频繁创建/销毁会导致严重的 GC 停顿。 - 提升缓存局部性: 传统对象在堆内存中布局分散,不利于 CPU 缓存高效利用(缓存未命中率高)。
- 利用现代 CPU 特性: 优化代码以更好地利用流水线、分支预测和 SIMD(单指令多数据流)指令。
- 减少数据移动: 在内存中直接操作紧凑的二进制格式数据,避免不必要的序列化/反序列化。
Tungsten 的主要优化技术:
-
内存管理和二进制格式 (Off-Heap & On-Heap Unsafe):
- 堆外内存管理 (Off-Heap): Tungsten 可以直接在 JVM 堆之外分配和管理内存(使用
sun.misc.Unsafe
API)。这块内存不受 JVM GC 管理。 - 堆内二进制格式 (On-Heap Unsafe): 即使在堆内,Tungsten 也不再使用标准的 Java 对象来表示数据,而是使用自己设计的紧凑二进制格式,将数据存储在
byte[]
数组中。 - 优势:
- 极低的内存开销: 二进制格式消除了 JVM 对象头和字段对齐填充的开销,通常可以节省 2-5 倍甚至更多的内存空间(例如,一个 Java
Integer
对象需要 16 字节,而 Tungsten 中一个 int 只需要 4 字节)。 - 显著减少 GC 压力: 数据表示为长寿命的、大块的
byte[]
数组(或堆外的连续内存块),而不是海量的小对象。这极大地减少了需要 GC 扫描和回收的对象数量,大幅降低了 GC 频率和停顿时间,尤其对于大数据量和长任务链至关重要。 - 缓存友好: 连续存储的二进制数据可以被高效地加载到 CPU 缓存行(Cache Line)中,大大提高了缓存局部性,减少了 CPU 等待数据的时间(缓存未命中)。
- 极低的内存开销: 二进制格式消除了 JVM 对象头和字段对齐填充的开销,通常可以节省 2-5 倍甚至更多的内存空间(例如,一个 Java
- 堆外内存管理 (Off-Heap): Tungsten 可以直接在 JVM 堆之外分配和管理内存(使用
-
全阶段代码生成 (Whole-Stage Code Generation):
- 传统火山模型 (Volcano Iterator Model) 的问题: Spark 早期执行引擎(及大多数传统数据库)采用火山模型。查询计划被分解成多个算子(如 Filter, Project, HashAggregate, Sort)。每个算子实现为一个迭代器接口 (
next()
),处理一行数据时,需要经历多次虚函数调用(算子间传递)、条件判断、中间结果创建等。 - 全阶段代码生成: Tungsten 分析整个执行计划(一个 Stage 内的连续算子链),将这些算子融合(Fuse) 在一起,动态生成高度优化的、针对特定查询的单循环字节码(通常使用 Janino 编译器即时编译成 JVM 字节码)。
- 优势:
- 消除虚函数调用开销: 生成的代码是“内联”的,没有算子间的
next()
方法调用开销。 - 减少中间数据结构和条件判断: 直接在一个循环内处理数据,避免了为中间结果创建临时对象和传递控制流的开销。
- 利用 CPU 寄存器/缓存: 生成的代码可以将常用变量保存在 CPU 寄存器中,访问更快。
- 启用 SIMD 优化: 在某些情况下(如处理基本类型的数组),生成的代码可能利用 JVM 的 JIT 编译器(如 C2)自动向量化(SIMD)指令,在单个 CPU 指令中处理多个数据元素。
- 整体效果: 通常能带来数倍的性能提升,尤其是在涉及复杂表达式计算、聚合、Join 等 CPU 密集型操作时。
- 消除虚函数调用开销: 生成的代码是“内联”的,没有算子间的
- 传统火山模型 (Volcano Iterator Model) 的问题: Spark 早期执行引擎(及大多数传统数据库)采用火山模型。查询计划被分解成多个算子(如 Filter, Project, HashAggregate, Sort)。每个算子实现为一个迭代器接口 (
-
缓存敏感计算 (Cache-aware Computation):
- Tungsten 算法在设计时考虑了 CPU 缓存层次结构(L1/L2/L3)的大小和行为。
- 优化示例 - Shuffle:
- Tungsten Sort Shuffle (默认): 取代了早期的
HashShuffle
。它在 Shuffle 的 Map 端对输出数据进行排序并写入单个顺序文件。 - 缓存敏感排序: 排序算法(如 TimSort)的实现会尽量利用 CPU 缓存。例如,当排序小数组时,使用适合 L1 缓存的插入排序;对大数组使用归并排序,并在合并时考虑缓存行大小。
- 减少随机 I/O: 写入单个顺序文件比
HashShuffle
写入大量小文件(每个分区一个文件 * 每个 Reduce 任务)效率高得多,减少了磁盘寻址和文件句柄开销。
- Tungsten Sort Shuffle (默认): 取代了早期的
- 优化示例 - Join 和 Aggregation: Hash Join 和 Hash Aggregation 使用的哈希表实现也经过优化,以减少缓存未命中。
Tungsten 如何影响开发者?
- 自动启用: 对于使用 DataFrames / Datasets API 或 Spark SQL 编写的代码,Tungsten 优化是自动启用的,开发者通常无需做额外工作即可享受其带来的性能红利。这是使用结构化 API 相对于 RDD API 的主要性能优势之一。
- RDD API 的有限支持: 虽然 RDD API 的计算后端也受益于 Tungsten 的内存管理(数据存储更紧凑,GC 更好),但它无法利用最强大的全阶段代码生成优化。因为 RDD 的
compute
函数是用户定义的(通常是 Scala/Java 函数对象),Spark 无法在运行时分析其逻辑并生成融合的优化代码。要获得最佳性能,建议尽可能使用 DataFrame/Dataset API。 - 序列化: Tungsten 使用自己的高效二进制序列化器(基于
Unsafe
)在内部传输数据(例如 Shuffle 数据),这比标准的 Java 序列化或 Kryo(在配置得当的情况下)通常更快、更紧凑。Encoders
在 Dataset API 中扮演了关键角色,它们定义了如何将 JVM 对象与 Tungsten 的二进制格式相互转换。 - 监控: Spark UI 的 SQL 选项卡会显示物理计划,并用
*
标记启用了全阶段代码生成的算子(如*Project
,*Filter
,*HashAggregate
),方便开发者确认优化是否生效。
总结:
Spark Tungsten 执行引擎通过三大核心技术:
- 紧凑的二进制内存格式(堆内/堆外): 大幅降低内存占用和 GC 开销,提升缓存局部性。
- 全阶段代码生成: 将查询计划中的多个算子融合,生成针对特定查询优化的单循环代码,消除虚函数调用和中间结构开销,充分利用 CPU 流水线和 SIMD。
- 缓存敏感算法: 优化 Shuffle(排序)、Join、聚合等核心操作的实现,减少缓存未命中和 I/O 开销。
这些优化使得 Spark 能够更高效地利用现代硬件(CPU、内存、缓存),在处理大规模数据时获得显著的性能提升(通常数倍甚至更高)。优先使用 DataFrame/Dataset API 是充分利用 Tungsten 所有优势(尤其是代码生成)的最佳实践。 Tungsten 是 Spark 能够高效处理 PB 级数据的关键基石之一。