如何控制输出文件的大小?

时间:2016-08-28 02:57:16

标签: apache-spark parquet

在spark中,控制输出文件大小的最佳方法是什么。例如,在log4j中,我们可以指定最大文件大小,然后文件旋转。

我正在寻找类似的镶木地板文件解决方案。编写文件时是否有可用的最大文件大小选项?

我的解决方法很少,但没有一个是好的。如果我想将文件限制为64mb,那么一个选项是重新分区数据并写入临时位置。然后使用临时位置中的文件大小将文件合并在一起。但是获取正确的文件大小很困难。

5 个答案:

答案 0 :(得分:25)

Spark无法控制Parquet文件的大小,因为内存中的DataFrame需要在写入磁盘之前进行编码和压缩。在此过程完成之前,无法估计磁盘上的实际文件大小。

所以我的解决方案是:

  • 将DataFrame写入HDFS,df.write.parquet(path)
  • 获取目录大小并计算文件数

    val fs = FileSystem.get(sc.hadoopConfiguration)
    val dirSize = fs.getContentSummary(path).getLength
    val fileNum = dirSize/(512 * 1024 * 1024)  // let's say 512 MB per file
    
  • 读取目录并重新写入HDFS

    val df = sqlContext.read.parquet(path)
    df.coalesce(fileNum).write.parquet(another_path)
    

    请勿重复使用原始df,否则会触发您的作业两次。

  • 删除旧目录并重新命名新目录

    fs.delete(new Path(path), true)
    fs.rename(new Path(newPath), new Path(path))
    

此解决方案的缺点是需要将数据写入两次,这会使磁盘IO加倍,但目前这是唯一的解决方案。

答案 1 :(得分:1)

正如其他人所提到的,你无法明确地达到每个文件的目标大小。但是,您可以使所有输出文件具有大约相同的行数。如果你平均知道你的压缩率是什么样的,那么在输出文件中均匀分布行数达到max_rows将使你获得关于目标的一致大小。

如果你在写作之前做了分区,这说起来容易做起来难。这是我们如何做的一些伪代码:

-- #3 distribute partitionC's rows based on partitions plus random integer that pertains to file number
select * from dataframe_table as t4
inner join

    -- #2 calculate the number of output files per partition
    ((select t1.partitionA, t1.partitionB, cast(t2.partition_num_rows / max_rows as int) + 1 as partition_num_files from dataframe_table) as t1
        inner join 

        -- #1 determine number of rows in output partition
        (select partitionA, partitionB, count(*) as partition_num_rows from dataframe_table group by (partitionA, partitionB)) as t2
        on t1.partitionA = t2.partitionA and t1.partitionB = t2.partitionB) as t3

on t3.partitionA = t4.partitionA and t3.partitionB=t4.partitionB
distribute by (t4.partitionA, t4.partitionC, floor(rand() * t3.partition_num_files)) sort by (partitionC, sortfield)

我在这里对分区进行了排序,因为在我们的用例中,这极大地改善了压缩,同时对性能的影响最小。

如果第1步和第2步的结果足够小,Spark可以广播加入它们以加快它们的速度。

答案 2 :(得分:1)

在考虑目标文件大小,内存使用量和执行时间时,这是我最理想的方法。这些文件还包括快速压缩和字典编码。

我的HDFS块大小为128兆(128 * 1024 * 1024):

<property>
    <name>dfs.blocksize</name>
    <value>134217728</value>
</property>

这是我最后的镶木地板文件,它们都非常接近hdfs块的大小。

133916650 2018-07-06 07:05 /year=2018/month=01/HoldingDetail_201801_0001.parquet
133459404 2018-07-06 07:05 /year=2018/month=01/HoldingDetail_201801_0002.parquet
133668445 2018-07-06 07:05 /year=2018/month=01/HoldingDetail_201801_0003.parquet
134004329 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0004.parquet
134015650 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0005.parquet
132053162 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0006.parquet
132917851 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0007.parquet
122594040 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0008.parquet

这就是我的方法。

A。提出一些粗略的行,以生成一堆10兆左右范围内的小木地板文件。就我而言,我选择了200,000条记录。许多较小的实木复合地板文件比一个大型实木复合地板文件更节省空间,因为如果单个文件中的数据种类更多,则字典编码和其他压缩技术将被放弃。一次写大约10兆也可以释放内存。

您的文件将如下所示:

07916650 2018-07-06 07:05 /year=2018/month=01/HoldingDetail_201801_0001.parquet
12259404 2018-07-06 07:05 /year=2018/month=01/HoldingDetail_201801_0002.parquet
11368445 2018-07-06 07:05 /year=2018/month=01/HoldingDetail_201801_0003.parquet
07044329 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0004.parquet
13145650 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0005.parquet
08534162 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0006.parquet
12178451 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0007.parquet
11940440 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0008.parquet
09166540 2018-07-06 07:05 /year=2018/month=01/HoldingDetail_201801_0009.parquet
12594044 2018-07-06 07:05 /year=2018/month=01/HoldingDetail_201801_0010.parquet
11684245 2018-07-06 07:05 /year=2018/month=01/HoldingDetail_201801_0011.parquet
07043129 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0012.parquet
13153650 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0013.parquet
08533162 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0014.parquet
12137851 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0015.parquet
11943040 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0016.parquet

B。创建所有较小的镶木地板文件的列表,文件大小加在一起时不超过HDFS块大小。在上面的示例中:

/year=2018/month=01/HoldingDetail_201801_0001.parquet
to
/year=2018/month=01/HoldingDetail_201801_0012.parquet
plus
/year=2018/month=01/HoldingDetail_201801_0014.parquet

占用133,408,651字节。

C。打开一个名为HoldingDetail_201801_temp.parquet的新文件

一次读取列表中所有较小的文件,并将它们作为拼合ROW GROUP写入临时文件。将每个文件作为行组写入非常重要,这样可以保留压缩编码并确保写入的字节数(减去架构元数据)与原始文件大小相同。

删除列表中所有较小的文件。 将临时文件重命名为HoldingDetail_201801_0001.parquet。

对剩余的较小文件重复步骤B和C,以创建* _0002.parquet,* _ 0003.parquet,* _ 0004.parquet等,这些文件将是目标文件,大小仅在hdfs块大小以下。

(我还添加了一项检查,如果文件大小之和> 0.95 * dfs.blocksize,则继续合并找到的文件)

答案 3 :(得分:0)

这是我的解决方案,对我来说很有趣。

val repartition_num = 20  
val hqc = new org.apache.spark.sql.hive.HiveContext(sc)
val t1 = hqc.sql("select * from customer")

// 20 parquet files will be generated in hdfs dir
// JUST control your file with partition number
t1.repartition(repartition_num ).saveAsParquetFile(parquet_dir)

这就是结果:

> hadoop fs -ls /tpch-parquet/customer/*.parquet  | wc -l
20

答案 4 :(得分:0)

Spark中还没有“滚动特定大小”选项,但是最好的秒数是:滚动特定数量的记录。

Spark 2.2起,就可以设置maxRecordsPerFile

另请参阅https://stackoverflow.com/a/48143315/630269