1、从数据倾斜的角度进行Spark的调优
1、使用Hive ETL预处理数据
主要目的就是是数据倾斜发生在Hive阶段发生,因为Spark可以和Hive整合在一起,去读取Hive中的数据表,加载数据。
2、过滤少数导致倾斜的key
就是将那些对于计算没有任何影响的不重要的key提前进行过滤,从而能过减少数据倾斜。
3、提高shuffle操作的并行度
增加shuffle read task的数量,可以让原本分配给一个task的多个key分配给多个 task,从而让每个task处理比原来更少的数据。举例来说,如果原本有5个key,每个key对应10条 数据,这5个key都是分配给一个task的,那么这个task就要处理50条数据。而增加了shuffle read task以后,每个task就分配到一个key,即每个task就处理10条数据,那么自然每个task的执行时 间都会变短了
4、双重聚合
将原本相同的key通过附加随机前缀的方式,变成多个不同的key,就可以让原本被 一个task处理的数据分散到多个task上去做局部聚合,进而解决单个task处理数据量过多的问题。 接着去除掉随机前缀,再次进行全局聚合,即使会数据倾斜,数据量相比较之前会少很多,可以忽略不计,就可以得到最终的结果
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
import scala.util.Random
object Demo06DoubleReduce {
/**
* 双重聚合
* 一般适用于 业务不复杂的情况
*
*/
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setMaster("local").setAppName("app")
val sc: SparkContext = new SparkContext(conf)
val lines: RDD[String] = sc.textFile("spark/data/word")
val wordRDD: RDD[String] = lines
.flatMap(_.split(","))
.filter(!_.equals(""))
// 通过抽样找到会照成数据倾斜的key 打上5以内随机前缀
val top1: Array[(String, Int)] = wordRDD
.sample(withReplacement = true, 0.1)
.map((_, 1))
.reduceByKey(_ + _)
.sortBy(-_._2)
.take(1)
//导致数据倾斜额key
val key: String = top1(0)._1
println("导致数据倾斜的key:" + key)
wordRDD.map(word => {
if (key.equals(word)) {
val pix: Int = Random.nextInt(3) // 一般跟Reduce数量保持一致
(pix + "-" + word, 1)
} else {
(word, 1)
}
})
.groupByKey() //第一次聚合
.map(t => (t._1, t._2.toList.sum))
.map(t => {
// 去掉随机前缀
if (t._1.contains("-")) {
(t._1.split("-")(1), t._2)
} else {
t
}
})
.groupByKey() //第二次聚合
.map(t => (t._1, t._2.toList.sum))
.foreach(println)
while (true) {
}
}
}
5、将reduce join转为map join
普通的join是会走shuffle过程的,而一旦shuffle,就相当于会将相同key的数据拉 取到一个shuffle read task中再进行join,此时就是reduce join。但是如果一个RDD是比较小的, 则可以采用广播小RDD全量数据+map算子来实现与join同样的效果,也就是map join,此时就不 会发生shuffle操作,也就不会发生数据倾斜
import org.apache.spark.broadcast.Broadcast
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object Demo07MapJoin {
/**
* map join
*
* 将小表广播,大表使用map算子
*
* 1、小表不能太大
* 2、如果driver内存不足,需要手动设置 如果广播变量大小超过了driver内存大小,会出现oom
*
*/
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setMaster("local").setAppName("Demo07MapJoin")
val sc: SparkContext = new SparkContext(conf)
//RDD 不能广播
val studentRDD: RDD[String] = sc.textFile("spark/data/stu/students.txt")
//将数据拉去到driver端,变成一个map集合
val stuMap: Map[String, String] = studentRDD
.map(s => (s.split(",")(0), s))
.collect() //将rdd的数据拉取Driver端变成一个数组
.toMap
//广播map集合
val broStu: Broadcast[Map[String, String]] = sc.broadcast(stuMap)
val scoreRDD: RDD[String] = sc.textFile("spark/data/score.txt")
//循环大表,通过key获取小表信息
scoreRDD.map(s => {
val sId: String = s.split(",")(0)
//重广播变量里面获取数据
val stuInfo: String = broStu.value.getOrElse(sId, "")
stuInfo + "," + s
}).foreach(println)
while (true) {
}
}
}
6、采样倾斜key并分拆join操作(使用前提是两个都是大表,且其中有一张表有数据倾斜)
对包含少数几个数据量过大的key的那个RDD,通过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结果。
import org.apache.spark.broadcast.Broadcast
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object Demo08DoubleJoin {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("app").setMaster("local")
val sc = new SparkContext(conf)
val dataList1 = List(
("java", 1),
("shujia", 2),
("shujia", 3),
("shujia", 1),
("shujia", 1))
val dataList2 = List(
("java", 100),
("java", 99),
("shujia", 88),
("shujia", 66))
val RDD1: RDD[(String, Int)] = sc.parallelize(dataList1)
val RDD2: RDD[(String, Int)] = sc.parallelize(dataList2)
val sampleRDD: RDD[(String, Int)] = RDD1.sample(false, 1.0)
//skewedKey 导致数据倾斜的key shujia
val skewedKey: String = sampleRDD.map(x => (x._1, 1))
.reduceByKey(_ + _)
.map(x => (x._2, x._1))
.sortByKey(ascending = false)
.take(1)(0)._2
//导致数据倾斜key的RDD
val skewedRDD1: RDD[(String, Int)] = RDD1.filter(tuple => {
tuple._1.equals(skewedKey)
})
//没有倾斜的key
val commonRDD1: RDD[(String, Int)] = RDD1.filter(tuple => {
!tuple._1.equals(skewedKey)
})
val skewedRDD2: RDD[(String, Int)] = RDD2.filter(tuple => {
tuple._1.equals(skewedKey)
})
val commonRDD2: RDD[(String, Int)] = RDD2.filter(tuple => {
!tuple._1.equals(skewedKey)
})
val n = 2
//对产生数据倾斜的key 使用mapjoin
val skewedMap: Map[String, Int] = skewedRDD2.collect().toMap
val bro: Broadcast[Map[String, Int]] = sc.broadcast(skewedMap)
val resultRDD1: RDD[(String, (Int, Int))] = skewedRDD1.map(kv => {
val word: String = kv._1
val i: Int = bro.value.getOrElse(word, 0)
(word, (kv._2, i))
})
//没有数据倾斜的RDD 正常join
val resultRDD2: RDD[(String, (Int, Int))] = commonRDD1.join(commonRDD2)
//将两个结果拼接
resultRDD1.union(resultRDD2)
.foreach(println)
}
}
7、使用随机前缀和扩容RDD进行join
将原先一样的key通过附加随机前缀变成不一样的key,然后就可以将这些处理后的 “不同key”分散到多个task中去处理,而不是让一个task处理大量的相同key。该方案与“解决方 案六”的不同之处就在于,上一种方案是尽量只对少数倾斜key对应的数据进行特殊处理,由于处 理过程需要扩容RDD,因此上一种方案扩容RDD后对内存的占用并不大;而这一种方案是针对有大 量倾斜key的情况,没法将部分key拆分出来进行单独处理,因此只能对整个RDD进行数据扩容,对 内存资源要求很高。
2、从代码的角度进行Spark的调优
1、对多次使用的RDD进行持久化
对多次使用的RDD使用Cache进行缓存,默认的情况下使用的是MEMORY_ONLY这种模式,但是使用这种模式的前提是内存必须是充足的,因为数据都是存储在内存中,就不需要进行序列化和反序列化的操作,所以就避免一些性能的开销,也不需要再去磁盘中读取数据文件,性能更高,但是在实际的生产过程中,会使用MEMORY_AND_DISK_SER策略,数据会首先存在内存中,当内存中不够会将溢出的数据进行一个压缩存储到磁盘中。
2、使用高性能的算子:ReduceBykey、aggregateBykey、mapforeachPartitions、foreachPartition
(注释:使用mapforeachPartitions、foreachPartition的原因是因为当与外部建立连接是没有办法被序列化,因此只能在算子内部进行连接,然后这个连接就会和Task一起发送到Executor上执行,然后一个Exector上会有很多的Task,因此读取一条数据就会建立以一次连接,导致效率变低,因此使用mapforeachPartitions、foreachPartition作用在每一个分区上,这样可以减少连接的次数。)
3、广播大变量
当在Driver端定义的的一个变量需要被算子内部所使用到,因为Task是在Executor上执行的,因此Driver端的变量调用的次数就与Task的数量有关,效率较低,因此使用广播变量,将Driver端需要被调用的变量广播到Executor上,被BlockManager所管理,当每次Task执行时需要调用时,就会去所在的Executor上去拉去数据。
4、使用Kryo优化序列化性能
因为需要进行网络传输,所以需要进行序列化,Spark默认使用的是官方的序列化的方式效率比较低。
在Spark中,主要有三个地方涉及到了序列化:
在算子函数中使用到外部变量时,该变量会被序列化后进行网络传输
将自定义的类型作为RDD的泛型类型时,所有自定义类型对象,都会进行序列化。因此这种情况下,也要求自定义的类必须实现 Serializable接口。
使用可序列化的持久化策略时(比如MEMORY_ONLY_SER),Spark会将RDD中的每个 partition都序列化成一个大的字节数组。
Spark支持使用Kryo序列化机制。Kryo序列化机制,比默认的Java序列化机制,速度要快 ,序列化后的数据要更小,大概是Java序列化机制的1/10。所以Kryo序列化优化以后,可 以让网络传输的数据变少;在集群中耗费的内存资源大大减少。
5、优化数据结构
6、使用高性能的库fastutil
fastutil是扩展了Java标准集合框架(Map、List、Set;HashMap、ArrayList、 HashSet)的类库,提供了特殊类型的map、set、list和queue;
fastutil能够提供更小的内存占用,更快的存取速度;我们使用fastutil提供的集合类,来 替代自己平时使用的JDK的原生的Map、List、Set,好处在于,fastutil集合类,可以减 小内存的占用,并且在进行集合的遍历、根据索引(或者key)获取元素的值和设置元素 的值的时候,提供更快的存取速度;
3、从参数的角度进行Spark的调优
1、数据本地化
Application任务执行流程:在Spark Application提交后,Driver会根据action算子划分成一个个的job,然后对每一 个job划分成一个个的stage,stage内部实际上是由一系列并行计算的task组成的,然后 以TaskSet的形式提交给你TaskScheduler,TaskScheduler在进行分配之前都会计算出 每一个task最优计算位置。Spark的task的分配算法优先将task发布到数据所在的节点上 ,从而达到数据最优计算位置。
数据本地化级别: PROCESS_LOCAL、 NODE_LOCA、 NO_PREF 、RACK_LOCAL、 ANY
配置参数
spark.locality.wait
spark.locality.wait.process
spark.locality.wait.node
spark.locality.wait.rack
2、JVM调优
概述: Spark task执行算子函数,可能会创建很多对象,这些对象,都是要放入JVM年轻代中 RDD的缓存数据也会放入到堆内存中
配置
spark.storage.memoryFraction 默认是0.6
3、shuffle调优
概述: reduceByKey:要把分布在集群各个节点上的数据中的同一个key,对应的values,都给 集中到一个节点的一个executor的一个task中,对集合起来的value执行传入的函数进行 reduce操作,最后变成一个value。
配置
spark.shuffle.manager, 默认是sort
spark.shuffle.consolidateFiles,默认是false
spark.shuffle.file.buffer,默认是32k
spark.shuffle.memoryFraction,默认是0.2
4、调节Executor 对外内存
Spark底层shuffle的传输方式是使用netty传输,netty在进行网络传输的过程会申请堆外内存(netty是零拷贝),所以使用了堆外内存。
1、num-executors : executor的数量
2、executor-memory : (4G-8G)每一个executor分配的内存(在缓存的时候会使用内存(0.6),代码运行会使用内存,shuffle会使用内存(0.2))
3、executor-cores : (2-4)每一个executor分配的核数
4、drover-memory : driver的内存,当广播变量比较大时需要设置
5、--conf spark.storage.memoryFraction=0.6
6、--conf spark.shuffle.memoryFraction=0.2 如果shuffle任务比较重,可以将其设置成0.4,同上一个参数也得要降成0.4
7、--conf spark.sql.shuffle.partitions=200 默认等于200,指定在DF中有shuffle的操作后得到的新DF的分区数
资源设置需要根据数据量和剩余资源的情况来设置
#Spark任务提交的模板:
spark-submit --class org.apache.spark.examples.SparkPi \
--master yarn \
--deploy-mode client
--num-executors 50 \
--executor-memory 4G \
--conf spark.default.parallelism=100 \
--conf spark.storage.memoryFraction=0.4 \
--conf spark.shuffle.memoryFraction=0.4 \
--jars 引用的第三方的jars包
./lib/spark-examples-1.6.0-hadoop2.6.0.jar 参数1 参数2
1、在资源充足的情况下,根据数据量设置资源
比如现在需要处理100G的数据
100G(没有小文件时) ---> 800分区 ---> 800个task
最理想的情况是每个task对应一个core
--num-executors 200
--executor-cores 4
--executor-memory 8G
最合理的设置方式,充分使用资源
--num-executors 100
--executor-cores 4
--executor-memory 8G
2、资源不足时,根据剩余资源指定,(1/3-1/2)
比如现在需要处理100G的数据
100G(没有小文件时) ---> 800分区 ---> 800个task
服务器配置(128G(可用96G),64核)
10台服务器 --> 1000G内存,640核
比如现在yarn闪剩余的资源是(400G,300核)
合理指定资源
--num-executors 25
--executor-cores 4
--executor-memory 8G
yarn上当资源被使用完时,再提交任务会一直等待,直到资源释放