Spark Hive 小文件合并

小文件带来的问题

对于HDFS

从 NN RPC请求角度,文件数越多,读写文件时,对于NN的RPC请求就越多,增大NN压力。

从 NN 元数据存储角度,文件数越多,NN存储的元数据就越大。

对于下游流程

下游流程,不论是MR、Hive还是Spark,在划分分片(getSplits)的时候,都要从NN获取文件信息。这个过程的耗时与文件数成正比,同时受NN压力的影响。在NN压力大,上游小文件多的情况下,下游的getSplits操作就会比较慢。

作业生成的文件数

为了简化问题,假设:

  1. 不考虑一个task写出文件大小的限制,那么一个task对于一个分区(一个目录)只写出一个文件

  2. 没有数据的task不会写出文件

在MR、Spark中,设写出到HDFS的stage中task的个数为T

  • 如果结果表没有分区,或者写出静态分区,则每个Task写出一个文件,那么最多会写出T个文件。

  • 如果结果表有动态分区,不同的分区是写到不同的目录下,令第i个动态分区dp的基数(cardinality)为card(dpi),那么如果k个动态分区,最多写出的文件数为card(dp1) * card(dp2) * ...* card(dpk ) * T。

如何减少作业生成的文件数

所以,控制最终输出的文件个数,可以从以下3个角度入手:

  1. 控制最终stage的task个数,也就是控制整个作业的并行度,具体来讲,可以从最开始单个map输入size,shuffle之后单个reduce的size两方面来控制。

  2. 在写入HDFS之后,计算平均文件大小,merge小文件(但是这种做法只能缓解NN元数据的压力,由于存在写小文件,统计平均文件大小,读小文件、写出大文件这一连串操作,会增加NN RPC的压力,在NN负载高的时候,还会增加作业本身的执行时间)。

  3. 控制最终stage的输入数据划分,让同一个分区的数据,尽量在一个task内。

Map端输入合并

引擎

参数

Spark

spark.hadoopRDD.targetBytesInPartition

spark.hadoop.mapreduce.input.fileinputformat.split.maxsize

spark.hadoop.mapreduce.input.fileinputformat.split.minsize

S

Hive

mapreduce.input.fileinputformat.split.maxsize

mapreduce.input.fileinputformat.split.minsize

mapred.min.split.size.per.node

mapred.min.split.size.per.rack

S

以上分析适用于FileInputFormat(文本格式),对于ORC文件,还需要将hive.exec.orc.split.strategy 设置为ETL

引擎

参数

Spark

spark.hadoopRDD.targetBytesInPartition

spark.hadoop.mapreduce.input.fileinputformat.split.maxsize

spark.hadoop.mapreduce.input.fileinputformat.split.minsize

S

spark.hadoop.hive.exec.orc.split.strategy

ETL

Hive

mapreduce.input.fileinputformat.split.maxsize

mapreduce.input.fileinputformat.split.minsize

mapred.min.split.size.per.node

mapred.min.split.size.per.rack

S

hive.exec.orc.split.strategy

ETL

Shuffle之后合并

无论是Spark还是Hive,在shuffle之后的合并都比较类似,都是根据上游的map的结果size,将多个map的结果合并给下游的一个reducer,具体的参数如下表:

功能

引擎

参数

确定下游合并的size

Spark

spark.sql.adaptive.shuffle.targetPostShuffleInputSize

134217728 (128M)

Hive

hive.exec.reducers.bytes.per.reducer

1G

确定最大reducer个数

Spark

spark.sql.shuffle.partitions

2000

Hive

hive.exec.reducers.max

1009?

写入HDFS之后合并

原理都是统计目录下的平均文件大小,如果小于某个阈值,就再启动一个map job,来合并文件

Hive

相关参数

参数

含义

hive.merge.mapfiles

Merge small files at the end of a map-only job

合并map-only的小文件

true

hive.merge.mapredfiles

Merge small files at the end of a map-reduce job.

合并map-reduce的小文件

false

hive.merge.size.per.task

Size of merged files at the end of the job.

小文件合并之时候,期望一个map的输入大小

