我眼中的Spark源码

专题篇

专题一 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

  1. Shuffle一定会有落盘,如果Shuffle过程中落盘数据量减少,那么可以提高性能。算子如果存在预聚合功能,可以提高性能

  2. Spark中的Shuffle落盘优化:一个核中的task落盘一个大文件,读取的task通过索引机制进行读取

  3. 写数据:ShuffleManager(SortShuffleManager),SortShuffleManagr中有不同的handle,不同的handler匹配不同的writer(根据预聚合、下游task数量进行使用)

  4. 以sortShuffleWriter为例,先判断能否预聚合,能预聚合先将规则传入;先将数据从内存溢写,然后合并成data和index;

  5. 读数据从ShffleRDD的read方法读取数据

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值