如何整合spark SQL查询的结果以避免大量小文件/避免空文件

时间:2017-10-25 12:24:10

标签: apache-spark hdfs apache-spark-sql

上下文:在我们的数据管道中,我们使用spark SQL来运行从我们的最终用户提供的大量查询作为我们随后参数化的文本文件。

情况

我们的查询如下:

INSERT OVERWRITE TABLE ... PARTITION (...)

SELECT 
  stuff
FROM
  sometable

问题是,当您查看结果时,不是创建一组大小为最大块大小的文件,而是创建200个小文件(因为默认情况下spark会创建200个分区)。 (对于某些查询,取决于输入数据和SELECT查询,200个读取数百万)。许多小文件使我们不受我们的系统管理员的欢迎。

尝试修复(不起作用)

大量文档表明,在这种情况下,您应该使用DISTRIBUTE BY以确保给定分区的所有数据都转到同一个分区,所以让我们尝试类似:

INSERT OVERWRITE TABLE ... PARTITION (...)

SELECT 
  stuff
FROM
  sometable
DISTRIBUTE BY
  1

那么为什么这不起作用(在火花2.0和火花2.2上测试过)?它确实将所有数据成功发送到一个reducer - 所有实际数据都在一个大文件中。但它仍然会创建200个文件,其中199个是空的! (我知道我们应该DISTRIBUTE BY我们的分区列,但这是为了提供最简单的示例)

修复确实有效,但不适合我们的用例

可以使用coalescepartition来做到这一点,因此(在pyspark语法中):

select = sqlContext.sql('''SELECT stuff FROM sometable''').coalesce(1)
select.write.insertInto(target_table, overwrite=True)

但我不想这样做,因为我们需要彻底改变用户向我们提供查询的方式。

我也看到我们可以设置:

conf.set("spark.sql.shuffle.partitions","1");

但我还没试过这个,因为我不想强迫(相当复杂的)查询中的所有计算都发生在一个reducer上,只发生在最终写入磁盘的那个。 (如果我不担心这个,请告诉我!)

问题

  • 仅使用 spark SQL语法,如何编写尽可能少写入文件的查询,并且不会创建大量空/小文件?

可能相关:

3 个答案:

答案 0 :(得分:1)

  

(我知道我们应该分配我们的分区列,但这是为了提供最简单的例子)

所以似乎我试图简化事情是我出错的地方。如果我DISTRIBUTE BY实际列而不是人工1(即DISTRIBUTE BY load_date或其他),那么它不会创建空文件。为什么?谁知道......

(这也与this帖子上的merge-multiple-small-files-in-to-few-larger-files-in-spark回答相匹配)

答案 1 :(得分:0)

从spark 2.4开始,您可以向查询添加提示以合并并重新分区最终的Select。例如:

INSERT OVERWRITE TABLE ... PARTITION (...) 
SELECT /*+ REPARTITION(5) */ client_id, country FROM mytable;

这将生成5个文件。

在Spark 2.4之前,并且可能会对查询性能产生影响,因此您可以将spark.sql.shuffle.partitions设置为所需文件的数量。

答案 2 :(得分:0)

有一段时间这对我来说确实是一个令人讨厌的问题,我花了一段时间才解决。

以下 2 种方法对我有用:

  1. 作为直线脚本在外部运行:
     set hive.exec.dynamic.partition.mode=nonstrict;
     set hive.merge.mapfiles=true;
     set hive.merge.mapredfiles=true;
     set hive.merge.smallfiles.avgsize=64512000;
     set hive.merge.size.per.task=12992400;
     set hive.exec.max.dynamic.partitions=2048;
     set hive.exec.max.dynamic.partitions.pernode=1024;

     <insert overwrite command>

这种方法的问题是这在 pyspark 内部不起作用,并且可以从 python 脚本中作为外部直线脚本运行

  1. 使用重新分区

我发现这个选项非常好。 Repartition(x) 允许将 pyspark 数据帧的记录压缩为“x”个文件。

现在,由于表大小各不相同(例如,我不想将包含 1000 万条记录的表重新分区为 1),因此无法想出一个静态数字“x”来重新分区每个表,因此我愿意以下。

-> set an upper threshold for the max number of records a partition should hold 
I use 100,000 

-> compute x as : 
x = df.count()/max_num_records_per_partition
In case the table is partitioned,I use df_partition instead of df...i.e for every set of partition values, i filter df_partition from df; and then compute x from df_partition

-> repartition as:
df = df.repartition(x)
In case if the table is partitioned; i use df_partition = df_partition.repartition(x)

-> insert overwrite dataframe 

这种方法对我来说很方便。

更进一步,表中的列数和用于每列的数据类型可用于创建权重,这些权重可用于更有效地估计给定数据帧的重新分区数。 (例如,与具有 5 列相同类型的数据帧相比,具有 20 列的数据帧将获得更高的权重;与具有类型为 1 列的数据帧相比,具有类型 Map 的 1 列数据帧将获得更高的权重布尔值