Learning Spark - LIGHTNING-FAST DATA ANALYSIS 第四章 - (4)

来源:互联网 发布:12123app网络请求失败 编辑:程序博客网 时间:2024/05/16 14:49

接着续,每天5分钟:Learning Spark - LIGHTNING-FAST DATA ANALYSIS 第四章 - (3)

停电了2个多小时,还好又来了~ 第四章结束啦~~ 待会再出个完整版~


受益于分区的操作

Spark的许多操作都牵扯到根据主键跨网络shuffle数据的问题。所有这些都能从分区受益。Spark 1.0中,受益于分区的操作包括cogroup(), groupWith(), join(), leftOuterJoin(), rightOuterJoin(), groupByKey(), reduceByKey(), combineByKey() 和lookup()。

 

作用于单个RDD的操作,比如reduceByKey(),运行在预分区的RDD上会导致每个主键的所有的数据在单机本地计算,在本地归约之后,最后才需要从各个worker节点发送到主节点。对于二元操作,比如cogroup()join(),预分区至少能使其中一个RDD(已知partitioner的那个)不用被shuffle。如果两个RDD有同样的partitioner,并且被缓存在同一台机器(比如其中一个是用另一个进行mapValues()创建的,会保留主键和分区信息),或者其中一个还没有被计算,那么就不会有跨网络的shuffle发生。

 

影响分区的操作

Spark知道内部每个操作是如何影响分区的,所以会自动对这些分区操作创建的RDD设置partitioner。比如说,假设你调用join()来连接两个RDD,由于有相同主键的元素已经被哈希到同一个机器,Spark知道结果是哈希分区,那么像reduceByKey()这个函数来操作连接的结果就会更加快速。

 

然而,另一面是,对于变换不能保证产生已知的分区,输出的RDD将不会有partitioner。比如,你对一个哈希分区的键值对RDD调用map(),传入map()的函数理论上可以改变每个元素的主键,所以结果RDD不会有partitionerSpark不会分析你的函数是否保持着主键,相反,它提供了mapValues()flatMapValues()这两个操作,它们都能保证每个二元组的主键保持相同。

 

如上所述,这些就是所有导致输出的RDD被设置partitioner的操作:cogroup(), groupWith(), join(), leftOuterJoin(), rightOuterJoin(), groupByKey(), reduceByKey(), combineByKey(), partitionBy(), sort(), mapValues() (如果父RDDpartitioner), flatMapValues() (如果父RDDpartitioner)和filter() (如果父RDDpartitioner)。所有其他操作产生的结果RDD都没有paritioner

最终,对于二元操作,哪一个paritioner会被设置到输出的RDD依赖于父RDDpartitioner。默认情况下,是带分区数设置了操作并行度的哈希partitioner。然而,如果其中一个RDDparitioner,则输出RDDpartitioner就是它;如果两个父RDD都有partitioner,那就会是第一个父RDD的那个。

示例:PageRank

作为一个能从RDD分区受益的涉及更多算法的例子,我们想到了PageRankPageRank算法,是以GoogleLarry Page命名,目标是对于一组文档中的每一个,依据有多少个文档连接到它来作为其重要性(rank)的度量。这当然可以用于网页重要度,也能够用于学术论文,或者社交网络中的用户影响。

 

PageRank是个迭代算法,要执行很多的连接,所以这是个RDD分区的好用例。该算法包括两个数据集:一个是(pageID, linkList),其元素包含了每个页的邻居列表;另一个是(pageID, rank),其元素是每个页的当前rank。处理流程如下:

 

1. 初始化每页的rank1.0

2. 对每次迭代,页面p发送rank(p)/numNeighbors(p)的贡献给它的邻居(该页连接出去的)

3. 设置每页的rank0.15+0.85*contributionsReceived

最后两步重复的多次迭代,算法会收敛到每个页面都有正确的PageRank值。实际上,一般要运行大约10次迭代。

 

示例4-25给出了SparkPageRank的实现代码。

 

示例4-25. Scala PageRank
// Assume that our neighbor list was saved as a Spark objectFile
val links = sc.objectFile[(String, Seq[String])]("links")
    .partitionBy(new HashPartitioner(100))
    .persist()
// Initialize each page's rank to 1.0; since we use mapValues, the resulting RDD
// will have the same partitioner as links
var ranks = links.mapValues(v => 1.0)
// Run 10 iterations of 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)
}
// Write out the final ranks
ranks.saveAsTextFile("ranks")

