Spark每日半小时(14)——影响分区方式的操作以及示例

影响分区方式的操作

Spark内部知道各操作会如何影响分区方式,并将会对数据进行分区的操作的结果RDD自动设置为对应的分区器。例如,如果你调用join()来连接两个RDD;由于键相同的元素会被哈希到同一台机器上,Spark知道输出结果也是哈希分区的,这样对连接的结果进行诸如reduceByKey()这样的操作时就会明显变快。

不过,转化操作的结果并不一定会按已知的分区方式分区,这时输出的RDD可能就会没有设置分区器。例如,当你对一个哈希分区的键值对RDD调用map()时,由于传给map()的函数理论上可以改变元素的键,因此结果就不会有固定的分区方式。Spark不会分析你的函数来判断键是否会被保留下来。不过,Spark提供了另外两个操作mapValues()和flatMapValues()做为替代方法,他们可以保证每个二元组的键保持不变。

这里列出了所有会为生成的结果RDD设好分区方式的操作:cogroup()、groupWith()、join()、leftOuterJoin()、rightOuterJoin()、groupByKey() 、reduceByKey()、combineByKey()、partitionBy()、sort()、mapValues()(如果父 RDD 有分区方式的话)、flatMapValues()(如果父 RDD 有分区方式的话),以及filter()(如果父 RDD 有分区方式的话)。其他所有的操作生成的结果都不会存在特定的分区方式。

最后,对于二元操作,输出数据的分区方式取决于父RDD的分区方式。默认情况下,结果会采用哈希分区,分区的数量和操作的并行度一样。不过,如果其中的一个父RDD已经设置过分区方式,那么结果就会采用那种分区方式;如果两个父RDD都设置过分区方式,结果RDD会采用第一个父RDD的分区方式。

示例:PageRank

PageRank是一种从RDD分区中获益的更复杂的算法,我们以它为例进行分析。PageRank算法是以Google的拉里·佩吉的名字命名的,用来根据外部文档指向一个文档的链接,对集合中每个文档的重要程度赋一个度量值。该算法可以用于对网页进行排序,当然,也可以用于排序科技文章或社交网络中有影响的用户。

PageRank是执行多次连接的一个迭代算法,因此它是RDD分区操作的一个很好的用例。算法会维护两个数据集:一个由(pageID、rank)的元素组成,包含每个页面的相邻页面的列表;另一个由(pageID,rank)元素组成,包含每个页面的当前排序值。它按如下步骤进行计算。

  1. 将每个页面的排序值初始化为1.0 。
  2. 在每次迭代中,对页面p,向其每个相邻页面(有直接链接的页面)发送一个值为rank(p)/numNeighbors(p)的贡献值。
  3. 将每个页面的排序值设为0.15+0.85*contributionsReceived。

最后两步会重复几个循环,在此过程中,算法会逐渐收敛于每个页面的实际PageRank值。在实际操作中,收敛通常需要大约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")

这就行了,算法从将ranksRDD的每个元素的值初始化为1.0开始,然后在每次迭代中不断更新ranks变量。在Spark中编写PageRank的主体相当简单:首先对当前的ranksRDD和静态的linksRDD进行一次join()操作,来获取每个页面ID对应的相邻页面列表和当前的排序值,然后使用flatMap创建出“contributions”来记录每个页面对各相邻页面的贡献。然后再把这些贡献值按照页面ID(根据获得共享的页面)分别累加起来,把该页面的排序值设为0.15+0.85*contributionsReceived。

虽然代码本身很简单,这个示例程序还是做了不少事情来确保RDD以比较高效的方式进行分区,以最小化通信开销:

  1. 请注意,linksRDD在每次迭代中都会和ranks发生连接操作。由于links是一个静态数据集,所以我们在程序一开始的时候就对它进行了分区操作,这样就不需要把它通过网络进行数据混洗了。实际上,linksRDD的字节数一般来说也会比ranks大很多,毕竟它包含每个页面的相邻页面列表(由页面ID组成),而不仅仅是一个Double值,因此这一优化相比PageRank的原始实现(例如普通的MapReduce)节约了相当可观的网络通信开销。
  2. 出于同样的原因,我们调用links的persist()方法,将它保留在内存中以供每次迭代使用。
  3. 当我们第一次创建ranks时,我们使用mapValues()而不是map()来保留父RDD(links)的分区方式,这样对它进行的第一次连接操作就会开销很小。
  4. 在循环体中,我们在reduceByKey()后使用mapValues();因为reduceByKey()的结果已经是哈希分区的了,这样一来,下一次循环中将映射操作的结果再次与links进行连接操作时就会更加高效。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值