Spark shuffle相关总结

主要是http://blog.cloudera.com/blog/2015/03/how-to-tune-your-apache-spark-jobs-part-1/这篇文章的内容。

这里个人总结了一下。

宽依赖和窄依赖(wide dependency and narrow dependency)
宽依赖指的是多个子RDD的Partition会依赖同一个父RDD的Partition。如reduceByKey, groupByKey, aggrateByKey等。
窄依赖指的是每一个父RDD的Partition最多被子RDD的一个Partition使用。 如map, filter, flatMap等。
当一个父RDD的Partition引出来的线有分支的时候那么就是宽依赖啦,宽依赖会导致shuffle的发生所以有称为shuffle dependency。


1、Stage划分
Spark通过宽依赖来划分Stage。


如wordCount中:

sc.textFile("words.txt").
  flatMap(_.split(" ")).
  map((_, 1)).
  reduceByKey(_ + _).
  saveAsTextFile(destPath)
textfile --> flatMap --> map --> reduceBykey 为一个stage
reduceByKey --> saveAsTextFile 为另一个stage


再看下面代码
sc.textFile("someFile.txt").
  map(mapFunc).
  flatMap(flatMapFunc).
  filter(filterFunc).
  count()
这段代码就只有一个stage,因为map,flatMap和filter操作都是窄依赖。


下面是charCount代码:

// 读入数据并将每行以空格分割成单词,结果是String类型的RDD
val tokenized = sc.textFile(args(0)).flatMap(_.split(' '))

// 每个单词映射成元组,value值为1,然后利用根据key来合并,对于相同的key,它们的value做相加操作
val wordCounts = tokenized.map((_, 1)).reduceByKey(_ + _)
val filtered = wordCounts.filter(_._2 >= 1000) // 过滤出现次数大于1000的单词

// 将每个单词转化成字符数组,经过flatMap后,返回结果为Char类型的RDD,再将每个字符映射成元组
val charCounts = filtered.flatMap(_._1.toCharArray).map((_, 1)).
  reduceByKey(_ + _) // 统计字符出现次数
charCounts.collect() // 返回集合到driver
分析
textFile --> flatMap --> map --> reduceByKey: stage-0
reduceByKey --> filter -->flatMap --> map --> reduceByKey: stage-1
reduceByKey --> collect: stage-2

每个stage最后都会有shuffle write,数据被写到磁盘上,然后下游stage会通过网络拉取数据。
所以有时候减少shuffle的产生是很有必要的,因为写磁盘和网络IO很大程度上是导致程序慢的原因。
当然有时候通过shuffle来repartition,增加并行度也是有必要的。


2、选择合适的算子
首要的目标是要选择合适的算子操作来减少shuffle的产生。repartition, join, cogroup, 和*By, *ByKey都会产生shuffle。
不过这些算子还是有不同的地方,有些操作具有同样结果,但效率不一样。
一、避免使用groupByKey
rdd.groupByKey().mapValues(_.sum)和rdd.reduceByKey(_ + _)的效果是一样的,但是后者要好些。
如有PairRDD("apple", 1),("apple", 1),("banana", 1),("orange", 1),("banana", 1),("apple", 1)
对于groupBykey来说就要把这些数先写到磁盘上,下游再通过网络进行拉取
对于reduceByKey来说先在内存中做本地聚合,所以shuffle的量也就是("apple", 3),("banana", 2),("orange", 1)这样数量就少了。
reduceByKey会在本地聚合,减少了shuffle的量,也就减少了网络和磁盘IO和下游计算要消耗的内存量。

二、避免使用reduceByKey当输入和输出值的类型不一样的时候。
如我们要分网站Host来计算用户的UV:
("www.baidu.com", "UserId1")
("www.sina.com.cn", "UserId1")
("www.baidu.com", "UserId2")
("www.baidu.com", "UserId3")
("www.sina.com.cn", "UserId4")
我们的输入是(String, String)类型的RDD
需要的结果是(String, Long)或(String, Int)类型的RDD
方法一:
我们为每个pairRDD都创建一个Set对象, 然后具有相同key的集合合并,最后获取集合的size,结果就是(host, uv)。
虽然reduceByKey会进本地聚合,但是创建了太多不必要的Set对象
rdd.map(kv => (kv._1, collection.mutable.Set[String]() + kv._2)).reduceByKey(_++_).mapValues(_.size)


方法二:
和reduceByKey一样会先进行本地的聚合,但有seqOp和combOp
seqOp用于同一分区的聚合-----将key相同的pair的值放入同一个Set, combOp聚合各分区结果----Set和Set合并
所以避免创建大量Set对象
rdd.aggregateByKey(collection.mutable.Set[String]())(// zeroValue 初始值
    (set, v) => set += v, // seqOp
    (set1, set2) => set1 ++= set2) // combOp
    .mapValue(_.size)




三、避免使用flatMap-join-groupBy。
直接用cogroup变成(key, (CompactBuffer(), CompactBuffer()))减去先拆分group再合并的开销


3、有时候一些算子没有shuffle如:
rdd1 = someRdd.reduceByKey(...)
rdd2 = someOtherRdd.reduceByKey(...)
rdd3 = rdd1.join(rdd2)
如果rdd1和rdd2采用同样的partitioner(如默认的HashPartitioner)而且结果分区数量也相同那么在join的时候不必再进行shuffle了,
因为join数据的分区是分别来自rdd1和rdd2中的单一一个分区。

rdd1:
partition1---->join的partition1
partition2---->join的partition2
partition3---->join的partition3
rdd2:
partition1---->join的partition1
partition2---->join的partition2
partition3---->join的partition3


4、什么时候需要shuffle?
There is an occasional exception to the rule of minimizing the number of shuffles. An extra shuffle can be advantageous to performance when it increases parallelism.当输入的文件不可被切分,而且文件很大的时候,RDD的分区就很少,不能充分的利用分配的cores。因为一个executor分配多个cores,一个cores执行一个task,一个task对应一个partition。这种情况利用repartition来增加分区,这时候会产生shuffle。有时候我们需要先跑临时数据,将临时数据存储,我们会将数据压缩成gz文件。但是gz文件再读的时候是不可分割的,且如果数据有很多相同,压缩后的文件大小可能很小,但实际上数据条数非常多,这样导致下一个Spark程序读数据时会产生数据倾斜。所以第一个程序的最后最好先repartition一下再save。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值