1 概述
官网对于Spark的调优讲解
对于spark的性能调优我推荐大家去看看美团的文章,这里我会据一些例子让大家更好的去理解。
2 调优
这里我列举出美团文章中提出的一些调优,其实都在官网上。对一些难理解的通过代码和图片的方式进行解析。
- 避免创建重复的RDD
- 尽可能复用同一个RDD
- 对多次使用的RDD进行持久化 (这篇文章进行了详细的讲解)
- 尽量避免使用shuffle类算子 (shuffle基础)
- 使用map-side预聚合的shuffle操作
- 使用高性能的算子
- 广播大变量
- 使用Kryo优化序列化性能
- 优化数据结构
- 资源参数调优
3 reduceByKey VS groupByKey(使用map-side预聚合的shuffle操作)
这里通过代码给大家演示下为什么要选择reduceByKey来代替groupByKey
- groupByKey
import org.apache.spark.{SparkConf, SparkContext}
object GroupByKey {
def main(args: Array[String]): Unit = {
val conf=new SparkConf().setAppName("GroupByKey").setMaster("local[2]")
val sc=new SparkContext(conf)
/*
cat input.txt
hello java
hello hadoop
hello hive
hello sqoop
hello hdfs
hello spark
*/
val file=sc.textFile("file:///home/hadoop/data/input.txt")
//通过groupByKey后得到的数据结构
/*
Array[(String, Iterable[Int])] =
Array((hive,CompactBuffer(1)),
(hello,CompactBuffer(1, 1, 1, 1, 1, 1)),
(java,CompactBuffer(1)), (sqoop,CompactBuffer(1)),
(spark,CompactBuffer(1)), (hadoop,CompactBuffer(1)),
(hdfs,CompactBuffer(1)))
*/
file.flatMap(x=>x.split(" ")).map((_,1)).groupByKey()
.map(x=>(x._1,x._2.sum)).collect()
sc.stop()
}
}
结果:
Array[(String, Int)] = Array((hive,1), (hello,6), (java,1), (sqoop,1), (spark,1), (hadoop,1), (hdfs,1))
- reduceByKey
import org.apache.spark.{SparkConf, SparkContext}
object ReduceByKey {
def main(args: Array[String]): Unit = {
val conf=new SparkConf().setAppName("GroupByKey").setMaster("local[2]")
val sc=new SparkContext(conf)
val file=sc.textFile("file:///home/hadoop/data/input.txt")
file.flatMap(x=>x.split(" ")).map((_,1)).reduceByKey((_+_)).collect()
sc.stop()
}
}
结果:
Array[(String, Int)] = Array((hive,1), (hello,6), (java,1), (sqoop,1), (spark,1), (hadoop,1), (hdfs,1))
我们通过两幅图看看两者之间的区别:
groupByKey:
reduceByKey:
总结:
其中第一张图是groupByKey的原理图,可以看到,没有进行任何本地聚合时,所有数据都会在集群节点之间传输;第二张图是reduceByKey的原理图,可以看到,每个节点本地的相同key数据,都进行了预聚合(这里的预聚合不就是mr中的combiner吗),然后才传输到其他节点上进行全局聚合。
我们在通过ui界面更加确定他们的区别了:
- groupByKey的shuffle read:252b
2.reduceByKey的shuffle read:238b
4 Map vs MapPartition
package cn.zhangyu
import java.util.Random
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object MapAndMapPartition {
def main(args: Array[String]): Unit = {
val conf=new SparkConf().setAppName("MapAndMapPartition").setMaster("local[3]")
val sc=new SparkContext(conf)
val users=new Array[String](100)
for (i<- 0 to 99){
users(i)="user"+i
}
//转化为一个RDD
val usersRDD=sc.parallelize(users)
//调用map函数
map(usersRDD)
//mapPartition函数
//mapPartition(usersRDD)
sc.stop()
}
//map函数
/**
* Return a new RDD by applying a function to all elements of this RDD.
* 对每个元素进行操作
*/
def map(usersRDD:RDD[String]): Unit ={
usersRDD.map(x=>{
val connection = DBUtils.getConnection()
println("----------")
DBUtils.returnConnection(connection)
}).foreach(println)
}
//mapPartition
def mapPartition(userRDD:RDD[String]): Unit ={
userRDD.mapPartitions(partition=>{
val connection = DBUtils.getConnection()
println("-------------")
DBUtils.returnConnection(connection)
partition
}).foreach(println)
}
}
//模拟连接数据库
object DBUtils {
def getConnection() = {
new Random().nextInt(10) + ""
}
def returnConnection(connection:String) = {
}
}
mapPartitions类的算子,一次函数调用会处理一个partition所有的数据,而不是一次函数调用处理一条,性能相对来说会高一些。但是有的时候,使用mapPartitions会出现OOM(内存溢出)的问题。因为单次函数调用就要处理掉一个partition所有的数据,如果内存不够,垃圾回收时是无法回收掉太多对象的,很可能出现OOM异常。所以使用这类操作时要慎重!
建议: 再操作数据库的时候使用mapPartitions代替partition,如果出现oom那就意味着你一个partition处理的数据量太大,那就要增加partition的数量。
注意: 其实跑上面一段代码相信大家可能有一个疑问我使用了mapPartitions算子到底有几个partition呢?
如果把setMaster("local[3]")
这一段注释掉那么就是两个partition因为默认就是两个partition我们可以通过setMaster("local[ n]")
去设置。
5 foreachPartitions vs foreach
使用foreachPartitions替代foreach原理类似于“使用mapPartitions替代map”,也是一次函数调用处理一个partition的所有数据,而不是一次函数调用处理一条数据。在实践中发现,foreachPartitions类的算子,对性能的提升还是很有帮助的。比如在foreach函数中,将RDD中所有数据写MySQL,那么如果是普通的foreach算子,就会一条数据一条数据地写,每次函数调用可能就会创建一个数据库连接,此时就势必会频繁地创建和销毁数据库连接,性能是非常低下;但是如果用foreachPartitions算子一次性处理一个partition的数据,那么对于每个partition,只要创建一个数据库连接即可,然后执行批量插入操作,此时性能是比较高的。实践中发现,对于1万条左右的数据量写MySQL,性能可以提升30%以上。
6 广播大变量
官方地址:http://spark.apache.org/docs/latest/rdd-programming-guide.html#broadcast-variables
有时在开发过程中,会遇到需要在算子函数中使用外部变量的场景(尤其是大变量,比如100M以上的大集合),那么此时就应该使用Spark的广播(Broadcast)功能来提升性能。
在算子函数中使用到外部变量时,默认情况下,Spark会将该变量复制多个副本,通过网络传输到task中,此时每个task都有一个变量副本。如果变量本身比较大的话(比如100M,甚至1G),那么大量的变量副本在网络中传输的性能开销,以及在各个节点的Executor中占用过多内存导致的频繁GC,都会极大地影响性能。
因此对于上述情况,如果使用的外部变量比较大,建议使用Spark的广播功能,对该变量进行广播。广播后的变量,会保证每个Executor的内存中,只驻留一份变量副本,而Executor中的task执行时共享该Executor中的那份变量副本。这样的话,可以大大减少变量副本的数量,从而减少网络传输的性能开销,并减少对Executor内存的占用开销,降低GC的频率。
package cn.zhangyu
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
/**
* 通过mapjoin展示广播变量的好处
*/
object MapJoin {
def main(args: Array[String]): Unit = {
val conf=new SparkConf().setAppName("MapJoin").setMaster("local[1]")
val sc=new SparkContext(conf)
//定义一个学生表
val students=sc.parallelize(Array(
(1,"张三"),
(2,"李四"),
(3,"王五"))
).map(x=>(x._1,x))
//定义一个学生信息表
val message=sc.parallelize(Array(
(1,"school1","201"),
(2,"school2","202"),
(3,"school3","203"))
).map(x=>(x._1,x))
//publicdef map[U](f: ((Int, ((Int, String), (Int, String, String))))
// students.join(message).map(x=>((x._1),(x._2._1._2),(x._2._2._2),(x._2._2._3))).collect().foreach(println)
/*
结果:
(1,张三,school1,201)
(3,王五,school3,203)
(2,李四,school2,202)
*/
//广播变量
broadcastJoin(sc)
}
def broadcastJoin(sc:SparkContext): Unit ={
//定义一个学生表
val students=sc.parallelize(Array(
(1,"张三"),
(2,"李四"),
(3,"王五"))
).collectAsMap()
//定义一个学生信息表
val message=sc.parallelize(Array(
(1,"school1","201"),
(2,"school2","202"),
(3,"school3","203"))
).map(x=>(x._1,x))
// Broadcast+map的join操作,不会导致shuffle操作。
// 使用Broadcast将一个数据量较小作为广播变量
//这里要注意,使用broadcast时,不能直接对RDD进行broadcast的操作.
val studentsBroadcast = sc.broadcast(students)
message.map(x => {
val broadcastValue = studentsBroadcast.value // Broadcast[Map[Int, String]] = sc.broadcast(students)
for (value <- broadcastValue if (broadcastValue.equals(x._1)) {
yield (x._1,value._2)
})
}).collect().foreach(println)
}
}
使用Kryo优化序列化性能
官方地址:http://spark.apache.org/docs/latest/tuning.html#data-serialization
- 使用java序列化
package cn.zhangyu
import org.apache.spark.storage.StorageLevel
import org.apache.spark.{SparkConf, SparkContext}
import scala.collection.mutable.ArrayBuffer
object SerializerApp1 {
def main(args: Array[String]): Unit = {
//创建sprakconf
val conf = new SparkConf().setMaster("local[2]").setAppName("SerializerApp1")
//创建sparkContext
val sc = new SparkContext(conf)
//创建一个数组对象
val persons = new ArrayBuffer[Person]()
for (i <- 1 to 1000){
persons += (Person(i,"person"+i,i % 2))
}
//转化为rdd
val rdd = sc.parallelize(persons)
//设置缓存级别
//rdd.persist(StorageLevel.MEMORY_ONLY)
rdd.persist(StorageLevel.MEMORY_ONLY_SER)
//persist 是一个lazy操作 所以这里需要一个action触发
rdd.count()
//删除缓存
//rdd.unpersist()
sc.stop()
}
}
//定义一个case class
case class Person(id:Int , name:String , gender:Int)
这里你可以通过打包上传进行测试,也可以通过spark-shell进行测试:
-
MEMORY_ONLY
缓存级别
-
MEMORY_ONLY_SER
-
使用Kryo序列化
package cn.zhangyu
import org.apache.spark.storage.StorageLevel
import org.apache.spark.{SparkConf, SparkContext}
import scala.collection.mutable.ArrayBuffer
object SerializerApp1 {
def main(args: Array[String]): Unit = {
//创建sprakconf
val conf = new SparkConf().setMaster("local[2]").setAppName("SerializerApp1")
//设置为kryo序列化
conf.set("spark.serializer","org.apache.spark.serializer.KryoSerializer")
//需要进行注册否则不生效
conf.registerKryoClasses(Array(classOf[Person]))
//创建sparkContext
val sc = new SparkContext(conf)
//创建一个数组对象
val persons = new ArrayBuffer[Person]()
for (i <- 1 to 1000){
persons += (Person(i,"person"+i,i % 2))
}
//转化为rdd
val rdd = sc.parallelize(persons)
//设置缓存级别
//rdd.persist(StorageLevel.MEMORY_ONLY)
rdd.persist(StorageLevel.MEMORY_ONLY_SER)
//persist 是一个lazy操作 所以这里需要一个action触发
rdd.count()
//rdd.unpersist()
//为了查看效果让他睡一会
Thread.sleep(100000)
sc.stop()
}
}
//定义一个case class
case class Person(id:Int , name:String , gender:Int)
这里通过spark-submit提交;
[hadoop@hadoop lib]$ spark-submit \
> --class cn.zhangyu.SerializerApp1 \
> --master local[2] \
> --name SerializerApp1 \
> spark_rdd-1.0-SNAPSHOT.jar
可以看到使用默认的序列化(java)大小为33.68k,使用了kryo序列化大小为14.5k。确实效果提升了。 但是大家要注意使用kryo进行序列化时一定要进行注册否则不会生效。