Spark写入Parquet报OutOfMemoryException:宽表单文件写入配置优化求助
我之前处理过类似的超宽表(500列)+大体积数据单文件写入场景,你的OOM问题本质是coalesce(1)把所有数据压到单个Executor后,Parquet的默认内存适配逻辑没跟上宽表+高基数字符串的内存开销——虽然Parquet确实只在内存缓存一个行组,但宽表的行组内存占用会比磁盘大小高2-3倍(字符串对象、字典索引等额外开销),再加上500列的累加效应,很容易撑爆16G的Executor内存。
结合你的16G内存限制和大量字符串的特点,以下是经过实践验证的合理配置:
一、Parquet核心写入配置调整
这些配置直接控制Parquet写入时的内存占用:
1. 缩小行组(Row Group)大小
默认的128MB行组是针对窄表优化的,宽表下内存中缓存的行组实际占用会远超128MB(比如500列的字符串行,内存开销可能是磁盘的3倍以上)。建议把行组大小降到32MB,这样单个行组的内存占用能控制在100MB以内,给Executor留足够的冗余空间:
// 全局配置或写入时指定 spark.conf.set("spark.sql.parquet.rowGroupSize", "33554432") // 32MB对应的字节数
2. 精细化控制字典编码
大量字符串是OOM的重灾区:如果字符串基数极高(比如每个值都唯一),默认的全局字典编码会导致字典无限膨胀,直接占满内存。正确的做法是:
- 全局开启字典(对低基数列比如枚举、分类字段友好),但针对高基数字符串列手动禁用字典
- 给字典设置内存上限,超过后自动禁用该列的字典编码
配置示例:
// 全局配置字典内存上限为1GB spark.conf.set("spark.sql.parquet.dictionary.maxMemory", "1073741824") // 写入时针对高基数列禁用字典(示例列名为long_unique_string) df.write .option("enableDictionary", "true") // 全局默认开启 .option("enableDictionary.long_unique_string", "false") // 指定列禁用 .parquet("/path/to/output")
3. 调小页(Page)大小
Parquet的页是读写的最小单位,默认1MB的页对于宽表字符串列来说,单页缓存的内存开销依然不小。建议降到256KB,减少单次缓存的数据量:
spark.conf.set("spark.sql.parquet.pageSize", "262144") // 256KB对应的字节数
二、Spark Executor内存适配配置
除了Parquet本身,还要调整Spark的内存分配策略,让更多内存用于Parquet写入的执行逻辑:
1. 增大堆外内存开销
字符串对象的内存、Parquet的JNI缓冲等可能占用堆外内存,默认的10%堆外内存(16G对应1.6G)可能不够,建议调到3GB:
spark.conf.set("spark.executor.memoryOverhead", "3072") // 单位为MB
2. 调整内存分配比例
Spark默认把一半内存分给存储(缓存)、一半分给执行,对于单文件写入场景,存储内存几乎用不上,建议把存储内存占比降到30%,给执行内存(Parquet写入的内存池)留更多空间:
spark.conf.set("spark.memory.storageFraction", "0.3")
三、额外优化建议
- 压缩选择Snappy:Snappy压缩率适中,速度快,不会增加太多CPU开销,同时能减少磁盘写入的压力(间接降低内存缓冲的持续时间):
df.write.option("compression", "snappy").parquet("/path/to/output") - 改用repartition(1)替代coalesce(1):coalesce(1)是直接把所有分区的数据拉到一个Executor,可能导致数据倾斜;repartition(1)会通过shuffle把数据均匀分配到单个Executor,内存压力更均衡。
验证方法
在写入时打开Spark UI,观察Executor的内存使用情况:
- 查看
Executor标签页的Used Heap,确保峰值不超过14GB(留2GB冗余) - 查看
Storage标签页,确认没有不必要的缓存占用内存
内容的提问来源于stack exchange,提问作者Joha

