在什么情况下散列分区优于 Spark 中的范围分区?

In what scenarios hash partitioning is preferred over range partitioning in Spark?

我已经阅读了各种关于散列分区的文章。但是我仍然不明白它在什么情况下比范围分区更有优势。使用 sortByKey 后跟范围分区允许数据在集群中均匀分布。但在散列分区中可能并非如此。考虑以下示例:

考虑一对键为 [8, 96, 240, 400, 401, 800] 的 RDD,所需的分区数为 4。

在这种情况下,散列分区按如下方式在 分区:

partition 0: [8, 96, 240, 400, 800]
partition 1: [ 401 ]
partition 2: []
partition 3: [] 

(计算分区:p = key.hashCode() % numPartitions)

上面的分区导致性能不佳,因为键没有均匀分布在所有节点上。既然范围分区可以在整个集群中平均分配键,那么在什么情况下散列分区被证明是最适合范围分区的?

虽然 hashCode 的弱点令人担忧,尤其是在处理小整数时,通常可以通过根据领域特定知识调整分区数来解决。也可以使用更合适的散列函数将默认 HashPartitioner 替换为自定义 Partitioner。只要没有数据倾斜,散列分区在规模上的平均表现就足够好。

数据倾斜是完全不同的问题。如果键分布明显偏斜,则分区数据的分布可能会偏斜,无论使用什么 Partitioner。例如考虑以下 RDD:

sc.range(0, 1000).map(i => if(i < 9000) 1 else i).map((_, None))

根本无法统一划分

为什么不默认使用 RangePartitioner

  • 它不如 HashPartioner 通用。虽然 HashPartitioner 只需要为 K 正确实施 ##==,但 RangePartitioner 需要 Ordering[K].
  • HashPartitioner不同,它必须近似数据分布,因此
  • 由于拆分是根据特定分布计算的,因此在跨数据集重复使用时可能会不稳定。考虑以下示例:

    val rdd1 = sc.range(0, 1000).map((_, None))
    val rdd2 = sc.range(1000, 2000).map((_, None))
    
    val rangePartitioner = new RangePartitioner(11, rdd1)
    
    rdd1.partitionBy(rangePartitioner).glom.map(_.length).collect
    
    Array[Int] = Array(88, 91, 99, 91, 87, 92, 83, 93, 91, 86, 99)
    
    rdd2.partitionBy(rangePartitioner).glom.map(_.length).collect
    
    Array[Int] = Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1000)
    

    正如您所想象的那样,这对 joins 等操作具有严重的影响。同时

    val hashPartitioner = new HashPartitioner(11)
    
    rdd1.partitionBy(hashPartitioner).glom.map(_.length).collect
    
    Array[Int] = Array(91, 91, 91, 91, 91, 91, 91, 91, 91, 91, 90)
    
    rdd2.partitionBy(hashPartitioner).glom.map(_.length).collect
    
    Array[Int] = Array(91, 91, 91, 91, 91, 91, 91, 91, 91, 90, 91)
    

这让我们回到你的问题:

in what scenarios it is more advantageous than range partitioning.

散列分区是许多系统中的默认方法,因为它相对不可知,通常表现得相当好,并且不需要有关数据分布的额外信息。这些属性使其更可取,因为它缺乏关于数据的任何先验知识。