Spark基本概念

参考:https://www.cnblogs.com/qingyunzong/p/8945933.html

一:Spark中的基本概念

 

(1)Application:表示你的应用程序

(2)Driver:表示main()函数,创建SparkContext。由SparkContext负责与ClusterManager通信,进行资源的申请,任务的分配和监控等。程序执行完毕后关闭SparkContext

(3)Executor:某个Application运行在Worker节点上的一个进程,该进程负责运行某些task,并且负责将数据存在内存或者磁盘上。在Spark on Yarn模式下,其进程名称为 CoarseGrainedExecutor Backend,一个CoarseGrainedExecutor Backend进程有且仅有一个executor对象,它负责将Task包装成taskRunner,并从线程池中抽取出一个空闲线程运行Task,这样,每个CoarseGrainedExecutorBackend能并行运行Task的数据就取决于分配给它的CPU的个数。(CPU决定进程并行度)

(4)Worker:集群中可以运行Application代码的节点。在Standalone模式中指的是通过slave文件配置的worker节点,在Spark on Yarn模式中指的就是NodeManager节点。

(5)Task:在Executor进程中执行任务的工作单元,多个Task组成一个Stage

(6)Job:包含多个Task组成的并行计算,是由Action行为触发的

(7)Stage:每个Job会被拆分很多组Task,作为一个TaskSet,其名称为Stage

 

(8)DAGScheduler:根据Job构建基于Stage的DAG,并提交Stage给TaskScheduler,其划分Stage的依据是RDD之间的依赖关系

(9)TaskScheduler:将TaskSet提交给Worker(集群)运行,每个Executor运行什么Task就是在此处分配的。

二:Spark的运行流程

(一)spark运行流程图

1.构建Spark Application的运行环境(启动SparkContext),SparkContext向资源管理器(可以是Standalone、Mesos或YARN)注册并申请运行Executor资源;

2.资源管理器分配Executor资源并启动StandaloneExecutorBackend,Executor运行情况将随着心跳发送到资源管理器上3.SparkContext构建成DAG图,将DAG图分解成Stage,并把Taskset发送给Task SchedulerExecutor向SparkContext申请Task

4.Task Scheduler将Task发放给Executor运行,同时SparkContext将应用程序代码发放给Executor5.Task在Executor上运行,运行完毕释放所有资源

(二)Spark运行架构特点

1.每个Application获取专属的executor进程,该进程在Application期间一直驻留,并以多线程方式运行tasks。
这种Application隔离机制有其优势的,无论是从调度角度看(每个Driver调度它自己的任务),还是从运行角度看(来自不同Application的Task运行在不同的JVM中)。
当然,这也意味着Spark Application不能跨应用程序共享数据,除非将数据写入到外部存储系统。
2.Spark与资源管理器无关,只要能够获取executor进程,并能保持相互通信就可以了。 3.提交SparkContext的Client应该靠近Worker节点(运行Executor的节点),最好是在同一个Rack里,因为Spark Application运行过程中SparkContext和Executor之间有大量的信息交换;
如果想在远程集群中运行,最好使用RPC将SparkContext提交给集群,不要远离Worker运行SparkContext。
4.Task采用了数据本地性和推测执行的优化机制。

三:DAGScheduler(一个面向Stage层面的调度器)

根据Job构建基于Stage的DAG,并提交Stage给TaskScheduler,其划分Stage的依据是RDD之间的依赖关系

dagScheduler.runJob(rdd, cleanedFunc, partitions, callSite, allowLocal,resultHandler, localProperties.get)

rdd: final RDD;
cleanedFunc: 计算每个分区的函数;
resultHander: 结果侦听器;

源码分析

(一)DAGScheduler功能介绍

Job=多个stage,Stage=多个同种task, Task分为ShuffleMapTask和ResultTask

从这个路线得知,最终一个job是依赖于分布在集群不同节点中的task,通过并行或者并发的运行来完成真正的工作。一个个的分布式的task才是Spark的真正执行者。

面向stage的切分,切分依据为宽依赖。(Dependency分为ShuffleDependency和NarrowDependency)

