Spark中如何实现DataFrame按列重分区时各分区数据量均衡
Spark DataFrame按指定列重分区实现数据量均衡的方案
默认repartition(分区数, 列名)的逻辑是对指定列哈希取模分配分区,当列唯一值少、或者哈希值分布不均匀时必然会出现分区倾斜,下面提供两种符合要求(不转RDD、不落地存储)的解决方案,适配不同业务场景:
方案1:允许同ID分散到多分区,仅要求分区数据量均衡
如果你的业务不需要保证同一个ID的所有数据都落在同一个分区,可通过给原ID拼接随机盐值的方式打散哈希分布,实现分区数据量接近均衡:
代码示例
import pyspark.sql.functions as f # 设定目标分区数 target_partition_num = 3 # 给id加随机盐值,盐值范围和目标分区数一致 df_with_salt = df.withColumn("salt", f.rand() * target_partition_num) # 按盐值+原id重分区 df_repartitioned = df_with_salt.repartition(target_partition_num, "salt", "id").drop("salt")
特点
- 实现简单,性能开销极低
- 分区数据量差异基本在5%以内,均衡度高
- 同ID可能分布在不同分区,不适合需要按ID做分区内聚合、关联的场景
方案2:要求同ID必须落在同一分区,同时保证分区均衡
如果需要保证同一个ID的所有数据都在同一个分区,可通过预统计ID权重、手动分配分区映射的方式实现:
实现逻辑
- 先统计每个ID对应的总数据行数
- 按ID的行数从大到小排序,计算累计行数,按总数据量均匀划分分区边界
- 生成ID到分区号的映射表,和原DataFrame关联得到每一行对应的目标分区号
- 按目标分区号字段重分区
代码示例
import pyspark.sql.functions as f from pyspark.sql import Window target_partition_num = 3 # 步骤1:统计每个id的行数 id_count = df.groupBy("id").count() # 步骤2:按行数倒序排列,计算累计行数,按总大小均匀分配分区 window_spec = Window.orderBy(f.desc("count")) id_count = id_count.withColumn("cum_count", f.sum("count").over(window_spec)) total_rows = id_count.select(f.sum("count")).first()[0] per_partition_size = total_rows / target_partition_num # 生成分区映射 id_partition_map = id_count.withColumn("target_partition", f.floor(f.col("cum_count")/per_partition_size).cast("int")) # 步骤3:关联原表得到分区号 df_with_partition = df.join(id_partition_map.select("id", "target_partition"), on="id", how="left") # 步骤4:按目标分区号重分区,删除辅助字段 df_repartitioned = df_with_partition.repartition(target_partition_num, "target_partition").drop("target_partition")
特点
- 完全满足同ID同分区的要求
- 分区数据量均衡度极高,差异可以控制在1%以内
- 仅增加一次轻量的ID统计和关联操作,性能开销低
验证方法
你可以用原有的分区数据量统计代码验证两种方案的效果,两种方案都不会触发落地存储,也不需要转换为RDD操作。
内容的提问来源于stack exchange,提问作者karpan
相关产品推荐
相关产品推荐

