专题篇
专题一 Scala的类型转换与检查
问题一:如果实例化了子类的对象,但是将其赋予了父类类型的变量,在后续的过程中,又需要将父类类型的变量转换为子类类型的变量,应该如何去做?
首先,需要使用isInstanceOf[XX]判断是否为指定类的对象,如果是的话,则可以使用asInstanceOf将对象转换成指定类型;
注意:
- 如果没有p.isInstanceOf[XX]先判断是否为指定类的对象,就直接用asInstanceOf转换,则可能抛出异常
- 如果对象是null,则isInstanceOf一定会返回false,asInstanceOf一定会返回null;
import org.apache.spark.broadcast.Broadcast
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
import scala.collection.mutable.ListBuffer
object Demo_FlatMap {
def main(args: Array[String]): Unit = {
/**
* Scala类型检查和转换
* isInstanceOf:检查某个对象是否属于某个给定的类
* asInstanceOf:将引用转换为子类的引用
* classOf:如果想要测试p指向的是一个Employee对象,但又不是其子类,可以用if(p.getClass == ClassOf[Employee])
*/
val testA: SuperClass = new SubClass
var textB: SubClass = null
//如果对象是null,isInstance一定会抛出false
println(textB.isInstanceOf[SubClass])
if (testA.isInstanceOf[SubClass]){
//将testA转换成SubClass
textB = testA.asInstanceOf[SubClass]
}
println(textB.isInstanceOf[SubClass])
}
}
class SuperClass{
}
class SubClass extends SuperClass{
}
问题二:如何能精确的判断出,对象是指定类的对象?
注意:
- isInstanceOf只能判断出对象是否为指定类以及其子类的对象,而不能判断出,对象就是指定类的对象
- 如果想要精确地判断出对象就是指定类地对象,就只能使用getClass和ClassOf
- p.getCLass可以精确地获取对象地类,classOf[XX]可以精确的获取类,然后使用==操作符即可判断;
import org.apache.spark.broadcast.Broadcast
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
import scala.collection.mutable.ListBuffer
object Demo_FlatMap {
def main(args: Array[String]): Unit = {
val p:SuperClass = new SubClass
//判断p是否为SuperClass的实例
println(p.isInstanceOf[SuperClass])
//判断p的类型是否为SuperClass
println(p.getClass == classOf[SuperClass])
//判断p的类型是否为SubClass
println(p.getClass == classOf[SubClass])
}
}
class SuperClass{
}
class SubClass extends SuperClass{
}
专题二 withScope
Scala柯里化
在Scala中,一个经过柯里化的函数在应用时,支持多个参数列表,而不是只有一个。当第一次调用时,只传入第一个参数,返回用于第二次调用的函数
private[spark] def withScope[U](body:=>U):U = RDDOperationScope.withScope[U](sc)(body)//这里使用了柯里化
贷出模式
把公共部分(函数体)抽出来封装成方法,把非公共部分通过函数值传进来
private[spark] def withScope[T](
sc:SparkContext,
name:String,
allowNesting:Boolean,
ignoreParent:Boolean)(body:=>T):T=>{
封装公共部分
body //非公共部分
}
因为每个RDD算子方法,都有共同部分和共同参数,所以用withScope封装了公共部分代码,用柯里化把共同参数先传进去。然后,将非公共部分代码,通过第二个参数传进去。这里,body参数,就是非公共部分函数值。
理解算子
def map[U:ClassTag](f:T=>U):RDD[U] = withScope{
val cleanF = sc.clean(f)
new MapPartitionsRDD[U, T](this, (context, pid, iter)=>iter.map(cleanF))
}
以map方法为例,这里相当于直接调用withScope方法,后面花括号里面的是函数,是参数。
在Scala中,当方法只有一个参数时,后面可以用花括号代替圆括号。
Spark源码篇
Yarn集群环境下应用程序的提交流程
1,提交应用程序
${SPARK_HOME}/bin/spark-submit \
--master yarn \
--class WordCount \
--deploy-mode cluster \
--total-executor-cores 1 \
--executor-cores 1 \
--executor-memory 600m \
/root/hua-1.0-SNAPSHOT.jar \
hdfs://192.168.10.111:9000/root/wordcount
2,spark的bin目录下,spark-submit.cmd中执行java -Xmx1g org.apache.spark.deploy.SparkSubmit命令(在虚拟机中,启动一个java进程,执行SparkSubmit类)。
3,执行SparkSubmit伴生对象的main方法(Ctrl+N全局找类,Ctrl+F找关键字),main方法中有一个doSubmit方法(调用伴生类中的方法)。doSubmit方法中,解析用户输入的参数(parseArguments方法),正式提交应用程序(submit方法)。
4,submit方法中,判断集群是否是Standalone模式(我们是Yarn),else分支执行runMain方法。
5,runMain方法中, val (childArgs, childClasspath, sparkConf, childMainClass) = prepareSubmitEnvironment(args)
6,prepareSubmitEnvironment(准备提交环境)方法内根据提交参数,判定集群运行方式(Yarn-Client、Yarn-Cluster、K8s、Standalone、mesos),模式匹配给childMainClass赋予不同值。Yarn运行方式下,ChildMainClass=org.apache.spark.deploy.yarn.YarnClusterApplication
7,runMain方法根据ChildMainClass类型,反射创建YarnClusterApplication对象,执行类中的start方法(Ctrl+H找到特质的实现方法),创建Client对象(其实是YarnClient包含rmClient)
8,创建Client对象,调用run方法,this.appId = submitApplication()
9,submitApplication方法中,yarnClient建立与ResourceManage的连接,请求提交一个应用程序,返回一个全局ID(appId)。创建容器环境,创建提交应用程序环境
10,YarnClient向ResourceManager提交封装参数的指令,RM在某一个NodeManager上执行这些执行,/bin/java org.apache.spark.deploy.yarn.ApplicationMaster(在创建容器环境中提交的指令),启动ApplicationMaster进程。
11,ApplicationMaster进程中,执行类中main方法,对传过来的命令行参数做封装,创建ApplicationMaster对象(伴生类)中包含创建YarnRMClient对象变量(ApplicationMaster用来连接ResourceManager),调用runDriver方法
12,执行runDriver方法,执行startUserApplication()方法,创建一个Driver线程,执行提交应用程序的main方法(执行我们自己写的程序),并初始化SparkContext。
13,调用registerAM,注册AM,向Yarn申请资源。调用Yarn返回资源可用列表
14,调用createAllocator,YarnRMClient的对象调用createAllocator方法,创建分配器。 allocator.allocateResources()分配器向Yarn获取可分配的资源
15,allocateResources方法中,资源的分配是以Containers为单位的。如果有可用分配的资源,交给handleAllocatedContainers处理
16,handleAllocatedContainers负责将containers进行分类处理(同一台主机、同一个机架),划分完毕后,交给runAllocatedContainers(运行已经分配的容器)
17,runAllocatedContainers判断是否需要启动excutor,如果需要就从线程池 launcherPool启动线程,执行run方法启动excutor。
18,run方法中,创建nmClient,ApplicationMaster建立和某个NodeManager的通信,调用startContainer方法
19,startContainer方法中,prepareCommand方法(准备指令),向指定的nodemanager启动容器,在指定的容器执行指令。指令:
/bin/java org.apache.spark.executor.org.apache.spark.executor.CoarseGrainedExecutorBackend
20,执行org.apache.spark.executor.CoarseGrainedExecutorBackend伴生对象的main方法,执行run方法
21,run方法中,创建org.apache.spark.executor.CoarseGrainedExecutorBackend类对象,注册成为通信终端(endpoint),建立excutor与Driver的通信,创建运行时环境。run方法中调用setupEndpoint方法
22,setupEndpoint方法在NettyRpcEnv类中被实现,调用registerRpcEndpoint方法
23,registerRpcEndpoint中,设置通信地址和通信引用,发送onStart(Excutor通信是有生命周期的)到box收信箱
24,调用onStart方法,向Driver发送注册excutor信息
25,打开SparkContext类对象,有一个后台通信对象SchedulerBackend(特质),CoarseGrainedSchedulerBackend类(集群模式)实现了这个特质。实现了接收Excutor消息,并回复的方法。
26,CoarseGrainedExecutorBackend收到Driver回复的注册成功之后,创建Executor对象(真正的计算对象)
27,回到ApplicationMaster的onDriver方法,有两条任务线:
- 注册节点申请资源
- Driver运行提交的代码
这两条线有相互交互,也有阻塞,也有并发执行。
Spark应用程序提交流程介绍后,剩下的是提交代码的执行,伴随提交代码的运行,应用程序也在工作(比如分配container,创建excutor,通信)。
组件通信
Spark早期使用Akka作为内部通信部件,但从Spark2开始,Spark抛弃了Akka,使用Netty通信框架。Spark是基于Netty新的RPC框架。
- AIO:异步非阻塞式IO
- NIO:非阻塞式IO
- BIO:阻塞式IO
Linux对AIO不支持,但是可用模拟异步操作。它使用Epoll模仿AIO操作
Spark的内存管理
内存的分类:
- 堆内内存:JVM控制的内存,无法由Spark释放控制
- 堆外内存:系统内存,可自由释放控制
- 存储内存(Storage):存储缓存数据、广播变量(将task共享的变量广播到excutor中) 堆内30% 堆外50%
- 执行内存(Execution):shuffle过程中的操作 堆内30% 堆外50%
- 其他内存(Other):系统自带、Rdd元数据信息 堆内40%
- 预留内存(System Reserved):堆内300MB
内存管理:MemoryManager(统一内存管理、动态内存管理)
动态占用机制:存储内存和执行内存可以互相占用
应用程序的执行
(0) 创建运行环境
创建核心对象SparkContext(SparkConf配置信息、SparkEnv运行环境、SchedulerBackend与Excutor后台通信、TaskScheduler任务调度器、DAGScheduler阶段调度器)
(1) RDD依赖
窄依赖MapPartitionsRDD:
new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.map(cleanF))
将当前RDD传到这个对象中,构成包含关系,这就血缘。
def this(@transient oneParent: RDD[_]) =
this(oneParent.context, List(new OneToOneDependency(oneParent)))
它们之间的依赖关系就是OneToOneDependency
宽依赖ShuffledRDD:
def groupBy[K](f: T => K, p: Partitioner)(implicit kt: ClassTag[K], ord: Ordering[K] = null)
: RDD[(K, Iterable[T])] = withScope {
val cleanF = sc.clean(f)
this.map(t => (cleanF(t), t)).groupByKey(p)
}
val bufs = combineByKeyWithClassTag[CompactBuffer[V]](
createCombiner, mergeValue, mergeCombiners, partitioner, mapSideCombine = false)
new ShuffledRDD[K, V, C](self, partitioner)
self是前一个RDD,常见一个ShuffledRDD,它们之间的依赖关系是ShuffleDependency
以上所有的关系,可以用有向无环图表示
(2) 阶段的划分
spark中阶段的划分=shuffle依赖+1
(3) 任务的切分
任务的切分=每个阶段最后一个RDD分区的数量
(4) 任务的调度
调度:怎么将任务分配到excutor中执行
将任务封装成TaskSet,将TaskSet封装成TaskManager,将TaskManager交给任务调度器。调度器有两种,Fair Scheduler和FIFO Scheduler(默认)。采用不同的调度器,任务调度方法不同。将Manger加入到Pool中,然后根据本地化级别(进程本地化、节点本地化、机架本地化)选择Excutor分配。分配之后,后台通信,通知启动task。
(5) 任务的执行
Executor计算对象使用ThreadPool执行task,task执行run方法,每个task重写runTask方法,制定运行规则。
Spark Shuffle
-
Shuffle一定会有落盘,如果Shuffle过程中落盘数据量减少,那么可以提高性能。算子如果存在预聚合功能,可以提高性能
-
Spark中的Shuffle落盘优化:一个核中的task落盘一个大文件,读取的task通过索引机制进行读取
-
写数据:ShuffleManager(SortShuffleManager),SortShuffleManagr中有不同的handle,不同的handler匹配不同的writer(根据预聚合、下游task数量进行使用)
-
以sortShuffleWriter为例,先判断能否预聚合,能预聚合先将规则传入;先将数据从内存溢写,然后合并成data和index;
-
读数据从ShffleRDD的read方法读取数据