Spark读取HDFS分区Parquet文件遇GC overhead limit exceeded错误求助
看起来你遇到的问题是Driver在加载大量Parquet文件元数据时内存不足,从错误栈能看到是InMemoryFileIndex在处理文件列表时触发了GC overhead——虽然你的文件总大小只有10GB,但23万+的文件数量加上1200个分区文件夹,会让Driver需要加载巨量的文件路径、属性等元数据,而Spark 2.3的文件列表处理机制在这种场景下效率不高。
下面是几个针对性的解决办法,按优先级排序:
1. 改用分区过滤而非通配符读取
你当前用的路径是hdfs_path_folder/date=2018-03-05/*,这种通配符会让Spark直接扫描所有子文件夹下的文件,Driver需要一次性加载所有23万+文件的元数据。
改为直接读取根路径+SQL过滤分区:
sparkSession.read() .schema(someSchema) .parquet("hdfs_path_folder") .filter("date = '2018-03-05'")
Spark的分区发现机制会自动识别date分区列,只扫描对应分区下的文件,而且元数据处理会更高效——Driver不需要加载所有子文件夹的文件列表,而是利用分区信息精准定位。
2. 调整Driver内存与GC参数
你提到增加Driver内存没用,可能是加的幅度不够,或者GC策略不合适。试试:
- 提升Driver内存到足够大(比如
--driver-memory 16G,根据你的集群资源调整) - 改用G1GC来优化内存碎片化和GC效率:
--driver-java-options "-XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+HeapDumpOnOutOfMemoryError"
G1GC比默认的ParallelGC更适合处理大量小对象(比如文件路径字符串)的场景,能减少GC overhead触发的概率。
3. 开启并行分区发现
Spark 2.3有个参数spark.sql.sources.parallelPartitionDiscovery.threshold,默认值是32——当待扫描的文件数超过这个阈值时,会让Executor并行扫描文件列表,而不是Driver单线程处理。
你可以在提交任务时添加这个参数:
--conf spark.sql.sources.parallelPartitionDiscovery.threshold=1000
这样Driver就不用独自承担加载23万+文件元数据的压力,由Executor分担后,内存占用会大幅降低。
4. 合并小文件(长期解决方案)
23万+的Parquet文件数量确实偏多,即使解决了当前OOM,后续查询也可能因为小文件过多导致任务数激增、调度开销大。建议提前合并对应分区的小文件:
// 读取原分区数据 val df = spark.read.parquet("hdfs_path_folder/date=2018-03-05") // 按总大小设置分区数(比如10GB分成10个分区,每个1GB左右) df.repartition(10) .write.mode("overwrite") .parquet("hdfs_path_folder_merged/date=2018-03-05")
之后读取合并后的路径,文件数大幅减少,Driver加载元数据的压力就消失了。
5. 升级Spark版本(可选)
Spark 2.3是比较老的版本(2018年发布),后续的2.4+版本对InMemoryFileIndex做了很多优化,包括更高效的元数据存储、减少内存占用等。如果业务允许,升级到更高版本的Spark能从根本上避免这类问题。
内容的提问来源于stack exchange,提问作者Farooq Mohammed

