Spark工作原理分析

Spark应用程序
指的是用户编写的Spark应用程序,包含了Driver功能代码和分布在集群中多个节点上运行的Executor代码。
Spark应用程序,由一个或多个作业JOB组成

Driver:驱动程序
Spark中的Driver即运行上述Application的Main()函数并且创建SparkContext,其中创建SparkContext的目的是为了准备Spark应用程序的运行环境。
在Spark中由SparkContext负责和ClusterManager通信,进行资源的申请、任务的分配和监控等;
当Executor全部运行完毕后,Driver负责将SparkContext关闭。通常SparkContext代表Driver,

Cluster Manager:资源管理器
指的是在集群上获取资源的外部服务,常用的有:Standalone,Spark原生的资源管理器,由Master负责资源的分配;Haddop Yarn,由Yarn中的ResearchManager负责资源的分配;Messos,由Messos中的Messos Master负责资源管理


Executor:执行器
Application运行在Worker节点上的一个进程,该进程负责运行Task,并且负责将数据存在内存或者磁盘上,每个Application都有各自独立的一批Executor,


Worker:计算节点
集群中任何可以运行Application代码的节点,类似于Yarn中的NodeManager节点。在Standalone模式中指的就是通过Slave文件配置的Worker节点,在Spark on Yarn模式中指的就是NodeManager节点,在Spark on Messos模式中指的就是Messos Slave节点,


RDD:弹性分布式数据集
Resillient Distributed Dataset,Spark的基本计算单元,可以通过一系列算子进行操作(主要有Transformation和Action操作),


窄依赖
父RDD每一个分区最多被一个子RDD的分区所用;表现为一个父RDD的分区对应于一个子RDD的分区,或两个父RDD的分区对应于一个子RDD 的分区


宽依赖
父RDD的每个分区都可能被多个子RDD分区所使用,子RDD分区通常对应所有的父RDD分区


DAG:有向无环图
Directed Acycle graph,反应RDD之间的依赖关系,


DAGScheduler:有向无环图调度器
基于DAG划分Stage 并以TaskSet的形势提交Stage给TaskScheduler;负责将作业拆分成不同阶段的具有依赖关系的多批任务;
最重要的任务之一就是:计算作业和任务的依赖关系,制定调度逻辑。在SparkContext初始化的过程中被实例化,
一个SparkContext对应创建一个DAGScheduler。


TaskScheduler:任务调度器
将Taskset提交给worker(集群)运行并回报结果;负责每个具体任务的实际物理调度。


Job:作业
由一个或多个调度阶段所组成的一次计算作业;包含多个Task组成的并行计算,往往由Spark Action催生,
一个JOB包含多个RDD及作用于相应RDD上的各种Operation。


Stage:调度阶段
一个任务集对应的调度阶段;每个Job会被拆分很多组Task,每组任务被称为Stage,也可称TaskSet,一个作业分为多个阶段;
Stage分成两种类型ShuffleMapStage、ResultStage。


TaskSet:任务集
由一组关联的,但相互之间没有Shuffle依赖关系的任务所组成的任务集。
1)一个Stage创建一个TaskSet;
2)为Stage的每个Rdd分区创建一个Task,多个Task封装成TaskSet


Task:任务
被送到某个Executor上的工作任务;单个分区数据集上的最小处理流程单元。


Spark运行基本流程


[img]http://dl2.iteye.com/upload/attachment/0130/5601/31bfce01-3067-3ac7-83c7-eb5d50f24e05.jpg[/img]

[img]http://dl2.iteye.com/upload/attachment/0130/5603/4e4e9aad-7822-3254-8e78-0567d4437a7e.jpg[/img]


支持多种资源管理器
Spark与资源管理器无关,只要能够获取executor进程,并能保持相互通信就可以了,Spark支持资源管理器包含: Standalone、On Mesos、On YARN、Or On EC2。


