文章作者邮箱:yugongshiye@sina.cn 地址:广东惠州
▲ 本章节目的
⚪ 掌握Spark的——更好的序列化实现;
⚪ 掌握Spark的——通过代码使用Kryo;
⚪ 掌握Spark的——配置多临时文件目录;
⚪ 掌握Spark的——启用推测执行机制;
⚪ 掌握Spark的——避免使用collect;
⚪ 掌握Spark的——RDD操作使用MapPartitions替代map;
⚪ 掌握Spark的——Spark共享变量;
一、Spark调优—上篇
1. 更好的序列化实现
Spark用到序列化的地方
1. Shuffle时需要将对象写入到外部的临时文件。
2. 每个Partition中的数据要发送到worker上,spark先把RDD包装成task对象,将task通过网络发给worker。
3. RDD如果支持内存+硬盘,只要往硬盘中写数据也会涉及序列化。
默认使用的是java的序列化。但java的序列化有两个问题:
1. 性能相对比较低,
2. 序列化完二进制的内容长度也比较大,造成网络传输时间拉长。
业界现在选择更好的实现为 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”)
备注:三种方式实现效果相同,优先级越来越高。
2. 通过代码使用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调优—下篇
1. 配置多临时文件目录
spark.local.dir参数。当shuffle、归并排序(sort、merge)时都会产生临时文件。这些临时文件都在这个指定的目录下。那这个文件夹有很多临时文件,如果都发生读写操作,有的线程在读这个文件,有的线程在往这个文件里写,磁盘I/O性能就非常低。
怎么解决呢?可以创建多个文件夹,每个文件夹都对应一个真实的硬盘。假如原来是3个程序同时读写一个硬盘,效率肯定低,现在让三个程序分别读取3个磁盘,这样冲突减少,效率就提高了。这样就有效提高外部文件读和写的效率。怎么配置呢?只需要在这个配置时配置多个路径就可以。中间用逗号分隔。
spark.local.dir=/home/tmp,/home/tmp2
2. 启用推测执行机制
可以设置spark.speculation true
开启后,spark会检测执行较慢的Task,并复制这个Task在其他节点运行,最后哪个节点先运行完,就用其结果,然后将慢Task 杀死。
3. 避免使用collect
我们在讲的时候一直强调,collect只适合在测试时,因为把结果都收集到Driver服务器上,数据要跨网络传输,同时要求Driver服务器内存大,所以收集过程慢。解决办法就是直接输出到分布式文件系统中。
4. 有些情况下,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;}
这样就一次链接一次断开,中间批量操作,效率提升。
三、Spark共享变量
1. 概述
Spark程序的大部分操作都是RDD操作,通过传入函数给RDD操作函数来计算。这些函数在不同的节点上并发执行,但每个内部的变量有不同的作用域,不能相互访问,所以有时会不太方便,Spark提供了两类共享变量供编程使用——广播变量和计数器。
2. 广播变量
这是一个只读对象,在所有节点上都有一份缓存,创建方法是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修饰符来定义广播变量。
3. 计数器
计数器只能增加,是共享变量,用于计数或求和。
计数器变量的创建方法是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