HashPartitioner如何工作?


82

我阅读了有关的文档HashPartitioner。不幸的是,除了API调用外,没有太多解释。我假设HashPartitioner基于键的哈希对分布式集进行分区。例如,如果我的数据是

(1,1), (1,2), (1,3), (2,1), (2,2), (2,3)

因此,分区程序会将其放入不同的分区中,而相同的密钥位于同一分区中。但是我不明白构造函数参数的重要性

new HashPartitoner(numPartitions) //What does numPartitions do?

对于上述数据集,如果我这样做,结果将如何不同

new HashPartitoner(1)
new HashPartitoner(2)
new HashPartitoner(10)

那么HashPartitioner实际上如何工作?

Answers:


161

好吧,让您的数据集稍微有趣一点:

val rdd = sc.parallelize(for {
    x <- 1 to 3
    y <- 1 to 2
} yield (x, None), 8)

我们有六个要素:

rdd.count
Long = 6

没有分区:

rdd.partitioner
Option[org.apache.spark.Partitioner] = None

和八个分区:

rdd.partitions.length
Int = 8

现在让我们定义小助手来计算每个分区的元素数量:

import org.apache.spark.rdd.RDD

def countByPartition(rdd: RDD[(Int, None.type)]) = {
    rdd.mapPartitions(iter => Iterator(iter.length))
}

由于没有分区器,因此我们的数据集在分区之间均匀分布(Spark中的默认分区方案):

countByPartition(rdd).collect()
Array[Int] = Array(0, 1, 1, 1, 0, 1, 1, 1)

初始分布

现在让我们重新划分数据集:

import org.apache.spark.HashPartitioner
val rddOneP = rdd.partitionBy(new HashPartitioner(1))

由于传递给的参数HashPartitioner定义了分区数,因此我们期望一个分区:

rddOneP.partitions.length
Int = 1

由于我们只有一个分区,因此它包含所有元素:

countByPartition(rddOneP).collect
Array[Int] = Array(6)

hash-partitioner-1

请注意,混洗后的值顺序是不确定的。

如果我们使用相同的方式 HashPartitioner(2)

val rddTwoP = rdd.partitionBy(new HashPartitioner(2))

我们将获得2个分区:

rddTwoP.partitions.length
Int = 2

由于rdd是按关键数据分区的,因此将不再均匀分布:

countByPartition(rddTwoP).collect()
Array[Int] = Array(2, 4)

因为具有三个键并且只有两个不同的hashCodemod值,numPartitions所以这里没有什么意外的:

(1 to 3).map((k: Int) => (k, k.hashCode, k.hashCode % 2))
scala.collection.immutable.IndexedSeq[(Int, Int, Int)] = Vector((1,1,1), (2,2,0), (3,3,1))

只是为了确认以上内容:

rddTwoP.mapPartitions(iter => Iterator(iter.map(_._1).toSet)).collect()
Array[scala.collection.immutable.Set[Int]] = Array(Set(2), Set(1, 3))

哈希分区器2

最后,HashPartitioner(7)我们得到七个分区,三个非空分区,每个分区有两个元素:

val rddSevenP = rdd.partitionBy(new HashPartitioner(7))
rddSevenP.partitions.length
Int = 7
countByPartition(rddTenP).collect()
Array[Int] = Array(0, 2, 2, 2, 0, 0, 0)

哈希分区器7

摘要和注释

  • HashPartitioner 采用单个参数定义分区数
  • 使用hash键将值分配给分区。hash函数可能因语言而异(Scala RDD可能使用hashCode,请DataSets使用MurmurHash 3,PySpark等portable_hash)。

    在这样简单的情况下,其中key是一个小整数,您可以假定它hash是一个标识(i = hash(i))。

    Scala API用于nonNegativeMod根据计算得出的哈希确定分区,

  • 如果密钥分配不统一,您可能会遇到部分集群空闲的情况

  • 键必须是可哈希的。您可以检查我的答案A列表,作为PySpark的reduceByKey的键,以阅读有关PySpark特定问题的信息。HashPartitioner文档突出了另一个可能的问题:

    Java数组具有基于数组标识而不是其内容的hashCode,因此,尝试使用HashPartitioner对RDD [Array [ ]]或RDD [(Array [ ],_)]进行分区将产生意外或错误的结果。

  • 在Python 3中,必须确保哈希是一致的。请参阅异常是什么:应该通过PYTHONHASHSEED在pyspark中禁用字符串哈希的随机性吗?

  • 哈希分区器既不是内射的也不是外射的。可以将多个键分配给单个分区,并且某些分区可以保留为空。

  • 请注意,当与REPL定义的案例类(Apache Spark中的案例类相等)结合使用时,当前基于散列的方法在Scala中不起作用。

  • HashPartitioner(或任何其他Partitioner)对数据进行混洗。除非在多个操作之间重用分区,否则不会减少要重排的数据量。


优秀的文章,谢谢。但是,我在图像中注意到(1, None)hash(2) % P其中P是分区。不是hash(1) % P吗?
javamonkey79 '18年

我正在使用spark 2.2,并且partitionByrdd中没有api。在dataframe.write下有一个partitionBy,但是它不使用Partitioner作为参数。
hakunami

令人敬畏的回复...随机播放基于分配器,好的分配器可能会减少随机播放的数据量。
里昂,

很好的回答。我有一个问题,我在互联网上没有得到可靠的答案。当我们使用df.repartition(n)时,即当我们不将任何列指定为键时,那么哈希函数如何在内部工作,因为没有要哈希的内容?
dsk

@dsk,如果您不指定键,我相信分区将使用RoundRobinPartitioning。这里有些讨论。
Mike Souder,

6

RDD分布,这意味着将其拆分为一定数量的零件。每个分区都可能位于不同的计算机上。带有参数的哈希分区器通过以下方式numPartitions选择要放置对的分区(key, value)

  1. 精确创建numPartitions分区。
  2. 地方(key, value)与数分区Hash(key) % numPartitions

3

HashPartitioner.getPartition方法以作为参数,并返回键所属分区的索引。分区程序必须知道什么是有效索引,因此它将返回正确范围内的数字。分区数是通过numPartitions构造函数参数指定的。

实现大致返回key.hashCode() % numPartitions。看到更多详细信息,Partitioner.scala

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.