Apache Spark Executor写入小数据时触发OutOfMemoryError问题排查
排查Spark Executor OutOfMemoryError问题求助
配置信息
- Windows机器,Spark 3.1.3 local模式运行
- JVM总内存设置为2g
- Spark executor内存配置:
spark.executor.memory=1g
场景复现
通过Spark SQL查询本地Parquet文件,测试超长记录处理能力,执行查询:
SELECT repeat('A', 50000000) AS test
将查询结果写入CSV文件时,JVM进程因java.lang.OutOfMemoryError: Java heap space崩溃。
疑问点
生成的数据约50MB,远小于1g的executor内存,但堆转储显示该字符数组存在大量副本,内存占用达800MB。而总数据量更大(如200MB)但无超长行的查询可正常运行。
- Spark为单条记录生成大量副本是否正常?
- 如何避免此类OOM?
异常栈信息
Thread 'Executor task launch worker for task 0.0 in stage 6.0 (TID 35)' with ID = 135 java.lang.OutOfMemoryError.<init>(OutOfMemoryError.java:48) java.util.Arrays.copyOf(Arrays.java:3332) com.univocity.parsers.common.input.ExpandingCharAppender.expand(ExpandingCharAppender.java:115) com.univocity.parsers.common.input.ExpandingCharAppender.expand(ExpandingCharAppender.java:123) com.univocity.parsers.common.input.ExpandingCharAppender.append(ExpandingCharAppender.java:207) com.univocity.parsers.csv.CsvWriter.append(CsvWriter.java:405) com.univocity.parsers.csv.CsvWriter.processRow(CsvWriter.java:228) com.univocity.parsers.common.AbstractWriter.submitRow(AbstractWriter.java:352) com.univocity.parsers.common.AbstractWriter.writeRow(AbstractWriter.java:830) org.apache.spark.sql.catalyst.csv.UnivocityGenerator.write(UnivocityGenerator.scala:94) org.apache.spark.sql.execution.datasources.csv.CsvOutputWriter.write(CsvOutputWriter.scala:46) org.apache.spark.sql.execution.datasources.SingleDirectoryDataWriter.write(FileFormatDataWriter.scala:140)
已查阅Spark内存管理资料,尝试缓存数据、仅持久化到磁盘,均未解决问题。
问题分析与解决方案
1. 单条记录产生大量副本是否正常?
不正常,这是Spark写入CSV依赖的Univocity Parsers库的特性导致的:
- 写入超长字符串时,Univocity的
ExpandingCharAppender采用倍增扩容策略:每次空间不足时,会将字符数组容量翻倍(比如从50MB扩容到100MB、200MB...),扩容过程中通过Arrays.copyOf生成原数组的副本,旧数组需等待GC回收。 - 单条50MB的字符串在多次扩容后,内存中会同时存在多个不同容量的数组副本,叠加后占用内存远超原数据大小,这就是堆转储显示800MB占用的原因。
- 短行数据因为扩容增量小,副本叠加的内存占比低,不会触发OOM,所以总数据量更大的短行查询能正常运行。
2. 避免此类OOM的方案
方案一:提前设置CSV Writer的字符缓冲区大小
Univocity允许配置初始缓冲区大小,避免频繁扩容和副本生成。在Spark中可通过参数指定:
// 写入CSV时添加配置 df.write .option("univocity.csv.writer.initial.buffer.size", "52428800") // 50MB,匹配生成的字符串大小 .csv("path/to/output")
或全局配置:
spark.sql.csv.univocity.writer.initial.buffer.size=52428800
此配置会让Writer直接初始化足够大的缓冲区,避免多次扩容,从根源减少数组副本。
方案二:调整内存分配(临时应急)
Local模式下Spark会将部分内存分配给Driver等组件,可适当调高内存配置:
spark.executor.memory=1.5g spark.driver.memory=512m
这只是临时缓解,无法解决副本产生的根本问题。
方案三:业务层面优化
如果业务允许,尽量拆分超长记录为多行;或改用Parquet、ORC等列式存储格式代替CSV,这类格式对大字段的处理更高效,不会出现CSV写入时的扩容副本问题。
方案四:升级Spark版本
Spark 3.2+对Univocity的CSV写入逻辑有优化,比如更智能的缓冲区初始化策略,能减少不必要的扩容操作,升级后可能直接解决该问题。
内容的提问来源于stack exchange,提问作者Yao
相关产品推荐
相关产品推荐