维护waiting jobs和active jobs,维护waiting stages、active stages和failed stages,以及与jobs的映射关系

1、接收用户提交的job;

2、将job根据类型划分为不同的stage,记录哪些RDD、Stage被物化,并在每一个stage内产生一系列的task,并封装成TaskSet;

3、决定每个Task的最佳位置(任务在数据所在的节点上运行),并结合当前的缓存情况;将TaskSet提交给TaskScheduler;

4、重新提交Shuffle输出丢失的Stage给TaskScheduler;

  注:一个Stage内部的错误不是由shuffle输出丢失造成的,DAGScheduler是不管的,由TaskScheduler负责尝试重新提交task执行;

(二)Task分析

Task分为ShuffleMapTask和ResultTask。

Task运行机制如下:

task运行之前的工作是Driver启动Executor,接着Executor准备好一切运行环境,并向Driver反向注册,最终Driver向Executor发送LunchTask事件消息,从Executor接受到LanchTask那一刻起,task就一发不可收拾了,开始通过java线程来进行以后的工作。

当然了,在task正式工作之前,还有一些工作,比如根据stage算法划分好stage,根据task最佳位置计算算法寻找到task的最佳位置(第一期盼都是希望能够在同一个节点的同一个进程中有task所需要的需要,第二才是同一节点的不同进程,第三才是同一机架的不同节点,第四才是不同机架)。这样做的目的是减少网络通信的开销,节省CPU资源,提高系统性能。

1.通过网络拉取运行所需的资源,并反序列化(由于多个task运行在多个Executor中,都是并行运行的,或者并发运行的,一个stage的task,处理的RDD是一样的,这是通过广播变量来完成的)
2.获取shuffleManager,从shuffleManager中获取shuffleWriter(shuffleWriter用于后面的数据处理并把返回的数据结果写入磁盘)
3.调用rdd.iterator(),并传入当前task要处理的partition(针对RDD的某个partition执行自定义的算子或逻辑函数,返回的数据都是通过上面生成的ShuffleWriter,经过HashPartitioner[默认是这个]分区之后写入对应的分区backet,其实就是写入磁盘文件中)
4.封装数据结果为MapStatus ,发送给MapOutputTracker,供ResultTask拉取。(MapStatus里面封装了ShuffleMaptask计算后的数据和存储位置地址等数据信息。其实也就是BlockManager相关信息,BlockManager 是Spark底层的内存,数据,磁盘数据管理的组件)
5.ResultTask拉取ShuffleMapTask的结果数据(经过2/3/4步骤之后的结果)

实现这个过程,task有ShuffleMapTask和ResultTask两个子类task来支撑,前者是用于通过各种map算子和自定义函数转换RDD。后者主要是触发了action操作,把map阶段后的新的RDD拉取过去,再执行我们自定义的函数体,实现各种业务功能。

总结:task的运行一开始不是直接调用底层的task的run方法直接处理job-->stage-->taskSet-->task这条路线的task任务的,它是通过分层和分工的思想来完成。task会派生出两个子类ShuffleMapTask和ResultTask分别完成对应的工作,ShuffleMapTask主要是对task所拥有的的RDD的partition做对应的RDD转换工作,ResultTask主要是根据action动作触发,并拉取ShuffleMapTask阶段的结果做进一步的算子和逻辑函数对数据的进一步处理。这两个阶段是通过MapOutputTracker来连接起来的。

ResultTask和ShuffleMapTask示例:<重点>

1.假如现在在一个节点上由4个shufflemapTask在执行,但是这个节点的core的数量数2,在远端有4个resultTask等待接收shuffleMapTask的数据进行处理

2.这样可以有两个shufflemaptask可以同时执行,在每一个shufflemaptask下面都会产生4个bucket,这是为什么呢,因为每一个shufflemaptask都会为每一个resulttask建立一个数据分区,但是这个bucket是在内存中的当数量达到一定的阈值的时候就会把数据写入本地的磁盘当中也就是shuffleblockfile。

3.shufflemaptask的输出会作为mapstatus发送到DAGscheduler上面mapoutputTracker上面的Master上面去。

