Spark调优
更好的序列化实现
Spark用到序列化的地方
1)Shuffle时需要将对象写入到外部的临时文件。
2)每个Partition中的数据要发送到worker上,spark先把RDD包装成task对象,将task通过网络发给worker。
3)RDD如果支持内存+硬盘,只要往硬盘中写数据也会涉及序列化。
默认使用的是java的序列化。但java的序列化有两个问题,一个是性能相对比较低,另外它序列化完二进制的内容长度也比较大,造成网络传输时间拉长。业界现在有很多更好的实现,如kryo,比java的序列化快10倍以上。而且生成内容长度也短。时间快,空间小,自然选择它了。
方法一:修改spark-defaults.conf配置文件
设置:
spark.serializer org.apache.spark.serializer.KryoSerializer
注意:用空格隔开
方法二:启动spark-shell或者spark-submit时配置
–conf spark.serializer=org.apache.spark.serializer.KryoSerializer
方法三:在代码中
val conf = new SparkConf()
conf.set(“spark.serializer”,“org.apache.spark.serializer.KryoSerializer”)
三种方式实现效果相同,优先级越来越高。
通过代码使用Kryo
文件数据:
rose 23
tom 25
Person类代码:
class Person(var1:String,var2:Int) extends Serializable{
var name=var1 var age=var2
}
MyKryoRegister类代码:
import org.apache.spark.serializer.KryoRegistrator import com.esotericsoftware.kryo.Kryo
class MyKryoRegister extends KryoRegistrator { def registerClasses(kryo: Kryo): Unit = { kryo.register(classOf[Person])
}
}
KryoDriver代码:
import org.apache.spark.SparkConf import org.apache.spark.SparkContext
import org.apache.spark.storage.StorageLevel
object KryoDriver {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local").setAppName("kryoTest")
.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
.set("spark.kryo.registrator", "cn.tedu.MyKryoRegister")
val sc = new SparkContext(conf)
val data=sc.textFile("d://people.txt")
val userData=data.map { x => new Person(x.split(" ")(0),x.split(" ")(1).toInt) }
userData.persist(StorageLevel.MEMORY_AND_DISK)
userData.foreach(x=>println(x.name))
}
}
配置多临时文件目录
spark.local.dir参数。当shuffle、归并排序(sort、merge)时都会产生临时文件。这些临时文件都在这个指定的目录下。那这个文件夹有很多临时文件,如果都发生读写操作,有的线程在读这个文件,有的线程在往这个文件里写,磁盘I/O性能就非常低。
怎么解决呢?可以创建多个文件夹,每个文件夹都对应一个真实的硬盘。假如原来是3个程序同时读写一个硬盘,效率肯定低,现在让三个程序分别读取3个磁盘,这样冲突减少,效率就提高了。这样就有效提高外部文件读和写的效率。怎么配置呢?只需要在这个配置时配置多个路径就可以。中间用逗号分隔。
spark.local.dir=/home/tmp,/home/tmp2
启用推测执行机制
可以设置spark.speculation true
开启后,spark会检测执行较慢的Task,并复制这个Task在其他节点运行,最后哪个节点先运行完,就用其结果,然后将慢Task 杀死
collect速度慢
我们在讲的时候一直强调,collect只适合在测试时,因为把结果都收集到Driver服务器上,数据要跨网络传输,同时要求Driver服务器内存大,所以收集过程慢。解决办法就是直接输出到分布式文件系统中。
有些情况下,RDD操作使用MapPartitions替代map
map方法对RDD的每一条记录逐一操作。mapPartitions是对RDD里的每个分区操作rdd.map{ x=>conn=getDBConn.conn;write(x.toString);conn close;}
这样频繁的链接、断开数据库,效率差。rdd.mapPartitions{(record:=>conn.getDBConn;for(item<-recorders; write(item.toString);conn close;}
这样就一次链接一次断开,中间批量操作,效率提升。
Checkpoint机制
概述
checkpoint的意思就是建立检查点,类似于快照,例如在spark计算里面
计算流程DAG特别长,服务器需要将整个DAG计算完成得出结果,但是如果在这很长的计算流程中突然中间算出的数据丢失
了,spark又会根据RDD的依赖关系从头到尾计算一遍,这样子就很费性能,当然我们可以将中间的计算结果通过cache或者persist放到内存或者磁盘中,但是这样也不能保证数据完全不会丢失,存储的这个内存出问题了或者磁盘坏了,也会导致spark从头再根据RDD计算一遍,所以就有了checkpoint,
其中checkpoint的作用就是将DAG中比较重要的中间数据做一个检查点将结果存储到一个高可用的地方
代码示例:
object Driver2 {
def main(args: Array[String]): Unit = {
val conf=new SparkConf().setMaster("local").setAppName("wordcount") val sc=new SparkContext(conf)
sc.setCheckpointDir("hdfs://hadoop01:9000/check01")
val data=sc.textFile("d://data/word.txt")
data.cache() data.checkpoint()
val wordcount=data.flatMap {_.split(" ")}.map {(_,1)}.reduceByKey(_+_)
wordcount.cache() wordcount.checkpoint()
wordcount.foreach{println}
}
}
总结:Spark的CheckPoint机制很重要,也很常用,尤其在机器学习中的一些迭代算法中很常 见。比如一个算法迭代10000次,如果不适用缓冲机制,如果某分区数据丢失,会导致整个计算
链重新计算,所以引入缓存机制。但是光引入缓存,也不完全可靠,比如缓存丢失或缓存存储不下,也会导致重新计算,所以使用CheckPoint机制再做一层保证。
补充:检查目录的路径,一般都是设置到HDFS上
Spark懒执行的意义
Spark中,Transformation方法都是懒操作方法,比如map,flatMap,reduceByKey等。当触发某个Action操作时才真正执行。
懒操作的意义:
①不运行job就触发计算,避免了大量的无意义的计算,即避免了大量的无意义的中间结果的产生,即避免产生无意义的磁盘I/O及网络传输
②更深层次的意义在于,执行运算时,看到之前的计算操作越多,执行优化的可能性就越高
Spark共享变量
概述
Spark程序的大部分操作都是RDD操作,通过传入函数给RDD操作函数来计算。这些函数在 不同的节点上并发执行,但每个内部的变量有不同的作用域,不能相互访问,所以有时会不太方便,Spark提供了两类共享变量供编程使用——广播变量和计数器。
1.广播变量
这是一个只读对象,在所有节点上都有一份缓存,创建方法是SparkContext.broadcast(), 比如:
scala> val broadcastVar = sc.broadcast(Array(1, 2, 3))
broadcastVar: org.apache.spark.broadcast.Broadcast[Array[Int]] = Broadcast(0) scala> broadcastVar.value
res0: Array[Int] = Array(1, 2, 3)
注意,广播变量是只读的,所以创建之后再更新它的值是没有意义的,一般用val修饰符来定义广播变量。
2.计数器
计数器只能增加,是共享变量,用于计数或求和。
计数器变量的创建方法是SparkContext.accumulator(v, name),其中v是初始值,name是名称。
示例如下:
scala> val accum = sc.accumulator(0, “My Accumulator”) accum: org.apache.spark.Accumulator[Int] = 0
scala> sc.parallelize(Array(1, 2, 3, 4)).foreach(x => accum += x) scala> accum.value
res1: Int = 10
spark解决数据倾斜问题
文件1
id name
1 tom
2 rose
文件2
id school sno
1 s1 211
2 s2 222
3 s3 233
4 s2 244
期望得到的数据 :
1tom s1
2rose s2
将少量的数据转化为Map进行广播,广播会将此 Map 发送到每个节点中,如果不进行广播, 每个task执行时都会去获取该Map数据,造成了性能浪费。
完整代码
import org.apache.spark.{SparkContext, SparkConf} import scala.collection.mutable.ArrayBuffer
object joinTest extends App{
val conf = new SparkConf().setMaster("local[2]").setAppName("test") val sc = new SparkContext(conf)
/**
*map-side-join
*取出小表中出现的用户与大表关联后取出所需要的信息
* */
//-- collectAsMap() 可以将(key,value) 转成Map(key->value)
val people_info = sc.parallelize(Array(("1","tom"),("2","rose"))).collectAsMap() val student_all = sc.parallelize(Array(("1","s1","211"),
("1","s2","222"),
("1","s3","233"),
("1","s2","244")))
//将需要关联的小表进行广播
val people_bc = sc.broadcast(people_info)
/**
*使用mapPartition而不是用map,减少创建broadCastMap.value的空间消耗
*同时匹配不到的数据也不需要返回()
* */
val res = student_all.mapPartitions(iter =>{
//获取小表的数据
val stuMap = people_bc.value
val arrayBuffer = ArrayBuffer[(String,String,String)]()
//做两表的操作
iter.foreach{case (idCard,school,sno) =>{ if(stuMap.contains(idCard)){
arrayBuffer.+= ((idCard, stuMap.getOrElse(idCard,""),school))
}
}}
arrayBuffer.iterator
})