256M

hive.merge.smallfiles.avgsize 

When the average output file size of a job is less than this number, Hive will start an additional map-reduce job to merge the output files into bigger files. This is only done for map-only jobs if hive.merge.mapfiles is true, and for map-reduce jobs if hive.merge.mapredfiles is true.

小文件合并的阈值

16M

  • 在决定一个目录是否需要合并小文件的时候,会统计目录下的平均大小,然后与hive.merge.smallfiles.avgsize 比较

  • hive.merge.size.per.task 参数的作用,就是在合并小文件的job中,将mapreduce.input.fileinputformat.split.maxsize 、mapreduce.input.fileinputformat.split.minsize 、mapred.min.split.size.per.node、mapred.min.split.size.per.rack 这4个参数的设置成 hive.merge.size.per.task,最终通过 CombineFileInputFormat 来实现文件合并

  • 在合并文件的时候,如何决定一个map task读多少数据?

    • max(hive.merge.size.per.task, hive.merge.smallfiles.avgsize)

Spark

参数

spark.sql.mergeSmallFileSize 与 hive.merge.smallfiles.avgsize 类似

spark.sql.targetBytesInPartitionWhenMerge 与hive.merge.size.per.task 类似

策略

在决定一个目录是否需要合并小文件的时候,会统计目录下的平均大小,然后与spark.sql.mergeSmallFileSize 比较

在合并文件的时候,如何决定一个map task读多少数据

max(spark.sql.mergeSmallFileSize, spark.sql.targetBytesInPartitionWhenMerge , spark.hadoopRDD.targetBytesInPartition )

Spark为什么有2个参数决定map端的输入

  1. 有时候在作业的最开始的输入,不需要合并小文件,但是作业写出到目标表之后,需要合并文件,所以需要有两个参数把这两种情况加以区分。

  2. spark.hadoopRDD.targetBytesInPartition,来设置最开始的输入输入端的map合并文件大小。

  3. spark.sql.targetBytesInPartitionWhenMerge 与hive.merge.size.per.task类似,是设置额外的合并job的map端输入size。

Spark与Hive参数对比

作用

引擎

参数

触发小文件合并的阈值

Spark

spark.sql.mergeSmallFileSize

Hive

hive.merge.smallfiles.avgsize

合并小文件时候,map的输入size控制

Spark

spark.sql.targetBytesInPartitionWhenMerge

spark.hadoopRDD.targetBytesInPartition

Hive

hive.merge.size.per.task

合并小文件时候,实际的map输入size确定

Spark

max(spark.sql.mergeSmallFileSize, spark.sql.targetBytesInPartitionWhenMerge , spark.hadoopRDD.targetBytesInPartition )

Hive

max(hive.merge.size.per.task, hive.merge.smallfiles.avgsize)

不同分区的数据分布

对于多级动态分区的情况,在写入结果表之前,最好能够根据分区进行distribute by一下,按照分区重新shuffle,让同一分区的数据,集中在一个task里面。比如有三个动态分区dp1, dp2, dp3,就distribute by dp1, dp2, dp3

参考

http://flyingdutchman.iteye.com/blog/1876400​​

http://blog.javachen.com/2013/09/04/how-to-decide-map-number.html​​

https://www.cnblogs.com/yurunmiao/p/4282497.html​​

https://blog.csdn.net/wawmg/article/details/17095125​​


 

