我希望每个分区都写成 1 GB 的文件。因此,具有 7 GB 数据的分区将作为 7 个文件写出,而具有 0.3 GB 数据的分区将作为单个文件写出。
目前接受的答案在大多数情况下可能已经足够好,但并不能完全满足将 0.3 GB 分区写入单个文件的请求。相反,它将为每个输出分区目录写出numPartitions 文件,包括 0.3 GB 分区。
您正在寻找一种通过数据分区大小动态扩展输出文件数量的方法。为此,我们将在 10465355 的方法的基础上使用 rand() 来控制 repartition() 的行为,并根据我们希望该分区的文件数量来扩展 rand() 的范围。
很难通过输出文件大小来控制分区行为,因此我们将使用每个输出文件所需的大致行数来控制它。
我将在 Python 中提供一个演示,但方法在 Scala 中基本相同。
from pyspark.sql import SparkSession
from pyspark.sql.functions import rand
spark = SparkSession.builder.getOrCreate()
skewed_data = (
spark.createDataFrame(
[(1,)] * 100 + [(2,)] * 10 + [(3,), (4,), (5,)],
schema=['id'],
)
)
partition_by_columns = ['id']
desired_rows_per_output_file = 10
partition_count = skewed_data.groupBy(partition_by_columns).count()
partition_balanced_data = (
skewed_data
.join(partition_count, on=partition_by_columns)
.withColumn(
'repartition_seed',
(
rand() * partition_count['count'] / desired_rows_per_output_file
).cast('int')
)
.repartition(*partition_by_columns, 'repartition_seed')
)
这种方法将平衡输出文件的大小,无论分区大小有多么倾斜。每个数据分区都会获得它需要的文件数量,以便每个输出文件具有大致请求的行数。
这种方法的先决条件是计算每个分区的大小,您可以在partition_count 中看到。如果您真的想动态扩展每个分区的输出文件数量,这是不可避免的。
为了证明这是正确的,让我们检查分区内容:
from pyspark.sql.functions import spark_partition_id
(
skewed_data
.groupBy('id')
.count()
.orderBy('id')
.show()
)
(
partition_balanced_data
.select(
*partition_by_columns,
spark_partition_id().alias('partition_id'),
)
.groupBy(*partition_by_columns, 'partition_id')
.count()
.orderBy(*partition_by_columns, 'partition_id')
.show(30)
)
这是输出的样子:
+---+-----+
| id|count|
+---+-----+
| 1| 100|
| 2| 10|
| 3| 1|
| 4| 1|
| 5| 1|
+---+-----+
+---+------------+-----+
| id|partition_id|count|
+---+------------+-----+
| 1| 7| 9|
| 1| 49| 6|
| 1| 53| 14|
| 1| 117| 12|
| 1| 126| 10|
| 1| 136| 11|
| 1| 147| 15|
| 1| 161| 7|
| 1| 177| 7|
| 1| 181| 9|
| 2| 85| 10|
| 3| 76| 1|
| 4| 197| 1|
| 5| 10| 1|
+---+------------+-----+
根据需要,每个输出文件大约有 10 行。 id=1 得到 10 个分区,id=2 得到 1 个分区,id={3,4,5} 每个得到 1 个分区。
此解决方案平衡了输出文件的大小,不受数据倾斜的影响,并且不会限制relying on maxRecordsPerFile 的并行度。