SparkCore开发调优
避免创建重复的RDD
Spark需要从HDFS上两次加载hello.txt文件的内容,并创建两个单独的RDD;第二次加载HDFS文件以及创建RDD的性能开销,很明显是白白浪费掉的。
val rdd1 = sc.textFile("hdfs://master:9000/hello.txt") rdd1.map(...) val rdd2 = sc.textFile("hdfs://master:9000/hello.txt") rdd2.reduce(...) |
正确使用。
val rdd1 = sc.textFile("hdfs://192.168.0.1:9000/hello.txt") rdd1.map(...) rdd1.reduce(...) |
对多次使用的RDD进行缓冲持久化
如果要对一个RDD进行持久化,只要对这个RDD调用cache()和persist()即可。
缓存,使用非序列化的方式将RDD中的数据全部持久化到内存中。
只有在第一次执行map算子时,才会将这个rdd1从源头处计算一次。第二次执行reduce算子时,就会直接从内存中提取数据进行计算,不会重复计算一个rdd。
val rdd1 = sc.textFile("hdfs://192.168.0.1:9000/hello.txt").cache() rdd1.map(...) rdd1.reduce(...) |
持久化,一共有12种持久化的方式。基于内存、磁盘、序列化、副本。
val rdd1 = sc .textFile("hdfs://192.168.0.1:9000/hello.txt").persist(StorageLevel.MEMORY_AND_DISK_SER) rdd1.map(...) rdd1.reduce(...) |
尽量避免使用shuffle类算子(广播变量)
尽可能避免使用reduceByKey、join、distinct、repartition等会进行shuffle的算子。
可以通过广播变量的方式将数据量较小的RDD广播出去。广播大变量,小数据。
val rdd1Data = rdd1.collect() val rdd1DataBroadCast = sc.broadcast(rdd1Data) rdd2.map(rdd1DataBroadCast...) |
使用性能较高的算子
reduceByKey替代groupByKey,因为reduceByKey会先进行一次局部聚合。
foreachPartitions替代foreach,例如在将数据写入数据库时,如果使用foreach每条数据都会连接一次数据库,频繁的连接数据库,势必会造成性能低下,但是使用foreachPartitions每个分区连接一次数据库,进行批量插入操作,性能较高。
filter之后进行coalesce操作(过略的脏数据比较多时),因为filter之后,RDD的每个partition中都会有很多数据被过滤掉,此时如果照常进行后续的计算,其实每个task处理的partition中的数据量并不是很多,有一点资源浪费,而且此时处理的task越多,可能速度反而越慢。因此用coalesce减少partition数量,将RDD中的数据压缩到更少的partition之后,只要使用更少的task即可处理完所有的partition。在某些场景下,对于性能的提升会有一定的帮助。
使用Kryo序列化
在Spark中,主要有三个地方涉及到了序列化:
在算子函数中使用到外部变量时,该变量会被序列化后进行网络传输,如广播变量。
将自定义的类型作为RDD的泛型类型时(比如Student是自定义类型),所有自定义类型对象,都会进行序列化。因此这种情况下,也要求自定义的类必须实现Serializable接口。或者定义成样例类,默认实现了序列化接口。
使用可序列化的持久化策略时(比如MEMORY_ONLY_SER),Spark会将RDD中的每个partition都序列化成一个大的字节数组。
Spark默认使用的是Java的序列化机制,但是Spark同时支持使用Kryo序列化库,Kryo序列化类库的性能比Java序列化类库的性能要高很多。官方介绍,Kryo序列化机制比Java序列化机制,性能高10倍左右。Spark之所以默认没有使用Kryo作为序列化类库,是因为Kryo要求最好要注册所有需要进行序列化的自定义类型,因此对于开发者来说,这种方式比较麻烦。但是Spark同时支持使用Kryo序列化库,Kryo序列化类库的性能比Java序列化类库的性能要高很多。官方介绍,Kryo序列化机制比Java序列化机制,性能高10倍左右。Spark之所以默认没有使用Kryo作为序列化类库,是因为Kryo要求最好要注册所有需要进行序列化的自定义类型,因此对于开发者来说,这种方式比较麻烦。
/ 创建SparkConf对象。 val conf = new SparkConf().setMaster(...).setAppName(...) // 设置序列化器为KryoSerializer。 conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer") // 注册要序列化的自定义类型。 conf.registerKryoClasses(Array(classOf[MyClass1], classOf[MyClass2])) |
SparkCore解决数据倾斜
数据倾斜发生的场景
绝大多数task执行得都非常快,但个别task执行极慢。比如,总共有1000个task,997个task都在1分钟之内执行完了,但是剩余两三个task却要一两个小时。
某个分区的数据异常的多。所以一个Task处理的时间也就比较长。数据倾斜一般发生在Shuffle过程中。
自定义分区器。
提高shuffle操作的并行度
说白了就是增多分区的个数。让Task增多。让之前一个分区中的数据分散成多个分区。但是这种方式对于某个同key值比较多的数据,显然是不适用的。读取文件的时候,无论是读取HDFS上的文件还是读取本地的文件都,Task的个数都和文件个数有关。
Hehe heheehhehehahahahaahhaahheihei 加入它们都在一个分区中。我增加我的分区个数。
he、ha、hei
代码实现:
val func = (index:Int,iter:Iterator[(String,Int)]) => { iter.map(index+":"+_) }
val func2 = (index:Int,iter:Iterator[String]) => { iter.map(index+":"+_) }
def main(args: Array[String]): Unit = { val conf = new SparkConf() .setAppName("haha") .setMaster("local[1]") .set("spark.testing.memory", "2147480000") val sc = new SparkContext(conf) val lines = sc.textFile("D:\\abc\\wordcount\\input") lines.flatMap(_.split(" ")).map((_,1)).reduceByKey(_+_,10) .mapPartitionsWithIndex(func) .foreach(println)
val lines2 = sc.makeRDD(List("tom","jim","susan")) lines2.mapPartitionsWithIndex(func2) .foreach(println) } |
加盐打散数据局部聚合、去盐再全局聚合。进行了两次聚合任务,但对于有某个key值数据倾斜比较严重时,这么做是值得的。
加盐的好处:使用默认的HashPartitioner,不用自定义分区。
坏处:要去盐。
自定义分区:返回随机的分区号。
Hello 1 2 3
1 hello 10
2 hello 20
代码实现。
val func = (index:Int,iter:Iterator[(String,Int)]) => { iter.map(index+":"+_) }
def main(args: Array[String]): Unit = { val conf = new SparkConf() .setAppName("haha") .setMaster("local[3]") .set("spark.testing.memory", "2147480000") val sc = new SparkContext(conf)
val lines = sc.makeRDD(List("aa","aa","aa","aa","aa","aa","aa")) //优化前 lines.map((_,1)) .reduceByKey(_+_) .mapPartitionsWithIndex(func) .foreach(println)
//优化后 val reduceData = lines.map(x => { val i: Int = Random.nextInt(3) (i.toString + "-" + x, 1) }).reduceByKey(_ + _) //拼上随机前缀聚合后 reduceData .mapPartitionsWithIndex(func) .foreach(println) //去掉随机前缀,再次聚合后 reduceData .map(x => (x._1.substring(2),x._2)) .reduceByKey(_+_) .mapPartitionsWithIndex(func) .foreach(println) } |
大小表join时使用广播变量。将某一个RDD比较小的数据广播出去。
def main(args: Array[String]): Unit = { val conf = new SparkConf() .setAppName("haha") .setMaster("local[3]") .set("spark.testing.memory", "2147480000") val sc = new SparkContext(conf) val weight = sc.textFile("D:\\abc\\join\\weigth.txt") .map(line => { val arr = line.split(",") (arr(0),arr(1)) }).collect().toMap // 将体重数据广播出去,小数据 val broadData = sc.broadcast(weight)
val student = sc.textFile("D:\\abc\\join\\student.txt") .map(line => { val arr = line.split(",") //通过广播变量避免了使用join算子执行出现的shuffle操作 val weightData = broadData.value val str = weightData.get(arr(0)).get (arr(0),arr(1),str) }).foreach(println) } |
抽取出倾斜的key,拆分RDD,加盐再join。适用于一个RDD/Hive表中的少数几个key的数据量过大,而另一个RDD/Hive表中的所有key都分布比较均匀。
步骤如下。
通过sample算子采样出一份样本来,然后统计一下每个key的数量,计算出来数据量最大的是哪几个key。
然后将这几个key对应的数据从原来的RDD中拆分出来,形成一个单独的RDD,并给每个key都打上n以内的随机数作为前缀,而不会导致倾斜的大部分key形成另外一个RDD。
接着将需要join的另一个RDD,也过滤出来那几个倾斜key对应的数据并形成一个单独的RDD,将每条数据膨胀成n条数据,这n条数据都按顺序附加一个0~n的前缀,不会导致倾斜的大部分key也形成另外一个RDD。
再将附加了随机前缀的独立RDD与另一个膨胀n倍的独立RDD进行join,此时就可以将原先相同的key打散成n份,分散到多个task中去进行join了。
而另外两个普通的RDD就照常join即可。
最后将两次join的结果使用union算子合并起来即可,就是最终的join结果。
关于sample算子。
sample(withReplacement : scala.Boolean, fraction : scala.Double,seed scala.Long)
sample算子时用来抽样用的,其有3个参数
withReplacement:表示抽出样本后是否在放回去,true表示会放回去,这也就意味着抽出的样本可能有重复。
fraction :抽出多少,这是一个double类型的参数,0-1之间,eg:0.3表示抽出30%
seed:表示一个种子,根据这个seed随机抽取,一般情况下只用前两个参数就可以,那么这个参数是干嘛的呢,这个参数一般用于调试,有时候不知道是程序出问题还是数据出了问题,就可以将这个参数设置为定值。
代码演示。
def main(args: Array[String]): Unit = { val conf=new SparkConf().setAppName("WordCount").setMaster("local[2]") val sc =new SparkContext(conf) val keys = getKeyBySample(sc) System.out.println("导致数据倾斜的key是:" + keys) } def getKeyBySample(sc:SparkContext)={ val data = Array("A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A", "A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A", "A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A", "B","B","B","B","B","B","B","B","C","D","E","F","G") val rdd= sc.parallelize(data)
val tupleRdd: Array[(Int, String)] =rdd.map(line=>(line,1)) //变成 (word,1) 的形式 .sample(true,0.4) //采样,取40%做样本 .reduceByKey((x,y)=>x+y) //单词统计 结果类似 (A,8),(B,18) .map(line=>(line._2,line._1)) //==>交换顺序(8,A) ,(18,B) 为了便于后面把单词次数多的,排序筛选出来 .sortBy(line=>line._1,false,2) //排序,按照单词频次倒排 .take(3) //取出来单词数前三的 ,这些可能就是脏key 后者热点key for(ele <- tupleRdd){ println(ele._2+"===的个数为======"+ele._1) } } |
资源调优
executor-cores
total-executor-cores
executor-memory
driver-memory
该参数用于设置Driver进程的内存,如果需要使用collect算子,比如广播变量时,将RDD的数据全部拉取到Driver上进行处理,那么必须确保Driver的内存足够大,否则会出现OOM内存溢出的问题。
spark.default.parallelism
该参数用于设置每个stage的默认task数量,默认是一个HDFS block对应一个task,Spark官网建议的设置原则是,设置该参数为num-executors * executor-cores的2~3倍较为合适。