Spark每日半小时(15)——自定义分区方式

虽然Spark提供的HashPartitioner与RangePartitioner已经能够满足大多数用例,但Spark还是允许你通过提供一个自定义的Partitioner对象来控制RDD的分区方式。这可以让你利用领域知识进一步减少通信开销。

举个例子,假设我们要在一个网页的集合上运行前一节中的PageRank算法。在这里,每个页面的ID(RDD中的键)是页面的URL。当我们使用简单的哈希函数进行分区时,拥有相似的URL的页面可能会被分到完全不同的节点上。然而,我们知道在同一个域名下的网页更有可能相互链接。由于PageRank需要在每次迭代中从每个页面向它所有相邻的页面发送一条消息,因此把这些页面分组到同一个分区中会更好。可以使用自定义的分区器来实现仅根据域名而不是整个URL来分区。

要实现自定义的分区器,你需要继承org.apache.spark.Partitioner类并实现下面三个方法。

  • numPartitions:Int:返回创建出来的分区数。
  • getPartition(key:Any):Int:返回给定键的分区编号(0到numPartitions-1)。
  • equals():Java判断相等性的标准方法。这个方法的实现非常重要,Spark需要用这个方法来检查你的分区器对象是否和其他分区器实例相同,这样Spark才可以判断两个RDD的分区方式是否相同。

有一个问题需要注意,当你的算法依赖于Java的hashCode()方法时,这个方法有可能会返回负数。你需要十分谨慎,确保getPartition()永远返回一个非负数。

下例展示了如何编写一个前面构思的基于域名的分区器,这个分区器只对URL中的域名部分求哈希。

class DomainNamePartitioner(numParts: Int) extends Partitioner {
override def numPartitions: Int = numParts
override def getPartition(key: Any): Int = {
val domain = new Java.net.URL(key.toString).getHost()
val code = (domain.hashCode % numPartitions)
if(code < 0) {
code + numPartitions // 使其非负
}else{
code
}
}
// 用来让Spark区分分区函数对象的Java equals方法
override def equals(other: Any): Boolean = other match {
case dnp: DomainNamePartitioner =>
dnp.numPartitions == numPartitions
case _ =>
false
}
}

注意,在equals()方法中,使用Scala的模式匹配操作符(match)来检查other是否是DomainNamePartitioner,并在成立时自动进行类型转换;这和Java中的instanceof()是一样的。

使用自定义的Partitioner是很容易的:只要把它传给partitionBy()方法即可。Spark中有许多依赖于数据混洗的方法,比如join()和groupByKey(),它们也可以接收一个可选的Partitioner对象来控制输出数据的分区方式。

在Java中创建一个自定义Partitioner的方法与Scala中的做法非常相似:只需要扩展spark.Partitioner类并且实现必要的方法即可。

在Python中,不需要扩展Partitioner类,而是把一个特定的哈希函数作为一个额外的参数传递给RDD.partitionBy()函数,如下

//Python自定义分区方式
import urlparse
def hash_domain(url):
return hash(urlparse.urlparse(url).netloc)
rdd.partitionBy(20, hash_domain) # 创建20个分区

注意,这里你所传过去的哈希函数会被其他RDD的分区函数区分开来。如果你想要对多个RDD使用相同的分区方式,就应该使用同一个函数对象,比如一个全局函数,而不是为每个RDD创建一个新的函数对象。

你可能感兴趣的:(#,大数据——Spark每日半小时,#,Spark每日半小时)