概述
键值对RDD一般通过初始化操作将数据转换成K-V形式,使用可控的分区方式把常被一起访问的数据放在同一个节点上,可以大大减少应用的通信开销。
Pair RDD
当从内存数据集创建pairRDD时,需要调用sparkContext.parallelizePairs
如{(1,2), (3,4), (3,6)}
函数 | 描述 | 注意 | 示例 |
---|---|---|---|
reduceByKey | 合并具有相同key的值 | {(1,2), (3,10)} | |
groupByKey | 对相同key的值分组 | {(1,[2]), (3,[4,6])} | |
combineByKey | 使用不同的返回类型并具有相同key的值 | ||
mapValues | 对pairRDD的每个value应用函数而不改变key | {(1,3), (3,5), (3,7)} | |
flatMapValues | 对pairRDD每个value应用函数 | {(1,2), (1,3), (1,4), (1,5), (3,4), (3,5)} | |
keys | 返回仅包含key的RDD | ||
values | 返回仅包含value的RDD | ||
sortByKey | 返回一个根据key排序的RDD | ||
subtractByKey | 删除RDD中key与另一个RDD中key相同的元素 | ||
join | 内连接 | ||
rightOuterJoin | 右连接 | ||
leftOuterJoin | 左连接 | ||
cogroup | 将两个RDD 中拥有相同键的数据分组到一起 |
注意:
调用reduceByKey和foldByKey会在计算key的全局结果之前先自动在每台机器上进行本地合并。
并行化调优
每个RDD都有固定数目的分区,分区数决定了RDD在执行时的并行度。
在执行聚合和分组操作时,可以要求Spark使用给定的分区数,虽然Spark会根据集群的大小推测一个有用的默认值,但是可以修改的。
如果希望在分组操作和聚合操作之外也能改变分区,可以repartition,会将数据通过网络混洗,创建新的分区集合,优化版的repartition是coalesce,可以rdd.getNumPartitions 查看RDD 的分区数,确保将RDD合并到比现在的RDD分区更少的分区中。
在分布式程序中,通信的代价是很大的,Spark通过控制RDD分区的方式来减少通信开销,只有当RDD多次在诸如join这种基于键操作使用时,分区才会有帮助。
Spark所有的pairRDD都可以分区,系统会根据一个针对key的函数对元素分区,Spark可以保证同一组的key出现在同一个节点上。
val sc = new SparkContext(...)
val userData = sc.sequenceFile[UserID, UserInfo]("hdfs://...").persist()
// 周期性调用函数来处理过去五分钟产生的事件日志
// 假设这是一个包含(UserID, LinkInfo)对的SequenceFile
def processNewLogs(logFileName: String) {
val events = sc.sequenceFile[UserID, LinkInfo](logFileName)
val joined = userData.join(events)// RDD of (UserID, (UserInfo, LinkInfo)) pairs
val offTopicVisits = joined.filter {
case (userId, (userInfo, linkInfo)) => // Expand the tuple into its components
!userInfo.topics.contains(linkInfo.topic)
}.count()
println("Number of visits to non-subscribed topics: " + offTopicVisits)
}
如上代码:
每次调用processNewLogs时都会join,默认情况下,join会将两个RDD的所有key的hash算出来,然后将hash相同的记录通过网络传到同一台机器上,然后在那台机器上对所有key相同的记录进行join,每次在join时都得对userData进行hash计算和跨节点数据混洗,虽然这些数据都不会变化。
val userData = sc.sequenceFile[UserID, UserInfo]("hdfs://...")
.partitionBy(new HashPartitioner(100)) // 构造100个分区
.persist()
可以在userData调用partitionBy指定分区,Spark就指定该RDD是按key的hash来分区的,这样在调用join时Spark只会对events数据进行混洗,网络传输数据就大大减少了。
事实上,很多Spark操作会自动为结果RDD设置已知的分区方式,如sortByKey和groupByKey会分别生成范围分区的RDD和哈希分区的RDD,而另一方面map会导致新RDD丢失父RDD的分区信息。
从分区中获益
Spark的很多操作都引入了将数据根据跨节点进行混洗的过程,所有这些操作都可以从分区中获益。如cogroup、groupWith、join、leftOuterJoin、rightOuterJoin、groupByKey、reduceByKey、combineByKey、lookup。
对于诸如cogroup() 和join() 这样的二元操作,预先进行数据分区会导致其中至少一个RDD不发生数据混洗,如果两个RDD 使用同样的分区方式,并且它们还缓存在同样的机器上(比如一个RDD 是通过mapValues() 从另一个RDD 中创建出来的,这两个RDD 就会拥有相同的键和分区方式),或者其中一个RDD 还没有被计算出来,那么跨节点的数据混洗就不会发生了。
影响分区的操作
当对一个哈希分区的key调用map时,由于map理论上会改变key,因此结果不会有固定的分区方式,可以使用mapValues和flatMaoValues替换,会保留原来的key。
这些操作都会保留上级分区信息:
cogroup()、groupWith()、join()、lef tOuterJoin()、rightOuterJoin()groupByKey()、reduceByKey()、combineByKey()partitionBy()、sort()、mapValues()(如果父RDD有分区方式的话)、flatMapValues()
Spark快速大数据分析