第四章 键值对操作(二)

数据分区(进阶)

在分布式程序中,通信的代价非常大,spark可以通过控制RDD分区方式来减少通信开销。如果数据只会被扫描一次,则没必要分区,若多次使用,分区才会有帮助。

spark中所有的键值对RDD都可以进行分区。spark可以确保同一组的键出现在一个节点上。或者可以使用范围分区法,将键在同一个范围区间内的记录都放在同一节点上。

scala自定义分区方式

//通过哈希分区,可以减少有效减少数据混洗,直接传递到特定分区,大大减少需要混洗的数据量,减少网络通信。
val sc = new SparkContext(...)
val userData = sc.sequenceFile[UserID, UserInfo]("hdfs://...")
.partitionBy(new HashPartitioner(100)) // 构造100个分区,分区数目至少应与集群分配的核心数一致。
.persist()

partitionBy()

partitionBy()是一个转化操作,返回值是一个新的RDD,但不会改变原来的RDD。RDD一旦创建就不会改变,因此应对结果进行持久化,不然每次使用都会重新计算,这样会抵消哈希分区的优势。

事实上,许多其他 Spark 操作会自动为结果RDD设定已知的分区方式信息,而且除join()外还有很多操作也会利用到已有的分区信息。比如, sortByKey() 和 groupByKey()会分别生成范围分区的RDD和哈希分区的RDD。

另一方面,诸如map()这样的操作会导致新的RDD失去父RDD的分区信息,因为这样的操作理论上可能会修改每条记录的键。

获取RDD分区方式

在java和scala中,可以使用RDD的partitioner属性来获取RDD的分区方式,它会返回一个scala.option对象。这是scala中用来存放可能存在的对象的容器。

可以使用isDefined()来检查其中是否有值。如果存在值,这个值回事一个spark.Partitioner对象。

partitioner属性还可以检查想要使用的操作是否会生成正确的结果。

scala> val pairs = sc.parallelize(List((1, 1), (2, 2), (3, 3)))
pairs: spark.RDD[(Int, Int)] = ParallelCollectionRDD[0] at parallelize at
<console>:12
scala> pairs.partitioner
res0: Option[spark.Partitioner] = None
scala> val partitioned = pairs.partitionBy(new spark.HashPartitioner(2))
partitioned: spark.RDD[(Int, Int)] = ShuffledRDD[1] at partitionBy at <console>:14
scala> partitioned.partitioner
res1: Option[spark.Partitioner] = Some(spark.HashPartitioner@5147788d)

在这段简短的代码中,我们创建出了一个由 (Int, Int) 对组成的 RDD,初始时没有分区方式信息(一个值为 None 的Option 对象)。然后通过对第一个 RDD进行哈希分区,创建出了第二个RDD。如果确实要在后续操作中使用partitioned,那就应当在定义partitioned 时,在第三行输入的最后加上persist()。这和之前的例子中需要对userData调用persist()的原因是一样的:如果不调用 persist() 的话,后续的RDD操作会对partitioned的整个谱系重新求值,这会导致对pairs一遍又一遍地进行哈希分区操作。

从分区中获益的操作

能够从数据分区中获益的操作
- cogroup()
- groupWith()
- join()
- leftOuterJoin()
- rightOuterJoin()
- groupByKey()
- reduceByKey()
- combineByKey()
- lookup()

影响分区方式的操作

Spark 内部知道各操作会如何影响分区方式,并将会对数据进行分区的操作的结果 RDD 自动设置为对应的分区器。

转化操作的结果并不一定会按已知的分区方式分区。map结果不会保留固定的分区方式。spark中提供了mapValues()和flatMapValues()作为替代方法,保证每个二元组的键不变。

这里列出了所有会为生成的结果 RDD 设好分区方式的操作:

  • cogroup()
  • groupWith()
  • join()
  • leftOuterJoin()
  • rightOuterJoin()
  • groupByKey()
  • reduceByKey()
  • combineByKey()
  • partitionBy()
  • sort()
  • mapValues()(如果父RDD有分区方式的话)
  • flatMapValues()(如果父RDD有分区方式的话)
  • filter()(如果父 RDD 有分区方式的话)。

其他所有的操作生成的结果都不会存在特定的分区方式。

  • 二元操作

输出数据的分区方式取决于父RDD的分区方式,默认采用哈希分区,分区数量和操作并行度保持一致。

如果其中一个父RDD已经设置过分区方式,那么结果就会采用那种分区方式,如果都设置过,则采用第一个父RDD的分区方式。

### 示例pagerank

pagerank步骤:

(1)将每个页面的排序值初始化为 1.0。

(2)在每次迭代中, 对页面 p,向其每个相邻页面(有直接链接的页面)发送一个值为rank(p)/numNeighbors(p) 的贡献值。

(3)将每个页面的排序值设为 0.15 + 0.85 * contributionsReceived。

最后两步会重复几个循环,收敛通常需要大约 10 轮迭代。

// 假设相邻页面列表以Spark objectFile的形式存储
val links = sc.objectFile[(String, Seq[String])]("links")
.partitionBy(new HashPartitioner(100))
.persist()
// 将每个页面的排序值初始化为1.0;由于使用mapValues,生成的RDD
// 的分区方式会和"links"的一样
var ranks = links.mapValues(v => 1.0)
// 运行10轮PageRank迭代
for(i <- 0 until 10) {
val contributions = links.join(ranks).flatMap {
case (pageId, (links, rank)) =>
links.map(dest => (dest, rank / links.size))
}
ranks = contributions.reduceByKey((x, y) => x + y).mapValues(v => 0.15 + 0.85*v)
}
// 写出最终排名
ranks.saveAsTextFile("ranks")

示例程序做了不少事情来确保 RDD 以比较高效的方式进行分区,以最小化通信开销:

(1) linksRDD在每次迭代中都会和ranks发生连接操作。由于links是一个静态数据集,所以在程序一开始的时候就对它进行了分区操作,这样就不需要把它通过网络进行数据混洗了。实际上,linksRDD的字节数一般来说也会比ranks大很多,毕竟它包含每个页面的相邻页面列表(由页面ID组成),因此这一优化相比PageRank的原始实现(例如普通的MapReduce)节约了相当可观的网络通信开销。

(2) 调用 links的persist()方法,将它保留在内存中以供每次迭代使用。

(3) 们第一次创建ranks时,使用mapValues()而不是map()来保留父RDD(links)的分区方式,这样对它进行的第一次连接操作就会开销很小。

(4) 在循环体中,我们在reduceByKey()后使用mapValues();因为reduceByKey()的结果已经是哈希分区的了,这样一来,下一次循环中将映射操作的结果再次与 links 进行连接操作时就会更加高效。

自定义分区方式

实现自定义分区器,需要继承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
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值