使用Spark的partitioningBy方法对S3中的大型偏斜数据集进行分区

时间:2018-10-28 23:52:01

标签: apache-spark apache-spark-sql partitioning

我正在尝试使用Spark将大的分区数据集写到磁盘,而partitionBy算法在我尝试的两种方法中都遇到了麻烦。

这些分区严重倾斜-有些分区很大,有些很小。

问题1

当我在repartitionBy之前使用重新分区时,Spark将所有分区写为单个文件,即使是大文件也是如此

 
val df = spark.read.parquet("some_data_lake")
df
  .repartition('some_col).write.partitionBy("some_col")
  .parquet("partitioned_lake")

这将永远执行,因为Spark不会并行编写大型分区。如果其中一个分区具有1TB的数据,Spark将尝试将整个1TB的数据作为单个文件写入。

问题2

当我不使用repartition时,Spark会写出太多文件。

此代码将写出疯狂的文件。

df.write.partitionBy("some_col").parquet("partitioned_lake")

我在一个很小的8 GB数据子集上运行了此操作,Spark写入了85,000多个文件!

当我尝试在生产数据集上运行它时,一个包含1.3 GB数据的分区被写为3,100个文件。

我想要的

我希望每个分区都写成1 GB文件。因此,具有7 GB数据的分区将作为7个文件被写出,而具有0.3 GB数据的分区将作为单个文件被写出。

我最好的前进道路是什么?

2 个答案:

答案 0 :(得分:2)

最简单的解决方案是在repartition中添加一列或多列并显式设置分区数。

val numPartitions = ???

df.repartition(numPartitions, $"some_col", $"some_other_col")
 .write.partitionBy("some_col")
 .parquet("partitioned_lake")

其中:

  • numPartitions-应该是写入分区目录的所需文件数的上限(实际数字可以更低)。
  • $"some_other_col"(和可选的附加列)应具有高基数,并且独立于$"some_column(这两者之间应具有功能依赖性,并且不应高度相关)。

    如果数据不包含此类列,则可以使用o.a.s.sql.functions.rand

    import org.apache.spark.sql.functions.rand
    
    df.repartition(numPartitions, $"some_col", rand)
      .write.partitionBy("some_col")
      .parquet("partitioned_lake")
    

答案 1 :(得分:2)

Nick Chammas 方法的替代方法是创建一个由主分区键分区的 row_number() 列,然后将其除以您希望在每个分区中出现的确切记录数。用 SPARK SQL 表示如下:

SELECT /*+ REPARTITION(id, file_num) */
  id,
  FLOOR(ROW_NUMBER() OVER(PARTITION BY id ORDER BY NULL) / rows_per_file) AS file_num
FROM skewed_data

这样做的额外好处是,它允许您通过在辅助键上使用 ORDER BY 子句,将大部分数据并置在一个分区中的多个文件中。如果与辅助键关联的行号跨越两个 file_num 值,则不能保证辅助键位于同一位置。也有可能,实际上也有可能,最终得到一个文件,每个分区中的记录很少。