问题导读:
1、Spark Job Stage划分算法有哪些?
2、Task最佳计算位置算法如何理解?
3、Task任务本地算法运用场景有哪些?


spark的task数量由什么决定_System



一、Stage划分算法


由于Spark的算子构建一般都是链式的,这就涉及了要如何进行这些链式计算,Spark的策略是对这些算子,先划分Stage,然后在进行计算。


由于数据是分布式的存储在各个节点上的,所以为了减少网络传输的开销,就必须最大化的追求数据本地性,所谓的数据本地性是指,在计算时,数据本身已经在内存中或者利用已有缓存无需计算的方式获取数据。


1.Stage划分算法思想


(1)一个Job由多个Stage构成


一个Job可以有一个或者多个Stage,Stage划分的依据就是宽依赖,产生宽依赖的算子:reduceByKey、groupByKey等等


(2)根据依赖关系,从前往后依次执行多个Stage


SparkApplication 中可以因为不同的Action触发众多的Job,也就是说一个Application中可以有很多的Job,每个Job是有一个或者多个Stage构成,后面的Stage依赖前面的Stage,也就是说只有前面的Stage计算完后,后面的Stage才会运行。


(3)Stage的执行时Lazy级别的


所有的Stage会形成一个DAG(有向无环图),由于RDD的Lazy特性,导致Stage也是Lazy级别的,只有遇到了Action才会真正发生作业的执行,在Action之前,Spark框架只是将要进行的计算记录下来,并没有真的执行。


Action导致作业执行的代码如下:触发作业,发送消息。

[AppleScript] 纯文本查看 复制代码

?

[AppleScript] 纯文本查看 复制代码

?

[AppleScript] 纯文本查看 复制代码

?

[AppleScript] 纯文本查看 复制代码

?

[AppleScript] 纯文本查看 复制代码

?

[AppleScript] 纯文本查看 复制代码

?

[AppleScript] 纯文本查看 复制代码

?



&  例如 collect 会导致SparkContext中的runJob方法的执行,最终会导致DAGScheduler中的submit的执行。这其中最为核心的是通过发送一个名为JobSubmit的case class对象给eventProcessLoop。


这里的eventProcessLoop是DAGSchedulerEventProcessLoop的具体实例,DAGSchedulerEventProcessLoop是EventLoop的子类,具体实现了EventLoop的doOnReceive;而DAGScheduler是EventLoop的子类,EventLoop内部有一个线程EventThread,EventThread的run方法会不断循环消息队列,不断从eventQueue(LinkedBlockingDeque[E]())中获取消息,然后调用DAGScheduler的doOnReceiver方法,同时传入DAGSchedulerEvent类型的event来处理通过post方法传过来的消息。

[AppleScript] 纯文本查看 复制代码

?

[AppleScript] 纯文本查看 复制代码

?

[AppleScript] 纯文本查看 复制代码

?

[AppleScript] 纯文本查看 复制代码

?

[AppleScript] 纯文本查看 复制代码

?



消息的接收和处理:


(1)DAGScheduler启动一个线程EventLoop(消息循环器),不断地从消息队列中取消息。消息是通过EventLoop的put方法放入消息队列,当EventLoop拿到消息后会回调DAGScheduler的OnReceive,进而调用doOnReceive方法进行处理。


&  为什么要开辟线程来执行消息的读、取?这样可以提交更多的Job,异步处理多Job,处理的业务逻辑一致(调用自己方法也是发送消息),解耦合,扩展性好。


(2)在doOnReceive中通过模式匹配的方式把JobSubmitted封装的内容路由到handleJobSubmitted。


(3)在handleJobSubmitted中首先创建finalStage。

[AppleScript] 纯文本查看 复制代码

?

[AppleScript] 纯文本查看 复制代码

?



(4)通过递归的方式创建DAG。

[AppleScript] 纯文本查看 复制代码

?



下面,我们将看到本节课最重要的一个函数。

[AppleScript] 纯文本查看 复制代码

?



可以看出,我们将Result RDD和firstJobId(由于有些Job在执行中会触发其他Job)作为出入参数,调用getParentStages方法,来创建我们Job的DAG。从源码上来看,是将我们前面的第二十三讲的思想,具体实现为代码。


这里,我们来看看宽依赖划分Stage的过程。

[AppleScript] 纯文本查看 复制代码

?

[AppleScript] 纯文本查看 复制代码

