概述
翻译Cloudera关于Spark调优方面的一篇博客How-to: Tune Your Apache Spark Jobs (Part 1),虽然文章写于两年前,但其内容并不过时。
How Spark Executes Your Program
这部分介绍了Spark中的一些基础概念,如driver 、job、stage、task等,以及Spark的运行机制。充分理解这部分内容是做任何调优的前提,但是我其他的博客进行了介绍,所以略过,感兴趣的同学请查看原文。
Picking the Right Operators
写Spark程序就是运用各种transformation和action完成业务逻辑,使用不同的transformation可以达到相同的效果,但背后的性能可能迥然不同。
Spark中的DataFrame、DataSet(以及过时的SchemaRDD)会使用Catalyst优化器优化程序,尽量使用最合适的transformation,这也降低了Spark的使用门槛,即使不熟悉transformation的行为,仍然可以写出性能可以接受的程序。
选用合适的transformation的主要作用在于,尽量避免Shuffle,Shuffle涉及磁盘及网络IO,是昂贵的操作,可能触发Shuffle的操作主要有 repartition ,join,cogroup,*By,*ByKey等。
避免使用groupByKey
rdd.groupByKey().mapValues(_.sum) |
rdd.reduceByKey(_ + _) |
上面两个代码片段结果一致,但groupByKey进行的是reduce端aggregation,需要将原始数据进行网络传输,reduceByKey会先进行map端aggregation,网络传输的数据会比groupByKey少,因此,如果能够进行map端aggregation的操作,使用reduceByKey。
避免使用reduceByKey(当value的输入输出不同时)
val file = sc.textFile(...) file.flatMap(_.split(" ")).map((_, 1)).reduceByKey(_ + _)
上述代码是WordCount的实现,reduceByKey的value输入是int,输出也是int,这是value输入输出相同的例子。
rdd.map(kv => (kv._1, new Set[String]() + kv._2)) .reduceByKey(_ ++ _)
上述代码,reduceByKey的value输入是String,输出是Set,这是value输入输出不一致的例子。同时,上面的代码会导致过多的Set生成,影响程序效率,可以使用aggregateByKey优化,如下
val zero = new collection.mutable.Set[String]() rdd.aggregateByKey(zero)( (set, v) => set += v, (set1, set2) => set1 ++= set2)
避免flatMap-join-groupBy
存在两个已经grouped by key的RDD,想join他们并同时保持grouped,只需使用cogroup,此外,join也是使用cogroup实现。
When Shuffles Don’t Happen
两个RDD执行join操作,会不会发生Shuffle呢?
val data = sc.parallelize(List("a c", "a b", "b c", "b d", "c d")) val wordcount = data.flatMap(_.split(" ")).map((_, 1)).reduceByKey(_ + _, 2) val wordcount2 = data.flatMap(_.split(" ")).map((_, 1)).reduceByKey(_ + _, 2) wordcount.join(wordcount2).collect()
上述代码通过reduceByKey参数设置了wordcount 和wordcount2的Partition数,均为2,其DAG如下
val data = sc.parallelize(List("a c", "a b", "b c", "b d", "c d")) val wordcount = data.flatMap(_.split(" ")).map((_, 1)).reduceByKey(_ + _, 2) val wordcount2 = data.flatMap(_.split(" ")).map((_, 1)).reduceByKey(_ + _, 3) wordcount.join(wordcount2).collect()
修改wordcount2对应的reduceByKey的参数,Partition设置为3,此时其DAG如下
对比两个DAG,第二个例子仅修改了Partition数,就多产生一次Shuffle,那么join何时需要Shuffle,何时不需要呢?
wordcount、wordcount2的Partition数 | 是否需要Shuffle |
相等 | 否 |
不相等 | 是 |
当wordcount、wordcount2的Partition数相等时,只需将其对应Partition聚合就可以,如下
当wordcount、wordcount2的Partition数不相等时,需要进行一次Shuffle使两者相等,然后再进行聚合,如下
这也解释了为什么第二个DAG会多一次Shuffle。
When More Shuffles are Better
Shuffle操作是昂贵的,但下面的情形,可以考虑做一下trade off。
- 提升Partition数,增大并行度。
Secondary Sort
Mapreduce原理中,map端会根据Key的hashCode对Key进行排序,reduce端进行merge-sort使得Key最终有序,可以通过自定义Key的方式控制结果的顺序,这就是二次排序(Secondary Sort),具体参考What is secondary sort in Hadoop, and how does it work?。
Spark中不再有二次排序的概念,不同的ShuffleWriter实现行为也不一致,只有SortShuffleWriter会进行map端的sort,并且reduce不会进行merge-sort,下面的issue讨论了reduce端merge-sort的问题,最终因为效率问题不了了之,具体请参考Add MR-style (merge-sort) SortShuffleReader for sort-based shuffle。
虽然Spark不再有二次排序,但其有个transformation,repartitionAndSortWithinPartitions ,通过一次Shuffle达到reparation和sortBy的效果,比使用repartition(..).sortBy(…)效率高。
参考:
How-to: Tune Your Apache Spark Jobs (Part 1)
What is secondary sort in Hadoop, and how does it work?