4.在resultTask需要拉取数据的时候会去找mapstatus然后使用BlockManager把数据拉取到本地。(到这儿有没有觉得这和MapReduce的执行过程简直就是一样的,其实不然他们还是有那么一点区别,MapReduce在shuffle阶段需要把数据完全存储完之后才把reduce采取拉取数据,但是spark的shuffle阶段不需要这样,shufflemaptask可以一边把数据写入本地的缓存,resultTask可以一边读取数据,这样的操作的速度是不是比mapreduce快,这是为什么呢,因为在hadoop的MapReduce阶段存在在分区内按照key排序,这就是为啥不能像spark的shuffle的原因)

5.假如有1000个shufflemaptask,1000个resultTask那么就会产生100万个磁盘文件,这样在会进行多次的磁盘io,由于磁盘io速度很慢,这样磁盘io就会严重的降低了整个系统的性能。

补充:Spark的shuffle原理剖析(及shuffle优化)

(1)可以自定义是否在map端进行聚合排序等操作

(2)采用类似拉链的操作去存储数据,只需记录下数据的开始和结束的位置

(三)主要职能

1.接收提交Job的主入口,submitJob(rdd, ...)runJob(rdd, ...)。在SparkContext里会调用这两个方法。

  • 生成一个Stage并提交,接着判断Stage是否有父Stage未完成,若有,提交并等待父Stage,以此类推。结果是:DAGScheduler里增加了一些waiting stage和一个running stage。
  • running stage提交后,分析stage里Task的类型,生成一个Task描述,即TaskSet。
  • 调用TaskScheduler.submitTask(taskSet, ...)方法,把Task描述提交给TaskScheduler。TaskScheduler依据资源量和触发分配条件,会为这个TaskSet分配资源并触发执行。
  • DAGScheduler提交job后,异步返回JobWaiter对象,能够返回job运行状态,能够cancel job,执行成功后会处理并返回结果
val sc = new SparkContext("local[2]", "WordCount", System.getenv("SPARK_HOME"), Seq(System.getenv("SPARK_TEST_JAR")))
val textFile = sc.textFile("xxx")
val result = textFile.flatMap(line => line.split("\t")).map(word => (word, 1)).reduceByKey(_ + _)
result.collect

当遇到Action操作时,会产生一个job:

RDD.collect

  ==>sc.runJob                  #####至此完成了将RDD提交DAGScheduler#####

    val results = new Array[U](partitions.size) //result存放的是返回值,数组大小为最后一个RDD的partition的个数

    ==>dagScheduler.runJob(rdd, func, partitions, resultHandler......)     //DAGScheduler的输入:RDD and partitions to compute

      ==>dagScheduler.submitJob

        ==>eventProcessActor ! JobSubmitted

driver有多少个action就会生成多少个job。

2.处理TaskCompletionEvent 

  • 如果task执行成功,对应的stage里减去这个task,做一些计数工作: 
1.如果task是ResultTask,计数器Accumulator加一,在job里为该task置true,job finish总数加一。
加完后如果finish数目与partition数目相等,说明这个stage完成了(stage中全部task都执行完毕),标记stage完成,从running stages里减去这个stage,做一些stage移除的清理工作

2.如果task是ShuffleMapTask,计数器Accumulator加一,在stage里加上一个output location,里面是一个MapStatus类。
MapStatus是ShuffleMapTask执行完成的返回,包含location信息和block size(可以选择压缩或未压缩)。
同时检查该stage完成,向MapOutputTracker注册本stage里的shuffleId和location信息。
然后检查stage的output location里是否存在空,若存在空,说明一些task失败了,整个stage重新提交;否则,继续从waiting stages里提交下一个需要做的stage。
  • 如果task是重提交,对应的stage里增加这个task
  • 如果task是fetch失败,马上标记对应的stage完成,从running stages里减去。如果不允许retry,abort整个stage;否则,重新提交整个stage。另外,把这个fetch相关的location和map任务信息,从stage里剔除,从MapOutputTracker注销掉。最后,如果这次fetch的blockManagerId对象不为空,做一次ExecutorLost处理,下次shuffle会换在另一个executor上去执行。
  • 其他task状态会由TaskScheduler处理,如Exception, TaskResultLost, commitDenied等。

 3.其他与job相关的操作还包括:cancel job, cancel stage, resubmit failed stage等