Job提交就近原则
提交SparkContext的Client应该靠近Worker节点(运行Executor的节点),最好是在同一个Rack(机架)里,因为Spark Application运行过
程中SparkContext和Executor之间有大量的信息交换;如果想在远程集群中运行,最好使用RPC将SparkContext提交给集群,不要远离
Worker运行SparkContext。


移动程序而非移动数据的原则执行
Task采用了数据本地性和推测执行的优化机制。关键方法:taskIdToLocations、getPreferedLocations。


Spark核心原理透视
计算流程

[img]http://dl2.iteye.com/upload/attachment/0130/5605/ee8b8863-ea46-348d-bf74-16f193665d19.jpg[/img]


从代码构建DAG图
Spark的计算发生在RDD的Action操作,而对Action之前的所有Transformation,Spark只是记录下RDD生成的轨迹,而不会触发真正的计算。
Spark内核会在需要计算发生的时刻绘制一张关于计算路径的有向无环图,也就是DAG。

[img]http://dl2.iteye.com/upload/attachment/0130/5607/2c2db726-7d90-351f-8f24-7c6e3cb3fd3f.jpg[/img]


将DAG划分为Stage核心算法
Application多个job多个Stage:Spark Application中可以因为不同的Action触发众多的job,一个Application中可以有很多的job,
每个job是由一个或者多个Stage构成的,后面的Stage依赖于前面的Stage,也就是说只有前面依赖的Stage计算完毕后,后面的Stage
才会运行。

划分依据:Stage划分的依据就是宽依赖,何时产生宽依赖,reduceByKey, groupByKey等算子,会导致宽依赖的产生。

核心算法:从后往前回溯,遇到窄依赖加入本stage,遇见宽依赖进行Stage切分。Spark内核会从触发Action操作的那个RDD开始从后往
前推,首先会为最后一个RDD创建一个stage,然后继续倒推,如果发现对某个RDD是宽依赖,那么就会将宽依赖的那个RDD创建一个新的
stage,那个RDD就是新的stage的最后一个RDD。然后依次类推,继续继续倒推,根据窄依赖或者宽依赖进行stage的划分,直到所有的
RDD全部遍历完成为止。

将DAG划分为Stage剖析
从HDFS中读入数据生成3个不同的RDD,通过一系列transformation操作后再将计算结果保存回HDFS。可以看到这个DAG中只有join操作是
一个宽依赖,Spark内核会以此为边界将其前后划分成不同的Stage. 同时我们可以注意到,在图中Stage2中,从map到union都是窄依赖,
这两步操作可以形成一个流水线操作,通过map操作生成的partition可以不用等待整个RDD计算结束,而是继续进行union操作,这样大
大提高了计算的效率。

[img]http://dl2.iteye.com/upload/attachment/0130/5609/b63029ed-78cf-3fca-a9ec-8584f2c8577d.jpg[/img]


提交Stages
调度阶段的提交,最终会被转换成一个任务集的提交,DAGScheduler通过TaskScheduler接口提交任务集,这个任务集最终会触发
TaskScheduler构建一个TaskSetManager的实例来管理这个任务集的生命周期,对于DAGScheduler来说,提交调度阶段的工作到此就
完成了。而TaskScheduler的具体实现则会在得到计算资源的时候,进一步通过TaskSetManager调度具体的任务到对应的Executor节
点上进行运算。

[img]http://dl2.iteye.com/upload/attachment/0130/5611/41005e23-9a9a-384f-951e-f40fbd198541.jpg[/img]


TaskSetManager负责管理TaskSchedulerImpl中一个单独TaskSet,跟踪每一个task,如果task失败,负责重试task直到达到task重试次数的最多次数。


监控Job、Task、Executor
DAGScheduler监控Job与Task:要保证相互依赖的作业调度阶段能够得到顺利的调度执行,DAGScheduler需要监控当前作业调度阶段乃至
任务的完成情况。这通过对外暴露一系列的回调函数来实现的,对于TaskScheduler来说,这些回调函数主要包括任务的开始结束失败、
任务集的失败,DAGScheduler根据这些任务的生命周期信息进一步维护作业和调度阶段的状态信息。

