pyspark:有效地使partitionBy写入与原始表

时间:2018-06-09 15:35:02

标签: apache-spark pyspark

我有一个与pyspark的repartitionBy()函数相关的问题,我最初在this question的评论中发布了这个问题。我被要求将其作为一个单独的问题发布,所以这里是:

我理解df.partitionBy(COL)会将每个COL值的所有行写入自己的文件夹,并且每个文件夹都会(假设这些行以前是通过其他键分布在所有分区上) )与整个表中的文件数量大致相同。我发现这种行为很烦人。如果我有一个包含500个分区的大表,并且在某些属性列上使用partitionBy(COL),那么我现在有100个文件夹,每个包含500个(现在非常小)文件。

我想要的是partitionBy(COL)行为,但文件大小和文件数量与原来大致相同。

作为演示,上一个问题分享了一个玩具示例,其中有一个包含10个分区的表并执行partitionBy(dayOfWeek),现在您有70个文件,因为每个文件夹中有10个。我想要~10个文件,每天一个,可能需要2或3天,有更多的数据。

这可以轻松完成吗?像df.write().repartition(COL).partitionBy(COL)这样的东西似乎可能会起作用,但是我担心(在一个非常大的表中将被分割成许多文件夹的情况)必须首先将它组合到一些少量的分区之前做partitionBy(COL)似乎是一个坏主意。

非常感谢任何建议!

2 个答案:

答案 0 :(得分:18)

您有多种选择。在我的下面的代码中,我假设您想要在拼花地板上书写,但当然您可以改变它。

(1)df.repartition(numPartitions,* cols).write.partitionBy(* cols).parquet(writePath)

这将首先使用基于散列的分区来确保来自COL的有限数量的值进入每个分区。根据您为numPartitions选择的值,某些分区可能为空,而其他分区可能拥挤值 - 对于不确定原因的人,请阅读this。然后,当您在DataFrameWriter上调用partitionBy时,每个分区中的每个唯一值都将放在其自己的单个文件中。

警告:这种方法可能导致不平衡的分区大小和不平衡的任务执行时间。当列中的值与许多行相关联时会发生这种情况(例如,城市列 - 新文件)约克市可能有很多行),而其他值则较少(例如,小城镇的价值)。

(2)df.sort(sortCols).write.parquet(writePath)

当您希望(1)您编写的文件大小几乎相等(2)对写入文件数量的精确控制时,此选项非常有用。这种方法首先对您的数据进行全局排序,然后找到将数据分解为k均匀大小的分区的拆分,其中在spark config k中指定了spark.sql.shuffle.partitions。这意味着具有相同排序键值的所有值彼此相邻,但有时它们会跨越分割,并且位于不同的文件中。如果您的用例要求所有具有相同密钥的行位于同一分区中,则不要使用此方法。

还有两个额外的奖励:(1)通过对数据进行排序,它在磁盘上的大小通常可以减少(例如,按user_id对所有事件进行排序,然后按时间排序将导致列值中的大量重复,这有助于压缩) (2)如果你写一个支持它的文件格式(如Parquet),那么后续的读者可以通过使用谓词下推来最佳地读取数据,因为镶木地板编写者将在元数据中写出每列的MAX和MIN值,如果查询指定分区(最小,最大)范围之外的值,则允许读者跳过行。

请注意,Spark中的排序比仅重新分区更昂贵,并且需要额外的阶段。在幕后,Spark将首先在一个阶段确定分裂,然后将数据混合到另一个阶段的分裂中。

(3)df.rdd.partitionBy(customPartitioner).toDF()。write.parquet(writePath)

如果您在Scala上使用spark,那么您可以编写一个客户分区程序,它可以克服基于散列的分区程序的烦人问题。遗憾的是,pySpark不是一个选项。如果你真的想在pySpark中编写一个自定义分区器,我发现使用rdd.repartitionAndSortWithinPartitions可以实现这一点,虽然有点尴尬:

df.rdd \
  .keyBy(sort_key_function) \  # Convert to key-value pairs
  .repartitionAndSortWithinPartitions(numPartitions=N_WRITE_PARTITIONS, 
                                      partitionFunc=part_func) \
  .values() # get rid of keys \
.toDF().write.parquet(writePath)

也许其他人知道在pyspark中使用数据框上的自定义分区器的更简单方法吗?

答案 1 :(得分:1)

df.write().repartition(COL).partitionBy(COL)将为每个分区写出一个文件。如果您的分区之一包含大量数据,则此方法将无法正常工作。例如如果一个分区包含100GB的数据,Spark将尝试写出100GB的文件,您的工作可能会崩溃。

df.write().repartition(2, COL).partitionBy(COL)将每个分区最多as described in this answer写出两个文件。这种方法适用于不太倾斜的数据集(因为每个分区的最佳文件数对于所有分区大致相同)。

This answer说明了如何为具有大量数据的分区写出更多文件,为小型分区写出更少的文件。