四:TaskScheduler

将TaskSet提交给Worker(集群)运行,每个Executor运行什么Task就是在此处分配的。

维护task和executor对应关系,executor和物理资源对应关系,在排队的task和正在跑的task。

内部维护一个任务队列,根据FIFO或Fair策略,调度任务。

TaskScheduler本身是个接口,spark里只实现了一个TaskSchedulerImpl,理论上任务调度可以定制。

(一)主要功能

1.submitTasks(taskSet),接收DAGScheduler提交来的tasks

  • 为tasks创建一个TaskSetManager,添加到任务队列里。TaskSetManager跟踪每个task的执行状况,维护了task的许多具体信息。
  • 触发一次资源的索要。
     首先,TaskScheduler对照手头的可用资源和Task队列,进行executor分配(考虑优先级、本地化等策略),符合条件的executor会被分配给TaskSetManager。
    然后,得到的Task描述交给SchedulerBackend,调用launchTask(tasks),触发executor上task的执行。task描述被序列化后发给executor,executor提取task信息,调用task的run()方法执行计算。

2.cancelTasks(stageId),取消一个stage的tasks

调用SchedulerBackend的killTask(taskId, executorId, ...)方法。taskId和executorId在TaskScheduler里一直维护着。

3.resourceOffer(offers: Seq[Workers]),这是非常重要的一个方法,调用者是SchedulerBackend,用途是底层资源SchedulerBackend把空余的workers资源交给TaskScheduler,让其根据调度策略为排队的任务分配合理的cpu和内存资源,然后把任务描述列表传回给SchedulerBackend 

  • 从worker offers里,搜集executor和host的对应关系、active executors、机架信息等等
  • worker offers资源列表进行随机洗牌,任务队列里的任务列表依据调度策略进行一次排序
  • 遍历每个taskSet,按照进程本地化、worker本地化、机器本地化、机架本地化的优先级顺序,为每个taskSet提供可用的cpu核数,看是否满足 
    默认一个task需要一个cpu,设置参数为"spark.task.cpus=1"
    为taskSet分配资源,校验是否满足的逻辑,最终在TaskSetManager的resourceOffer(execId, host, maxLocality)方法里
    满足的话,会生成最终的任务描述,并且调用DAGScheduler的taskStarted(task, info)方法,通知DAGScheduler,这时候每次会触发DAGScheduler做一次submitMissingStage的尝试,即stage的tasks都分配到了资源的话,马上会被提交执行

4.statusUpdate(taskId, taskState, data),另一个非常重要的方法,调用者是SchedulerBacnend,用途是SchedulerBacnend会将task执行的状态汇报给TaskScheduler做一些决定

  • TaskLost,找到该task对应的executor,从active executor里移除,避免这个executor被分配到其他task继续失败下去。
  • task finish包括四种状态:finished, killed, failed, lost。只有finished是成功执行完成了。其他三种是失败。
  • task成功执行完,调用TaskResultGetter.enqueueSuccessfulTask(taskSet, tid, data),否则调用TaskResultGetter.enqueueFailedTask(taskSet, tid, state, data)TaskResultGetter内部维护了一个线程池,负责异步fetch task执行结果并反序列化。默认开四个线程做这件事,可配参数"spark.resultGetter.threads"=4

(二)TaskResultGetter取task result的逻辑

1.对于success task,如果taskResult里的数据是直接结果数据,直接把data反序列出来得到结果;如果不是,会调用blockManager.getRemoteBytes(blockId)从远程获取。如果远程取回的数据是空的,那么会调用TaskScheduler.handleFailedTask,告诉它这个任务是完成了的但是数据是丢失的。否则,取到数据之后会通知BlockManagerMaster移除这个block信息,调用TaskScheduler.handleSuccessfulTask,告诉它这个任务是执行成功的,并且把result data传回去。