就是这!算法最开始为ranks RDD的每个元素初始化成1.0,然后每轮迭代保持更新ranks变量。PageRank的主体在Spark中表达的很简单:首先,为了按pageID将链接列表和rank值关联到一起,使用当前的ranks RDD和静态的links做了个join(),然后用flatMap创建“贡献”值发送到页面的每个邻居。然后按照pageID将这些值加起来(比如按页接收贡献值),并设置页面的rank值为0.15+0.85*contributionsReceived

 

尽管代码本身很简单,本例也做了些事确保RDD被高效的分区,最小化通信。

1. 注意links RDD每次迭代都被join()ranks。由于links是静态数据集,我们在一开始就用partitionBy()对其分区,所以不需要跨网络shuffle数据。实际上links RDD很可能比ranks在字节数上要大得多。因为它包含了每个页的所有邻居列表,而不只是个Double。所以这个优化对于这个PageRank的简单实现(例如,相对于MapReduce)节省了可能的网络传输。

2. 同样的原因,我们对links调用persist()使其在迭代过程中一直在内存里。

3. 当我们第一次创建ranks,使用的是mapValues()而不是map(),保留了父RDD(links)的分区信息。所以我们的第一次join()开销不大。

4. 在循环体中,我们在reduceByKey()后面跟了一个mapValues()调用。因为reduceByKey()的结果是哈希分区的,这会使得在下一轮迭代中对map的结果和links做连接时更高效。

 

要最大化潜在的分区相关的优化,你应该使用mapValues()或者flatMapValues()。无论什么时候都不要改变元素的主键。

 

 

自定义分区

虽然SparkHashPartitionerRangePartitioner对大多数情况下都适用,Spark也支持你用自定义的partitioner对象来调整RDD分区。这可以帮助你利用领域相关的知识进一步降低通信。

 

例如,假设我们想对一组网页运行前一节的PageRank算法。这里每个页面的IDRDD的主键)将是它的URL。用简单的哈希函数来分区,有着近似URL的网页(比如http://www.cnn.com/WORLDhttp://www.cnn.com/US)可能会被分区到不同机器上。然而,我们知道相同域名的页面容易产生很多的相互链接。由于PageRank每轮迭代都要从一个页面发送消息到它所有邻居,所以将这些页面分组到同一个分区会有帮助。我们可以通过自定义partitioner查找域名而不是URL来处理它。

 

 

 

 

要实现自定义paritioner,你需要子类化org.apache.spark.Partitioner并实现三个方法:

l numPartitionsInt,返回你要创建的分区数

l getPartition(key: Any)Int,对给定主键返回分区ID(0numPartitions-1)

l equals():标准的Java等值方法。实现它很重要,因为当Spark判决两个RDD对象的分区方式相同否,需要比较你的Partitioner对象和它自身的其他实例。

 

有一个问题,如果你的算法中依靠Javahashcode()方法,它可以返回负数。你要小心确保getPartition()总是返回非负数。

 

示例4-26显示了我们如何写之前概述的基于域名的partitioner,仅哈希每个URL的域名部分。

 

示例4-26. Scala custom partitioner
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 // Make it non-negative
        } else {
            code
        }
    }
    // Java equals method to let Spark compare our Partitioner objects
    override def equals(other: Any): Boolean = other match {
        case dnp: DomainNamePartitioner =>
            dnp.numPartitions == numPartitions
        case _ =>
            false
    }
}

注意equals()方法,我们用的是Scala的模式匹配操作符(match)来测试另一个是不是DomainNamePartitioner,如果是就转换。就跟Java中的instanceOf()一样。

 

使用用户自定义的Partitioner很容易:将它传到partitionBy()方法。在Spark中有许多基于Shffule的方法,比如join()groupByKey(),都能带一个可选的Partitioner参数来控制输出的分区。

 

Java中创建自定义ParitionerScala中很类似:继承spark.Partitioner类并实现要求的方法。

 

Python中不用继承Partitioner类,而是传入一个哈希函数作为RDD.partitionBy()的额外参数,见示例4-27所示。

 

示例4-27. Python custom partitioner
import urlparse
def hash_domain(url):
return hash(urlparse.urlparse(url).netloc)
rdd.partitionBy(20, hash_domain) # Create 20 partitions

注意,你传入的哈希函数会和其他RDD做一致性的比较。如果你想多个RDD有相同的partitioner,传入同一个函数(比如全局函数)而不是为每一个都创建新的lambda函数。

 

总结

本章我们学习了在Spark中用特殊的函数处理键值对的数据。第三章中的技术仍然适用于pair RDD。下一章我们看看如何加载和保存数据。


0 0
原创粉丝点击