为何Spark读取目录比读取文件路径列表快得多?
问题描述
我在S3中有一个包含数百万个小文件的目录,这些文件均小于10MB且为GZ格式,我知道这对Spark来说效率较低。我运行一个简单的批处理作业将这些文件转换为Parquet格式,尝试了两种读取方式:
第一种方式:
spark.read.csv("s3://input_bucket_name/data/")
第二种方式:
spark.read.csv("file1", "file2"... "file8million")
其中列表中的每个文件都位于同一存储桶和子文件夹中。
两种方式的表现差异:
- 传入整个目录时,驱动程序的文件索引启动延迟较短(批处理开始前约20分钟),Spark UI中20分钟后仅显示一个转换任务。
- 传入单个文件名列表时,索引时间增至2小时以上,转换作业在UI中要等这段时间后才会显示。文件列表模式下有两个任务:(1) 首先是列出800万个文件的叶子节点,(2) 之后是转换作业。
我想了解出现这种情况的原因,底层读取API是否存在差异导致该行为?
原因分析
这两种读取方式的底层处理逻辑存在显著差异,直接导致了索引时间的巨大差距:
- 目录模式:利用S3批量列表API高效处理
当传入S3目录路径时,Spark依赖Hadoop S3A客户端调用S3的ListObjectsV2批量列表API:
- 通过目录前缀一次性过滤出目标路径下的所有文件,支持分页批量返回结果,大幅减少API调用次数。
- 批量获取所有文件的元数据(大小、修改时间等),网络往返开销被压缩到最低。
- 整个列表与校验过程完成后直接进入数据转换阶段,因此Spark UI中只显示一个转换任务,前置索引耗时仅约20分钟。
- 单个文件列表模式:逐个校验的冗余开销
当传入数百万个独立文件路径时,Spark会对每个路径执行独立操作:
- 针对每个文件路径,Spark都会调用S3的
HeadObjectAPI单独校验文件存在性、获取元数据。800万个文件意味着800万次独立API请求,网络开销呈数量级增长。 - 驱动端需要逐个处理每个文件的校验结果,内存与CPU负载大幅提升,导致索引时间拉长至2小时以上。
- Spark UI中显示的“列出800万个文件的叶子节点”任务,正是驱动端逐个校验文件路径的过程,只有完成这一步才会启动实际的转换任务,因此呈现两个阶段的任务流程。
额外说明:即使所有文件都在同一目录下,传入单个文件列表也无法复用S3的批量列表优化——Spark会将每个路径视为独立资源,而非同一目录下的集合,完全浪费了对象存储的特性优势。
内容的提问来源于stack exchange,提问作者mitriola
相关产品推荐
相关产品推荐