2.对于failed task,从data里解析出fail的理由,调用TaskScheduler.handleFailedTask,告诉它这个任务失败了,理由是什么。

五:SchedulerBackend

TaskScheduler下层,用于对接不同的资源管理系统,SchedulerBackend是个接口,需要实现的主要方法如下:

def start(): Unit
def stop(): Unit
def reviveOffers(): Unit // 重要方法:SchedulerBackend把自己手头上的可用资源交给TaskScheduler,TaskScheduler根据调度策略分配给排队的任务吗,返回一批可执行的任务描述,SchedulerBackend负责launchTask,即最终把task塞到了executor模型上,executor里的线程池会执行task的run()
def killTask(taskId: Long, executorId: String, interruptThread: Boolean): Unit =
    throw new UnsupportedOperationException

粗粒度:进程常驻的模式,典型代表是standalone模式,mesos粗粒度模式,yarn

细粒度:mesos细粒度模式

这里讨论粗粒度模式,更好理解:CoarseGrainedSchedulerBackend

维护executor相关信息(包括executor的地址、通信端口、host、总核数,剩余核数),手头上executor有多少被注册使用了,有多少剩余,总共还有多少核是空的等等。

(一)主要功能

1.Driver端主要通过actor监听和处理下面这些事件: 

    • RegisterExecutor(executorId, hostPort, cores, logUrls)。这是executor添加的来源,通常worker拉起、重启会触发executor的注册。CoarseGrainedSchedulerBackend把这些executor维护起来,更新内部的资源信息,比如总核数增加。最后调用一次makeOffer(),即把手头资源丢给TaskScheduler去分配一次,返回任务描述回来,把任务launch起来。这个makeOffer()的调用会出现在任何与资源变化相关的事件中,下面会看到。
    • StatusUpdate(executorId, taskId, state, data)。task的状态回调。首先,调用TaskScheduler.statusUpdate上报上去。然后,判断这个task是否执行结束了,结束了的话把executor上的freeCore加回去,调用一次makeOffer()
    • ReviveOffers。这个事件就是别人直接向SchedulerBackend请求资源,直接调用makeOffer()
    • KillTask(taskId, executorId, interruptThread)。这个killTask的事件,会被发送给executor的actor,executor会处理KillTask这个事件。
    • StopExecutors。通知每一个executor,处理StopExecutor事件。
    • RemoveExecutor(executorId, reason)。从维护信息中,那这堆executor涉及的资源数减掉,然后调用TaskScheduler.executorLost()方法,通知上层我这边有一批资源不能用了,你处理下吧。TaskScheduler会继续把executorLost的事件上报给DAGScheduler,原因是DAGScheduler关心shuffle任务的output location。DAGScheduler会告诉BlockManager这个executor不可用了,移走它,然后把所有的stage的shuffleOutput信息都遍历一遍,移走这个executor,并且把更新后的shuffleOutput信息注册到MapOutputTracker上,最后清理下本地的CachedLocationsMap。

2.reviveOffers()方法的实现。直接调用了makeOffers()方法,得到一批可执行的任务描述,调用launchTasks

3.launchTasks(tasks: Seq[Seq[TaskDescription]])方法。 

    • 遍历每个task描述,序列化成二进制,然后发送给每个对应的executor这个任务信息 
      • 如果这个二进制信息太大,超过了9.2M(默认的akkaFrameSize 10M 减去 默认 为akka留空的200K),会出错,abort整个taskSet,并打印提醒增大akka frame size
      • 如果二进制数据大小可接受,发送给executor的actor,处理LaunchTask(serializedTask)事件。

六:Executor

Executor是spark里的进程模型,可以套用到不同的资源管理系统上,与SchedulerBackend配合使用。

内部有个线程池,有个running tasks map,有个actor,接收上面提到的由SchedulerBackend发来的事件

(一)事件处理

  1. launchTask。根据task描述,生成一个TaskRunner线程,丢尽running tasks map里,用线程池执行这个TaskRunner
  2. killTask。从running tasks map里拿出线程对象,调它的kill方法。
posted @ 2020-03-17 22:30  山上有风景  阅读(732)  评论(0编辑  收藏  举报