Spark Pair RDD的高级特性-分区


1、数据分区的意义

在分布式程序中,通信的代价是很大的,因此控制数据分布以获得最少的网络传输可以极大地提升整体性能。和单节点的程序需要为记录集合选择合适的数据结构一样,Spark程序可以通过控制RDD分区方式来减少通信开销。而只有当数据集多次在诸如连接这种基于键的操作使用时,分区才会有帮助。

Spark中所有的键值对RDD都可以进行分区。系统会根据一个针对键的函数对元素进行分组,并确保同一组的键出现在同一个节点上。比如,可以使用哈希分区将一个RDD分成100个分区,此时键的哈希值对100取模的结果相同的记录会被放在一个节点上;也可以使用范围分区法,将键在同一个范围区间的记录都放在同一个节点上。

可以通过RDD的partitioner属性来获取RDD的分区方式。

2、分区案例

一个应用的内存中保存着一张较大的用户信息表,即由(UserID,UserInfo)对组成的RDD,其中UserInfo包含一个该用户所订阅的主题的列表。同时还有一个小文件,文件内存放着过去五分钟内用户的访问日志,即由(UserID,LinkInfo)对组成的表。现在需要对用户访问其未订阅主题的页面情况进行统计,可以使用Spark的join()来实现这个组合操作,其中需要把UserInfo和LinkInfo的有序对根据UserID进行分组。

//初始化代码,从HDFS上的一个Hadoop SequenceFile中读取用户信息
//userData中的元素会根据它们被读取时的来源,即HDFS块所在的节点来分布
//Spark此时无法获知某个特定的UserID对应的记录位于哪个节点上
val sc = new SparkContext(...)
val userData = sc.sequenceFile("hdfs://...").persist()

//周期性调用函数来处理过去五分钟产生的事件日志
//假设这是一个包含(UserID,LinkInfo)对的SequenceFile
def processUserInfo(fileName:String) {
	val events = sc.sequenceFile(fileName)
	val joined = userData.join(events)
	val offTopicVists = joined.filter{
		case(userId,(userInfo,linkInfo)) =>
			!userInfo.topics.contains(linkInfo.topic)
	}.count()
	println("Number of visits to non-subscribed topics:"+offTopicVists)
}

这段代码可以运行,但是不够高效,因为每次调用processUserInfo()时都会调用join()方法,默认情况下,连接操作会将两个数据集中的所有键的哈希值都求出来,将该哈希值相同的记录通过网络传到同一台机器上,然后在那台机器上对所有键相同的记录进行连接操作。因为userData表比每五分钟出现的访问日志表events表要大得多,所以要浪费时间做很多额外工作:在每次调用时都对userData表进行哈希值计算和跨节点数据混洗,虽然这些数据从来都不会变化。

要解决这一问题可以在程序开始时,对userData表使用partitionBy()转化操作,将这张表转化为哈希分区表,可以通过向partitionBy()传递一个spark.HashPartitioner对象来实现该操作。

val sc = new SparkContext(...)
val userData = sc.sequenceFile("hdfs://...")
				 .partitionBy(new HashPartitioner(100))
				 .persist()

由于在构建userData时调用了partitionBy(),Spark就知道了该RDD是根据键的哈希值来分区的,这样在调用userData.join(events)时,Spark就只会对events进行数据混洗操作,将events中特定的UserID的记录发送到userData的对应分区所在的那台机器上。这样,需要通过网络传输的数据就大大减少了,程序运行速度也可以提升。
注意:
(1)对partitionBy()的结果须进行持久化并保存为uesrData。否则后续的RDD操作会对userData的整个谱系重新求值,导致对RDD一遍又一遍地进行哈希分区和跨节点的混洗,和没有指定分区方式时发生的情况十分相似。
(2)传给partitionBy()的100表示分区数目,它会控制之后对这个RDD进一步操作(如连接操作)时有多少任务会并行执行,这个值至少应该和集群中的总核心数一样。
(3)除join()外还有其他很多操作也会利用到已有的分区信息,比如sortByKey()和groupByKey()会分别生成范围分区的RDD和哈希分区的RDD。

3、从分区中获益的操作

Spark的许多操作都引入了将数据根据键跨节点进行混洗的过程,所有这些操作都会从数据分区中获益。能够从数据分区中获益的操作有:
cogroup()、groupWith()、join()、leftOuterJoin()、rightOuterJoin()、groupByKey()、reduceByKey()、combineByKey()、lookup()。

对于join()这样的二元操作,预先进行数据分区会导致其中至少一个RDD(使用已知分区器的那个RDD)不发生数据混洗。

4、影响分区方式的操作

Spark内部知道各操作会如何影响分区方式,并将会对数据进行分区的操作的结果RDD自动设置为对应的分区器。比如,使用join()连接两个RDD时,由于键相同的元素会被哈希到同一台机器上,则输出结果也是哈希分区的。

不过,转化操作的结果并不一定会按已知的分区方式分区,比如map()方法理论上可以改变元素的键,因此结果就不会有固定的分区方式。

Spark会为生成的结果RDD设好分区方式的操作有:
cogroup()、groupWith()、join()、leftOuterJoin()、rightOuterJoin()、groupByKey()、reduceByKey()、combineByKey()、partitionBy()、sort()。

如果父RDD有分区方式的话,以下操作也会设有分区方式:
mapValues()、flatMapValues()、filter()。

其他操作生成的结果都不会存在特定的分区方式。所以为了最大化分区相关优化的潜在作用,应该在无需改变元素的键时尽量使用mapValues()或flatMapValues()。

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

5、自定义分区方式

class DefPartitioner(partNum:Int) extends Partitioner{
    override def numPartitions = partNum

    override def getPartition(key: Any) :Int = {
        val code = (key.hashCode % numPartitions)
        if(code <0){
            code += numPartitions
        }else{
            code
        }
    }

    override def equals(other:Any):Boolean = other match {
        case par:DefPartitioner =>
            par.numPartitions == numPartitions
        case _ =>
            false
    }
}

在程序中使用自定义分区函数,传给partitionBy()即可,比如,rdd.partitionBy(new DefPartitioner(5))。

Spark中有很多依赖于数据混洗的方法,比如join()、groupByKey(),它们都可以接收一个可选的Partitioner对象来控制输出数据的分区方式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值