Spark整理(4)
一,广播变量和累加器
1.1 广播变量
- 理解图
广播变量降低了网络传输的压力,一个Exector中对应一份数据即可,task需要可自行到BlockManager中获取,不必每次获取都向Driver获取。
- 注意点
1.不能将RDD当做广播变量广播出去,因为RDD是不存储数据的,可以将RDD的结果广播出去
2.广播变量只能在Driver端定义,不能在Exector端定义
3.在Driver端可以修改广播变量的值,在Exector端无法修改广播变量的值
- scala代码演示
package com.shsxt.spark.test
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object Sp_BroadCast {
def main(args: Array[String]): Unit = {
val conf = new SparkConf()
conf.setMaster("local").setAppName("broadCast")
val sc = new SparkContext(conf)
val list = List(3,2)
//将List存入广播变量中
val broadList = sc.broadcast(list)
val rdd: RDD[Int] = sc.parallelize(List(2,3,4,3,2,8,10))
rdd.filter(x=>{
//在RDD中就可以直接使用了
broadList.value.contains(x)
}).foreach(println)
sc.stop()
}
}
- java代码演示
package com.shsxt.spark.test;
import org.apache.spark.SparkConf;
import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.api.java.JavaSparkContext;
import org.apache.spark.api.java.function.Function;
import org.apache.spark.api.java.function.VoidFunction;
import org.apache.spark.broadcast.Broadcast;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Ja_BroadCast {
public static void main(String args[]){
SparkConf conf = new SparkConf();
conf.setMaster("local").setAppName("broadCast");
JavaSparkContext sc = new JavaSparkContext(conf);
List<Integer> list = new ArrayList<Integer>();
list.add(1);
list.add(2);
Broadcast<List<Integer>> listBroadcast = sc.broadcast(list);
JavaRDD<Integer> javaRDD = sc.parallelize(Arrays.asList(1, 2, 4, 6, 7, 2, 1));
javaRDD.filter(new Function<Integer, Boolean>() {
@Override
public Boolean call(Integer v1) throws Exception {
return listBroadcast.value().contains(v1);
}
}).foreach(new VoidFunction<Integer>() {
@Override
public void call(Integer integer) throws Exception {
System.out.println(integer);
}
});
}
}
1.2 累加器
<!-- 有问题代码 -->
val sum =0 //driver定义的变量
rdd.foreach(x=>{sum+=1}) //executor端执行
println(sum) //结果不准确
-
累加器理解图
-
注意点
累加器在Driver端定义赋初始值,累加器只能在Driver端读取,在Executor端更新
- scala代码实现
package com.shsxt.spark.test
import org.apache.spark.{Accumulator, SparkConf, SparkContext}
object Sp_Accumulator {
def main(args: Array[String]): Unit = {
val conf = new SparkConf()
conf.setMaster("local").setAppName("broadCast")
val sc = new SparkContext(conf)
//定义累加器
val sum: Accumulator[Int] = sc.accumulator(0)
sc.textFile("./wc.txt",2).foreach(x=>{
//更新累加器
sum.add(1)
})
//driver端 获取累加器值
println(sum.value)
}
}
- java代码实现
package com.shsxt.spark.test;
import org.apache.spark.Accumulator;
import org.apache.spark.SparkConf;
import org.apache.spark.api.java.JavaSparkContext;
import org.apache.spark.api.java.function.VoidFunction;
public class Ja_Accumulator {
public static void main(String args[]){
SparkConf conf = new SparkConf();
conf.setMaster("local").setAppName("broadCast");
JavaSparkContext sc = new JavaSparkContext(conf);
Accumulator<Integer> accumulator = sc.accumulator(0);
sc.textFile("./wc.txt",2).foreach(new VoidFunction<String>() {
@Override
public void call(String s) throws Exception {
System.out.println(accumulator);
accumulator.add(1);
}
});
System.out.println(accumulator.value());
sc.stop();
}
}
二,SparkShuffle
2.1 SparkShuffle概念
reduceByKey会将上一个RDD中的每一个key对应的所有的value集合成一个value,然后会生成一个新的RDD,元素类型是<key,value>对的形式,这样每一个key对应一个聚合起来的value.
问题: 聚合之前,每一个Key对应的value不一定都在一个partition上,也不太可能都在同一个节点上,因为RDD是分布式弹性数据集,RDD的partition极有可能分布在各个节点上。
如何聚合?
- shuffle write :上一个stage的每个map task就必须保证将自己处理的当前分区的数据相同的key写入到一个分区文件中,可能会写入多个不同的分区文件中
- shuffle read : reduce task 就会从上一个stage的所有task所在的机器上寻找属于自己的那些分区文件,这样就可以保证每一个Key所对应的value都会汇聚到同一个节点上去处理和聚合。
Spark中有两种类型shuffle类型,HashShuffle和SortShuffle,Spark1.2之前是HashShuffle默认的分区器HashPartitioner, Spark1.2引入SortShuffle默认的分区器是RangePartitioner.
2.2 HashShuffle
- 普通机制
执行流程:
1.每一个map task将不同结果写到不同的buffer中,每个buffer的大小为32k. buffer 起到缓存数据的作用
2.每一个Buffer文件最后对应一个磁盘小文件。
3.reduce task来拉取对应的磁盘小文件
- 总结
1.map task 的计算结果会根据分区器(默认是hashPartitioner)来决定写入到哪一个磁盘小文件中,reducetask会去map端拉取相应的磁盘小文件。
2.产生的磁盘小文件的个数: M(MapTask的个数)*R(reduce Task的个数)
- 存在的问题(产生的小文件过多)
a.在Shuffle Write过程中会产生很多写磁盘小文件的对象
b.在Shuffle read过程中会产生很对读磁盘小文件的对象
c.在JVM堆内存中对象过多会造成频繁的gc,gc后还无法解决运行内存的话,会OOM
d.在数据传输过程中会有频繁的网络通信,频繁的网络通信出现网络通信故障的可能性大大增加,一旦网络通信出现了故障会导致shuffle file cannot find 由于这个错误导致的task失败,TaskScheduler不负责重试,由DAGScheduler负责重试Stage.
-
合并机制
-
*产生的小文件的个数: C(core的个数)R(reduceTask 个数)
2.3 SortShuffle
-
普通机制
-
执行流程
1.mapTask的计算结果会写入到一个内存数据结构里面,内存数据结构的大小默认是5M
2.在shuffle的时候会有一个定时器,不定期的去估算这个内存数据结构的大小,当内存中的数据超过5M时,比如数据大小现在为 5.05MB,那么它会申请5.05*2 - 5=5.1M内存给内存数据结构。
3.如果申请不成功会发生溢写(溢写磁盘),申请成功不会发生溢写。
4.在溢写之前内存结构中的数据会进行排序分区
5.然后开始溢写,写磁盘是batch的形式去写,一个batch是一万条数据
6.mapTask执行完成后,会将这些磁盘小文件合并成一个大的磁盘文件,同时生成一个索引文件
7.reduceTask去map端拉取数据的时候,先去读取索引文件,根据索引文件再去拉取对应的数据。
-
产生磁盘小文件的个数: 2*M (MapTask的个数)
-
bypass机制
-
总结
1.bypass运行机制触发条件如下:shuffle reduce task的个数小于 spark.shuffle.sort.bypassMerageThreshold的参数。这个参数的默认是200.
2.产生的磁盘小文件为: 2*M(MapTask的个数)
2.4 Shuffle文件寻址
2.4.1 MapOutputTracker
MapOutputTracker是Spark架构中的一个模块,是一个主从架构。
管理磁盘小文件的地址:
- MapOutputTrackerMater是主对象,存在Driver中
- MapOutputTrackerWorker是从对象,存在Executor中
2.4.2 BlockManager
BlockManager块管理者,是Spark架构中一个模块,也是一个主从架构
- BlockManagerMaster 主对象,存在与Driver中
BlockManagerMaster会在集群中有用到广播变量和缓存数据或者删除缓存数据时,通知BlockManagerSlave传输或者删除数据。
- BlockManagerWorker 从对象 ,存在Executor中
BlockManagerWorker会与BlockManagerWorker之间通信。
- 无论Driver端还是Exector端的BlockManager都含有4个对象
1.DiskStore对象 负责磁盘的管理
2.MemoryStore 负责内存的管理
3.ConnectionManager 负责连接其他的BlockManagerWorker
4.BlockTransferService 负责数据的传输
2.4.3 shuffle文件寻址图
- 流程
- 当map task执行完成后,会将task执行情况和磁盘小文件的地址封装到MapStatus中,通过MapOutputTrackerWorker对象汇报给Driver中的MapOutputTrackerMaster
- 在所有的map task任务执行完毕后,Driver中就掌握了所有的磁盘小文件的地址
- 在reduce task执行之前,首先会在本地的MapOutputTrackerWorker中获取文件,如果没有,会通过Executor中的MapOutputTrackerWorker向Driver端的MapOutputTrackerMaster请求磁盘小文件的地址
- 获取到文件地址后,会通过BlockManager中的ConnectionManager连接数据节点上的ConnectionManager,然后通过BlockTransferService进行数据的传输。
- BlockTransferService默认启动5个task(线程)去节点拉取数据。默认情况下,5个task拉取数据量不能超过48M.
三,Spark内存管理
Spark执行应用程序时,Spark集群会启动Driver和Executor两种JVM进程。Driver负责创建SaprkContext上下文,提交任务,task的分发等。
Executor负责task的计算任务,并将结果返回给Driver,同时需要为需要持久化的RDD提供存储。Driver端的内存管理比较简单,这里所说的Saprk内存管理针对Executor端的内存管理。
Spark内存管理分为静态内存管理和统一内存管理,Spark1.6之前使用的是静态内存管理,Spark1.6之后引入了统一内存管理。
静态内存管理中存储内存,执行内存和其他内存的大小在Spark应用程序运行期间均为固定的,但用户可以在应用程序启动前进行配置。
统一内存管理和静态内存管理的区别在于存储内存和执行内存共享同一块空间,可以互相借用对方的空间。
Spark1.6以上版本默认使用的是统一内存管理,可以通过参数(spark.memory.useLegacyMode设置为true,默认是false)使用静态内存管理。
3.1 静态内存管理分布图
- 20% task的计算
- 20% shuffle(20% * 80% shuffle的聚合内存 ; 20% *20% 预留内存,防止OOM)
- 60% spark存储内存 (60% * 10% 预留内存,防止OOM ; 60% * 90% * 80% 存储RDD的缓存数据和广播变量 ; 60% * 90% * 20% unroll用于解压序列化数据)
3.2 统一内存管理分布图
- 300M 预留,用于JVM自身运行
- *(总-300)25% 用于task的计算
- (总-300) * 75% * 50% 用于shuffle聚合 ;剩下一半用于 存储RDD的缓存数据和广播变量 (互相动态借用)
四,Shuffle调优
三种方式调优配置项
4.1 在代码中,不推荐,硬编码
new SparkConf().set(“spark.shuffle.file.buffer”,”64”)
4.2 在提交Spark任务时,推荐使用
spark-submit --conf spark.shuffle.file.buffer=64 –conf ….
4.3 在配置文件中修改
conf下的spark-default.conf配置文件中,不推荐,因为是写死,所有的应用程序都要使用
4.4 相关优化配置项
- spark.shuffle.file.buffer(默认32k)
参数说明: 该参数用于设置 map task 在进行shuffle write时buffer的大小, 在进行溢写到磁盘之前,先是写入到buffer内存缓存区中。
建议: 内存允许的情况下,可以将buffer大小设置为 64k,这样可以降低溢写的次数,降低磁盘IO的次数,从而提升性能。
- spark.reducer.maxSizeInFlight(48M)
参数说明: 该参数用于设置shuffle read 的buffer缓存区大小。这个Buffer缓冲区决定了每次能够拉取多少数据。
建议: 如果作业的可用资源较为充足,可以适当的增加大小(比如 96M),从而减少网络拉取的次数,从而提升性能。
- spark.shuffle.io.maxRetries(3)
参数说明: shuffle read 从 shuffle write节点拉取属于自己的数据时,如果是因为网络异常导致拉取失败,会自动重试,该参数代表了可重试的最大次数。
建议: 对于那些包含了特别耗时的shuffle操作的作业时,建议增大最大重试次数。(比如60次),以避免由于JVM的 full gc 或者网络不稳定等因素导致的数据拉取失败。在实践中发现,对于针对超大数据量(数十亿~上百亿)的shuffle过程,调节该参数可以大幅度提升稳定性。
shuffle file not find taskScheduler不负责重试task,由DAGScheduler负责重试stage
- spark.shuffle.io.retryWait(5s)
参数说明: 同上具体解释,该参数代表了每次重试拉取数据的等待间隔,默认是5s。
建议: 建议增大间隔时长(比如 60s),以增加shuffle操作的稳定性。
- spark.shuffle.manager(sort | hash)
参数说明:该参数用于设置ShuffleManager的类型。Spark 1.5以后,有三个可选项:hash、sort和tungsten-sort。HashShuffleManager是Spark 1.2以前的默认选项,但是Spark 1.2以及之后的版本默认都是SortShuffleManager了。tungsten-sort与sort类似,但是使用了tungsten计划中的堆外内存管理机制,内存使用效率更高。
调优建议:由于SortShuffleManager默认会对数据进行排序,因此如果你的业务逻辑中需要该排序机制的话,则使用默认的SortShuffleManager就可以;而如果你的业务逻辑不需要对数据进行排序,那么建议参考后面的几个参数调优,通过bypass机制或优化的HashShuffleManager来避免排序操作,同时提供较好的磁盘读写性能。这里要注意的是,tungsten-sort要慎用,因为之前发现了一些相应的bug。
- spark.shuffle.sort.bypassMergeThreshold(200)
参数说明: 当ShuffleManager为SortShuffleManager时,如果shuffle read task的数量小于这个阈值(默认是200),则shuffle write过程中不会进行排序操作,而是直接按照未经优化的HashShuffleManager的方式去写数据,但是最后会将每个task产生的所有临时磁盘文件都合并成一个文件,并会创建单独的索引文件。
建议: 当你使用SortShuffleManager时,如果的确不需要排序操作,那么建议将这个参数调大一些,大于shuffle read task的数量。那么此时就会自动启用bypass机制,map-side就不会进行排序了,减少了排序的性能开销。但是这种方式下,依然会产生大量的磁盘文件,因此shuffle write性能有待提高。
- spark.shuffle.consolidateFiles(false)
参数说明:如果使用HashShuffleManager,该参数有效。如果设置为true,那么就会开启consolidate机制,会大幅度合并shuffle write的输出文件,对于shuffle read task数量特别多的情况下,这种方法可以极大地减少磁盘IO开销,提升性能。
调优建议:如果的确不需要SortShuffleManager的排序机制,那么除了使用bypass机制,还可以尝试将spark.shffle.manager参数手动指定为hash,使用HashShuffleManager,同时开启consolidate机制。在实践中尝试过,发现其性能比开启了bypass机制的SortShuffleManager要高出10%~30%。
- spark.shuffle.memoryFraction(0.2)
参数说明: 该参数代表了Executor内存中,分配给shuffle read task 进行聚合操作的内存比例,默认是20%
建议: 在资源参数调优中讲解过这个参数。如果内存充足,而且很少使用持久化操作,建议调高这个比例,给shuffle read的聚合操作更多内存,以避免由于内存不足导致聚合过程中频繁读写磁盘。在实践中发现,合理调节该参数可以将性能提升10%左右。
五,Spark资源调度分析
相关总结:
- Executor在集群中分散启动,有利于task计算的数据本地化
org,apache.spark.deploy.master.Master下:
//拷贝来自源码
private def schedule(): Unit = {
//判断Master的状态是否为Alive
if (state != RecoveryState.ALIVE) { return }
// Drivers take strict precedence over executors
//workers代表的是集群刚启动的时候worker向master注册的信息。这里面将workers分散。
val shuffledWorkers = Random.shuffle(workers) // Randomization helps balance drivers
for (worker <- shuffledWorkers if worker.state == WorkerState.ALIVE) {
//waitingDriver实际上是指Driver向master注册的信息。这种情况是在cluster模式下。
for (driver <- waitingDrivers) {
//对于Driver来说资源上是指mem和core ,if条件里面判断的时候Driver所需要的内存和core
if (worker.memoryFree >= driver.desc.mem && worker.coresFree >= driver.desc.cores) {
//这个方法传入的两个参数是:worker:Driver所要运行的位置 driver:当前Driver的信息:SparkContext(各种任务调度器)
launchDriver(worker, driver)
//将Driver启动完成后,需要从waitingDrivers等待队列中删除此Driver
waitingDrivers -= driver
}
}
}
//这里面是给作业分配资源。对于作业来说资源指Executor
startExecutorsOnWorkers()
}
- 默认情况下(提交任务的时候没有设置 --executor-cores选项),每一个Worker会为当前的Application启动一个Executor,这个Executor会使用这个Worker的所有的cores和1G内存。
- 如果想在Worker上启动多个Executor,提交Application的时候要加 --executor-cores 这个选项
- 默认情况下没有设置 --total-executor-cores,一个Application会使用Spark集群中所有的cores
5.1 结论演示
使用Spark-submit提交任务演示,也可以使用Spark-shell
1.默认情况下每个worker为当前的Application启动一个Executor,这个Executor使用集群中所有的cores和1G内存。
./spark-submit
--master spark://node01:7077
--class org.apache.spark.examples.SparkPi
../lib/spark-examples-1.6.0-hadoop2.6.0.jar
10000
2.在Worker上启动多个Executor,设置 --executor-cores参数指定每个executor所使用的cores个数
./spark-submit
--master spark://node01:7077
--executor-cores 1
--class org.apache.spark.examples.SparkPi
../lib/spark-examples-1.6.0-hadoop2.6.0.jar
10000
3.内存不足的情况下启动core的情况。Spark不仅看 executor-core的配置,而且还看配置的core的内存是否够用
./spark-submit
--master spark://node01:7077
--executor-cores 1
--executor-memory 3g
--class org.apache.spark.examples.SparkPi
../lib/spark-examples-1.6.0-hadoop2.6.0.jar 10000
- –total-executor-cores 集群中共使用多少cores
./spark-submit
--master spark://node01:7077
--executor-cores 1
--executor-memory 2g
--total-executor-cores 3
--class org.apache.spark.examples.SparkPi
../lib/spark-examples-1.6.0-hadoop2.6.0.jar
10000
六,Spark任务调度分析
- Action算子开始分析
任务调度可以从一个Action类算子开始。因为Action类算子会触发一个job的执行。
- 划分Stage,以taskSet形式提交任务
DAGScheduler类中 getMissingParentStages()方法是切割job划分Stage。
//根据最后的RDD所在的stage获取父stage,即切割job划分stage
private def getMissingParentStages(stage: Stage): List[Stage] = {
val missing = new HashSet[Stage]
val visited = new HashSet[RDD[_]]
// We are manually maintaining a stack here to prevent StackOverflowError
// caused by recursively visiting
val waitingForVisit = new Stack[RDD[_]]
def visit(rdd: RDD[_]) {
if (!visited(rdd)) {
visited += rdd
val rddHasUncachedPartitions = getCacheLocs(rdd).contains(Nil)
if (rddHasUncachedPartitions) {
for (dep <- rdd.dependencies) {
dep match {
//宽依赖
case shufDep: ShuffleDependency[_, _, _] =>
//划分stage
val mapStage = getShuffleMapStage(shufDep, stage.firstJobId)
if (!mapStage.isAvailable/*0==partitionlength*/) {
missing += mapStage
}
//窄依赖
case narrowDep: NarrowDependency[_] =>
waitingForVisit.push(narrowDep.rdd)
}
}
}
}
}
//压栈
waitingForVisit.push(stage.rdd)
while (waitingForVisit.nonEmpty) {
visit(waitingForVisit.pop())
}
missing.toList
}
//递归方法 递归提交父stage
private def submitStage(stage: Stage) {
val jobId = activeJobForStage(stage)
if (jobId.isDefined) {
logDebug("submitStage(" + stage + ")")
if (!waitingStages(stage) && !runningStages(stage) && !failedStages(stage)) {
//查找丢失的父stage
val missing = getMissingParentStages(stage).sortBy(_.id)
logDebug("missing: " + missing)
if (missing.isEmpty) {
logInfo("Submitting " + stage + " (" + stage.rdd + "), which has no missing parents")
//递归完成所有
submitMissingTasks(stage, jobId.get)
} else {
for (parent <- missing) {
//递归
submitStage(parent)
}
waitingStages += stage
}
}
} else {
abortStage(stage, "No active job for stage " + stage.id, None)
}
}