执行的几个阶段
Driver是用户编写的数据处理逻辑,包含用户创建的SparkContext。SparkContext是用户逻辑与Spark集群主要的交互接口,会和ClusterManager交互。
Executor是一个Worker上为某个应用启动的一个进程,负责运行任务,并负责将数据存在磁盘或内存上。
Task是被送到Executor上的计算单元。
新创建的SparkContext实例会连接到ClusterManager,ClusterManager根据用户提交设置的CPU和内存信息分配计算资源,启动Executor、
Driver将用户程序划分成不同执行阶段,每个阶段由一组完全相同的Task组成,这些Task分别作用于待处理数据的不同分区。阶段划分完成创建Task后,Driver向Executor发送Task。
Executor收到Task,下载Task的运行时依赖,准备好执行环境执行Task,并将运行状态汇报给Driver。
RDD
RDD提供了一种高度受限的共享内存,即RDD是只读的,并只能通过其他RDD上的批量操作来创建。
最难实现的是数据容错性,主要有两种方式:数据检查点和记录数据的更新。
检查点成本太高,一般将创建RDD的一系列转换记录,即Lineage记录下来,以便恢复丢失的分区。
RDD的主要属性:
分片是组成RDD的基本单位,每个分片都会被一个计算任务处理,决定了并行计算的粒度。默认数量是程序分配到的CPUCore的数目。每个分区的存储是由BlockManager实现的,每个分区被逻辑映射成BlockManager的一个Block,这个Block被一个Task计算。
每个RDD都会实现compute函数,compute函数会对迭代器进行符合,不需要保存每次计算的结果。
RDD每次转换都会生成一个新的RDD,RDD之间会形成像流水线一样的前后依赖关系。即lineage
Spark有个列表存储每个分区的优先位置,在进行任务调度时,会尽可能将计算任务分配到处理数据块的存储位置。
为避免缓存丢失影响整个job,Spark引入检查点机制。缓存是在计算结束后缓存。检查点是在计算完成后重新建立一个job计算,推荐先将RDD缓存,减少计算。
RDD之间的关系可从两个维度理解:RDD从哪些RDD转换而来,也就是RDD的父RDD是什么;依赖于父RDD的哪些分区
范围依赖,也算是种窄依赖。RangeDependency,仅被UnionRDD使用。将多个RDD合并成一个,这些RDD是拼接而成,每个父RDD的分区相对顺序不会变。
宽依赖支持两种ShuffleManager,HashShuffleManager和SortShuffleManager
DAG将RDD之间形成了lineage,保证一个RDD被计算前,它所依赖的父RDD都完成了计算;实现了容错性
窄依赖由于分区关系的确定性,转换处理可在同一线程完成,被Spark划分到同一个stage;宽依赖,只能父RDD处理完才能接下来计算,是划分stage的依据。
一个stage内部,每个分区都会被分配一个task,这些task是可以并行执行的。
RDD经过一系列转换后,会在最后一个RDD上触发一个动作,该动作会生成一个job。job被划分为一批task后,被提交到集群计算节点计算。
计算节点执行计算逻辑的部分为Executor,生成的task会发送到已经启动的Executor上,由Executor完成计算任务的执行。
SparkEnv包含了一个运行时节点所需的所有环境信息。cacheManager负责调用BlockManager管理RDD的缓存,RDD缓存下来,可直接通过BlockManager读取缓存后返回。
文件的传输基于HTTP。
cacheManager对storage进行了封装,使得RDD可更简单地从storage读取或写入数据。RDD的每个分区对应storage的一个block,block是分区经过处理后的数据。可认为分区和block是一一对应的。cacheManager通过getOrCompute判断当前RDD是否需要计算,先看该RDD是否被缓存或checkpoint。
cacheManager会通过RDD的ID和当前计算分区的ID向Storage模块的BlockManager发起查询请求,如果能获得Block的信息直接返回,否则代表该RDD是需要计算的。
Scheduler
任务调度模块分为DAGScheduler和TaskScheduler,负责将用户提交的计算任务按DAG划分为不同的阶段且将不同阶段的计算任务提交到集群进行最终计算。
DAGScheduler主要负责分析用户提交的应用,根据计算任务的依赖关系建立DAG,然后将DAG划分成不同的Stage,每个Stage由可并发执行的一组Task构成,这些Task的执行逻辑完全相同,只是作用于不同的数据。
DAGScheduler将这组task划分完成后,会将Task提交到TaskScheduler。TaskScheduler通过集群管理器在集群中的某个Worker的Executor上启动任务。在Executor中运行的任务,如果缓存中没有计算结果,就开始计算,计算结果会回传到Driver或保存到本地。
最主要的三个类:DAGScheduler,TaskScheduler,SchedulerBackend
SchedulerBackend,向当前等待分配计算资源的Task分配Executor,并在分配的Executor上启动Task。
TaskScheduler为创建它的SparkContext调度任务,从DAGScheduler接收不同stage的任务,向集群提交这些任务
一个TaskScheduler对应一个SchedulerBackend
任务调度流程
客户端生成job,DAGScheduler划分Stage,DAGScheduler提交Stage,DAGScheduler为需要计算的分区生成TaskSet,TaskScheduler提交计算任务,调度器调度任务,TaskScheduler为任务分配资源,SchedulerBackend将任务提交到Executor运行。
DAGScheduler
用户提交的job会调用DAGScheduler的runJob,又会调用submitJob,submitJob会为这个job生成一个job ID,并生成一个JobWaiter实例监听Job的执行情况。只有Job的所有Task都执行成功,Job才标记为成功。
stage的划分是从后往前遍历的。由于shuffle的存在,stage之间不能并行计算。getParentStages是划分Stage的核心实现,每遇到宽依赖,就会生成一个parentStage。
对一个stage中的每个分区,会生成ShuffleMapTask,计算结果传给Driver端的mapOutputTracker,其他Task可查询它来获得结果,这个结果并不是真实的数据,而是这些数据所在的位置。
最后一步向TaskScheduler提交Task。对于最后一个stage,对应的task是resultTask,其他的stage对应的都是ShuffleMapTask。先判断分区中是否有缓存,哪些需要计算,就为哪些分区生成Task,这些Task被封装到TaskSet,提交给TaskScheduler。
TaskSet保存了一组完全相同的Task,处理逻辑相同,不同的是数据,每个Task负责处理一个分区,每个TaskSet封装一个stage的任务。
TaskScheduler
每个TaskScheduler对应一个SchedulerBackend,TaskScheduler负责Application不同job之间的调度,在Task执行失败后启动重试。SchedulerBackend负责与集群交互,获取Application分配到的资源,将资源传给TaskScheduler,右TaskScheduler为Task分配计算资源。
TaskScheduler和DAGScheduler都是在SparkContext创建时创建的,会根据传入的Master的URL规则判断集群的部署方式,生成不同的TaskScheduler和SchedulerBackend。
SchedulerBackend作用是分配当前可用的资源,在分配的Executor上启动Task。
Task的提交
先将保存一组任务的TaskSet加入到TaskSetManager,TaskSetManager会根据数据的就近原则为Task分配计算资源,监控Task的状态,若失败会重试。
schedulableBuilder是Application级别的调度器,现支持两种调度策略,FIFO和FAIR。可通过spark.scheduler.mode设置,FAIR的原则是谁最需要资源给谁。
FIFO调度,JobID小的先被调度,同一个Job,StageID小的先被调度。
FAIR调度,在rootPool的基础上根据配置文件构建调度树。
Task运算结果处理
Task在Executor执行完成后,会通知Driver更新任务状态。
Driver会将任务状态更新通知TaskScheduler,然后在这个Executor上重新分配新的计算任务。
对于ShuffleMapTask,首先将结果保存到Stage,当Stage的所有Task都结束了,将整体结果注册到MapOutputTrackerMaster,这样下个Stage的Task可获取到Shuffle数据的元数据信息,随后获取数据。
问题
如何将用户逻辑转换为可并发执行的任务
如果定义和实现Shuffle
如何传递数据,将数据传递到正确的任务上
如何避免数据在集群上移动
如何为用户分配计算资源
实际上在创建SparkContext的时候,Master就会开始为用户提交的计算分配资源,DAGScheduler将计算任务划分完成后,TaskScheduler就会在已经分配好的计算资源上启动任务。