### 使用 Spark 合并 HDFS 上的小文件 为了有效解决 HDFS 中小文件过多的问题,可以通过 Apache Spark 来实现小文件合并。以下是基于 Spark 的解决方案,涵盖了主要的技术细节和注意事项。 #### 配置 Spark Session 在开始之前,需要创建一个 SparkSession 并对其进行必要的配置。以下是一个典型的 SparkSession 初始化代码示例: ```java import org.apache.spark.sql.SparkSession; import org.apache.spark.sql.SaveMode; public class MergeFilesApplication { public static void main(String[] args) { // 设置 Hadoop 用户名 System.setProperty("HADOOP_USER_NAME", "hdfs"); // 创建 SparkSession SparkSession sparkSession = SparkSession.builder() .config("spark.scheduler.mode", "FAIR") // 配置调度模式为公平调度 .config("spark.sql.warehouse.dir", "/warehouse/tablespace/external/hive") // 配置 Hive 仓库目录 .appName("MergeFilesApplication") .getOrCreate(); } } ``` 上述代码初始化了一个 SparkSession,并设置了调度模式和其他必要参数[^1]。 --- #### 设置动态分区调整参数 为了优化性能,在实际应用中通常会启用 Spark 的自适应执行(Adaptive Query Execution, AQE)。AQE 可以自动调整 shuffle partition 数量,从而减少不必要的小文件生成。以下是常用的配置参数: | 参数 | 描述 | |------|------| | `spark.sql.shuffle.partitions` | 初始 shuffle 分区数,默认值为 200 | | `spark.sql.adaptive.enabled` | 是否启用 AQE 功能 | | `spark.sql.adaptive.shuffle.targetPostShuffleInputSize` | 每个 reducer 的目标输入数据大小(字节) | | `spark.sql.adaptive.shuffle.targetPostShuffleRowCount` | 每个 reducer 的目标记录数 | | `spark.sql.adaptive.minNumPostShufflePartitions` | 自动调整后的最小分区数 | | `spark.sql.adaptive.maxNumPostShufflePartitions` | 自动调整后的最大分区数 | 可以在 Spark SQL 中通过以下方式设置这些参数: ```sql SET spark.sql.shuffle.partitions=10; SET spark.sql.adaptive.enabled=true; SET spark.sql.adaptive.shuffle.targetPostShuffleInputSize=128000000; -- 单位为字节 SET spark.sql.adaptive.shuffle.targetPostShuffleRowCount=10000000; SET spark.sql.adaptive.minNumPostShufflePartitions=1; SET spark.sql.adaptive.maxNumPostShufflePartitions=100; ``` 这些参数有助于更好地控制分区数量,避免因分区过少或过多而产生的问题[^2]。 --- #### 数据读取与合并 假设 HDFS 上有一个包含大量小文件的目录 `sourceDir`,可以使用以下代码将其合并成较少的大文件: ```java // 读取 Parquet 文件 Dataset<Row> dataFrame = sparkSession.read().parquet(sourceDir); // 减少分区数以合并小文件 int partitions = 10; // 根据需求设定合适的分区数 dataFrame.coalesce(partitions) .sortWithinPartitions("col1", "col2") // 如果需要按列排序 .write() .mode(SaveMode.Overwrite) // 覆盖已有数据 .option("compression", "gzip") // 压缩算法 .parquet(targetMergedDir); // 输出到目标目录 ``` 在此过程中,`coalesce` 方法用于减少分区数,从而降低输出文件的数量。如果希望进一步提升效率,还可以结合 `repartition` 或者 `sortWithinPartitions` 进行操作[^3]。 --- #### 解决文件覆盖冲突 在某些情况下,直接写入目标目录可能导致 `file already exists` 错误。为了避免这一问题,可以通过以下两种方式进行处理: 1. **强制覆盖**:将 Spark 配置参数 `spark.hadoop.validateOutputSpecs` 设为 `false`,允许覆盖已存在的目录。 ```scala spark.conf.set("spark.hadoop.validateOutputSpecs", "false") ``` 2. **删除旧文件后再写入**:先手动清理目标目录中的内容再进行写入操作。 ```java import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; FileSystem fs = FileSystem.get(new Configuration()); Path outputPath = new Path(targetMergedDir); if (fs.exists(outputPath)) { fs.delete(outputPath, true); // 删除整个目录 } ``` 这两种方法都可以有效解决问题,但需要注意潜在的风险,例如意外删除重要数据[^4]。 --- ### 注意事项 - 在大规模生产环境中,建议合理规划分区策略,避免过度集中化导致性能瓶颈。 - 若原始数据格式并非 Parquet,可以根据实际情况替换为 CSV、JSON 等其他支持的格式。 - 当前方案适用于静态数据场景;如果是流式数据,则需采用不同的设计思路。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值