我正在尝试使用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数据的分区将作为单个文件被写出。
我最好的前进道路是什么?
答案 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
值,则不能保证辅助键位于同一位置。也有可能,实际上也有可能,最终得到一个文件,每个分区中的记录很少。