DAGScheduler监控Executor的生命状态:TaskScheduler通过回调函数通知DAGScheduler具体的Executor的生命状态,如果某一个
Executor崩溃了,则对应的调度阶段任务集的ShuffleMapTask的输出结果也将标志为不可用,这将导致对应任务集状态的变更,进而重新
执行相关计算任务,以获取丢失的相关数据。


获取任务执行结果
结果DAGScheduler:一个具体的任务在Executor中执行完毕后,其结果需要以某种形式返回给DAGScheduler,根据任务类型的不同,任务结果的返回方式也不同。

两种结果,中间结果与最终结果:对于FinalStage所对应的任务,返回给DAGScheduler的是运算结果本身,而对于中间调度阶段对应的任务ShuffleMapTask,返回给DAGScheduler的是一个MapStatus里
的相关存储信息,而非结果本身,这些存储位置信息将作为下一个调度阶段的任务获取输入数据的依据。

两种类型,DirectTaskResult与IndirectTaskResult:根据任务结果大小的不同,ResultTask返回的结果又分为两类,如果结果足够小,则直接放在DirectTaskResult对象内中,如果超过特定尺寸则在Executor端会将DirectTaskResult先序列
化,再把序列化的结果作为一个数据块存放在BlockManager中,然后将BlockManager返回的BlockID放在IndirectTaskResult对象中返回
给TaskScheduler,TaskScheduler进而调用TaskResultGetter将IndirectTaskResult中的BlockID取出并通过BlockManager最终取得对应
的DirectTaskResult。


任务调度总体诠释

[img]http://dl2.iteye.com/upload/attachment/0130/5613/8b83627e-8ba8-30e5-9203-98d8b164d903.jpg[/img]


[url][/url]


中间结果会以某种形式进行保存着,用以提供给下一个阶段的任务作为输入
[url][/url]


[url][/url]
Driver、Master、Worker、Executor,其实它就是JVM进程。

程序运行时Driver是直接与Executor进行交互的。

在Driver中,RDD首先交给DAGSchedule进行Stage的划分。
然后底层的调度器TaskScheduler就与Executor进行交互。
Driver和上图中4个Worker节点的Executor发指令,让它们在各自的线程池中运行Job。
运行时Driver能获得Executor的具体运行资源,这样Driver与Executor之间进行通信,通过网络的方式,Driver把划分好的Task传送给Executor,Task就是我们的Spark程序的业务逻辑代码。
Executor接收任务,进行反序列化,得到数据的输入和输出,在分布式集群的相同数据分片上,数据的业务逻辑一样,只是数据不一样罢了,然后由Executor的线程池负责执行。


编程的Spark程序,打包提交到Driver端,这样就构成了一个Driver,
Driver内部的调度流程,根据算子逻辑的依赖关系,DAGScheduler来划分成不同的Stage,每个Stage中的计算逻辑是一样的,只是数据分片不一样。TaskScheduler向Executor发送任务,
Executor反序列化数据之后,也就得到数据的输入和输出,也就是任务的业务逻辑,Executor运行的就是我们的业务逻辑代码。


我们整个Spark应用程序,可以分成:Driver和Executor两部分。
Driver由框架直接生成;
Executor执行的才是我们的业务逻辑代码。
执行的时候,框架控制我们代码的执行。Executor需要执行的结果汇报给框架也就是Driver。


下面就是Driver具体的调用过程:
通过DAGScheduler划分阶段,形成一系列的TaskSet,然后传给TaskScheduler,把具体的Task交给Worker节点上的Executor的线程池处理。线程池中的线程工作,通过BlockManager来读写数据。


sprak 的worker运行后会一直存在,不会退出


[url]https://www.jianshu.com/p/04454941dbac[/url] (spark源码分析Master与Worker启动流程篇)


Executor内部是通过线程池的方式来完成Task的计算的;


运行流程:
[url][/url]
[url][/url]