?

[AppleScript] 纯文本查看 复制代码

?

[AppleScript] 纯文本查看 复制代码

?



&  这里需要说明,ShuffleMapTask的计算结果都会传给Driver端的mapOutputTracker,其他的Task可以通过查询它来获得这些结果。mapOutputTracker.registerShuffle(…)实现了存储这些元数据信息的占位,而ShuffleMapTask的结果通过registerMapOutputs来保存计算结果。这个结果是数据所在位置、大小等元数据信息,这样下一个Stage的Task就可以通过这些元数据信息获取需要处理的数据了。

[AppleScript] 纯文本查看 复制代码

?



具体流程图如下:

spark的task数量由什么决定_数据_02

 

由于创建DAG的过程是递归进行的,所以在创建后一个Stage时,必然保证其直接父Stage已经创建(未创建,便会递归调用getParentStage来创建父Stage),知道递归的最底层,也就是遇到了DAG中的最左边的RDD,此时会为这个时候的ShuffleDependency创建Stage0,并跳出底层递归,然后逐层创建stage,并返回给上一层,知道最后来到顶层也就是Result Stage,创建Result Stage,完成DAG的创建。


&  注意:1.在每层递归中getParentStage只是查找当前Stage的直接父Stage(Stages)。


2.由于是递归调用,而Stage的id是在递归调用结束前利用nextStageId.getAndIncreme来设置的所以,父Stage的id小于子Stage的id。


二、Task任务本地性算法


1.Task任务本算法运用场景


在上一节,我们介绍了Job Stage划分算法,并最终得到了DAG图中的Result Stage(final Stage)。接下来我们通过查看Task任务本地性(为了保证Data Locality)的运用场景----Task的运行调度处理,来引入Task任务本地性算法。


在得到逻辑上Result Stage,Spark为了进行计算就必须先报任务以一定的集群可识别形式提交给集群进行计算。Spark的任务提交过程如下:

(1)生成ActiveJob,为提交finalStage做准备。

[AppleScript] 纯文本查看 复制代码

?



(2)提交finalStage

[AppleScript] 纯文本查看 复制代码

?



(3)提交MissingTask

[AppleScript] 纯文本查看 复制代码

?



从源码中,我们可以发现,我们的missingTask会最先会再到需要计算的分片,然后对Stage的运行环境进行设定,然后取得Task计算的本地性级别,最后会根据这些信息建立Tasks来处理每个分片,在提交给底层TaskScheduler之前,Spark还会将Tasks封装成TaskSet。最后提交TaskSet给TaskScheduler,等待TaskScheduler最终向集群提交这些Task,并且DAGScheduler会监听这些Task的状态。


2.数据本地性


(1)这里我们来着重讲解获取数据本地性部分的代码:

 

[AppleScript] 纯文本查看 复制代码

?




这里会将要计算的分片(Partition)转换为(id, getPreferredLocs(stage.rdd, id)) 类型的truple,进而由truple转换未一个Map映射,在Task构造时需要一个locs参数,便可以利用这个映射由id得到相应Partition的本地性级别。


(2)在每个分片(Partition)内部则是通过getPreferredLocs方法得到的

[AppleScript] 纯文本查看 复制代码

?



在具体算法实现的时候,首先查询DAGScheduler的内存数据结构中是否存在当前partition的数据本地性信息,若有的话就直接放回该信息;若没有首先会调用rdd.getPreferredLocations来得到数据的本地性。


&  例如想让Spark运行在HBase上或者是一种现在Spark还没有直接支持的数据库上,此时开发者需要自定义RDD,为了保证Task计算的数据本地性,最为关键的方式就是必须实现RDD的getPreferredLocations方法,来支持各种来源的数据。

(3)DAGScheduler计算数据本地性时,巧妙的借助了RDD自身的getPreferredLocations中的数据,最大化的优化效率,因为getPreferredLocations中表明了每个Partition的数据本地性。虽然当然Partition可能被persist或checkpoint,但是persist或checkpoint默认情况下肯定和getPreferredLocations中的partition的数据本地性是一致的。所以,这中算法就极大的简化了Task数据本地性算法的实现,并且优化了效率

/** 
* Return an array that contains all of the elements in this RDD.  
*/ 
def collect(): Array[T] = withScope { 
val results = sc.runJob(this, (iter: Iterator[T]) => iter.toArray) 
Array.concat(results: _*) 
}