WordCount Job执行(源码剖析)

 
sc.textFile("hdfs://...").flatMap(_.split(" ")).map((_, 1)).reduceByKey(_+_).collect
 
val hadoopRDD0 = sc.textFile("hdfs://...") // HadoopRDD[0]
val mapPartitionsRDD1 = hadoopRDD0.flatMap(_.split(" ")) // MapPartitionsRDD[2]
val mapPartitionsRDD2 = mapPartitionsRDD1.map((_, 1)) // MapPartitionsRDD[2]
val shuffledRDD3 = mapPartitionsRDD2.reduceByKey(_+_) // ShuffledRDD[3]
shuffledRDD3.collect // action
 
1. collect触发Job
首先,collect调用了SparkContext上的runJob方法。这个方法是一个阻塞方法,会在Job完成之前一直阻塞等待,直到Job执行完成之后返回所得的结果
 
2. DAGScheduler提交Job
SparkContext的runJob被调用之后,这个Job的信息被递传给了SparkContext持有的一个DAGScheduler上。DAGScheduler本身维护着一个消息队列,在收到这个Job之后,将给自己的消息队列发送一个JobSubmitted消息。这个消息中包含了新生成的一个JobId, 触发action的RDD,经过清理后的闭包函数,要处理的各个分区的在RDD中的索引,以及一些其他信息。
DAGScheduler的消息队列在收到JobSubmitted消息后,将触发调用handleJobSubmitted方法。在这个方法中,首先会根据这个触发action的RDD的依赖信息计算出这个Job的所有Stage。在这个WordCount中,我们是在reduceByKey生成的shuffledRDD3(其生成的过程涉及到通用的combineByKey方法,具体可以参考这篇文章)上触发的action,所以我们的ResultStage所对应的finalRDD就是shuffledRDD3,ResultStage所要执行的就是shuffledRDD3的所有分区。shuffledRDD3有一个ShuffleDependency,指向mapPartitionsRDD2,据此ShuffleDependency会生成一个ShuffleMapStage,它是ResultStage的父Stage。
 
3. 根据继承关系分析Stages
在分析出所有的Stage之后,DAGScheduler会根据ResultStage创建出一个ActiveJob对象,用来表示这个活跃的Job。然后提交ResultStage,但是在真正执行这个Stage之前,先递归的判断它有没有父Stage,若有的话先提交它的父Stage,并将当前Stage加入等待队列;若没有父Stage,才会真正的开始执行这个Stage。等待队列中的Stage,会在父Stage都执行完成之后再被执行。
由此可以看出,在一个Job中,Stage之间必须按序执行,后一个Stage的执行将依赖前一个Stage的结果。一个Job只会有一个ResultStage,并且这个ResultStage一定会是整个Job的最后一个Stage,所以ResultStage执行的结束也就标志着整个Job的结束。
 
4. Task的创建和提交
按照之前的分析,我们的Job一共有两个Stage,一个ShuffleMapStage,一个ResultStage,并将先执行ShuffleMapStage。在执行Stage的时候,会按此Stage对应的RDD的分区数量,对应每一个分区创建一个Task。如果是ShuffleMapStage则创建ShuffleMapTask,如果是ResultStage则创建ResultTask。这些Task在后面将会被序列化后发到其他的executor上面去运行。
 
5. SchedulerBackend分配资源(executors)和发送Task
SchedulerBackend是一个接口,它在不同的部署模式下会有不同的实现(实际上TaskScheduler也是这样)。SchedulerBackend的作用是调度和控制整个集群里面的资源(我是这么理解的,这里的资源指的是可用的executors),当reviveOffers方法被调用后,它会将当前可用的所有资源信息,通过调用TaskScheduler的resourceOffers提供给TaskScheduler(实际上这个过程是通过另一个EndPoint类以消息队列的方式实现的,这样可以保证同时只会进行一个对资源的申请或释放过程)。
TaskScheduler在收到当前所有可用的资源信息后,会将这些资源信息按序提供给当前正在执行的多个TaskSet,每个TaskSet再根据这些资源信息将当前可以执行的Task序列化后包装到一个TaskDescription对象中返回(这个TaskDescription对象中也包含了这个任务将要运行在哪个executor上),最终通过TaskScheduler将所有当前的资源情况可以执行的Task对应的TaskDescription返回给SchedulerBackend。
SchedulerBackend这时才根据每个TaskDescription将executors资源真正的分配给这些Task,并记录已分配掉的资源和剩余的资源,然后将TaskDescription中序列化后的Task通过网络(Spark使用akka框架)发送给它对应的executor。
 
6. executor执行Task
集群中的executor在收到Task后,申请一个线程开始运行这个Task。这是整个Job中最核心的部分了,真正的计算都在这一步发生。首先将其反序列化,然后调用这个Task对象上的runTask方法。在这里对于ShuffleMapTask和ResultTask,runTask方法有着不同的实现,并将返回不同的内容。
 
7. executor返回结果
在runTask计算结束返回数据后,executor将其返回的数据进行序列化,然后根据序列化后数据的大小进行判断:如果数据大与某个值,就将其写入本地的内存或磁盘(如果内存不够),然后将数据的位置blockId和数据大小封装到一个IndirectTaskResult中,并将其序列化;如果数据不是很大,则直接将其封装入一个DirectTaskResult并进行序列化。最终将序列化后的DirectTaskResult或者IndirectTaskResult递传给executor上运行的一个ExecutorBackend上(通过statusUpdate方法)。
 
8. driver接收executor返回的结果并释放资源
在driver端的SchedulerBackend收到这个StatusUpdate消息之后,将结果续传给TaskScheduler,并进行资源的释放,在释放资源后再调用一次reviveOffers,这样又可以重复上面所描述的过程,将释放出来的资源安排给其他的Task来执行。
 
9. TaskResultGetter解析并拉取结果
TaskScheduler在收到任务结果后,将这个任务标记为结束,然后使用一个TaskResultGetter类来进行结果的解析。TaskResultGetter将结果反序列化,判断如果其是一个DirectTaskResult则直接抽取出其中的结果;如果是一个IndirectTaskResult则需要根据其中的blockId信息去对应的机器上拉取结果。最终都是将结果拉取到driver的内存中(这就是我们最好不要在大数据集上执行类似collect的方法的原因,它会将所有的数据拉入driver的内存中,造成大量的内存开销,甚至内存不足)。然后TaskResultGetter会将拉取到的结果递交给TaskScheduler,TaskScheduler再将此结果递交给DAGScheduler。
 
10. 处理结果并在Job完成时返回
DAGScheduler在收到Task完成的消息后,先判断这完成的是一个什么任务。如果是一个ShuffleMapTask则需要将返回的结果(MapStatus)记录到driver中,并判断如果当前的ShuffleMapStage若是已经完成,则去提交下一个Stage。如果是一个ResultTask完成了, 则将其结果递交给JobWaiter,并标记这个任务已完成。
JobWaiter是DAGScheduler在最开始submitJob的时候创建的一个对象,用于阻塞等待任务的完成,并进行结果的处理。JobWaiter在每收到一个ResultTask的结果时,都将结果在resultHandler上执行。这个resultHandler则是由SparkContext传进来的一个函数,其作用是将数据放入一个数组中,这个数组最终将作为SparkContext.runJob方法的返回值,被最开始的collect方法接收然后返回。若JobWaiter收到了每个ResultTask的结果,则表示整个Job已经完成,此时就停止阻塞等待,于是SparkContext.runJob返回一个结果的数组,并由collect接收后返回给用户程序。
至此,一个Spark的WordCount执行结束。
 
 
————————————————————————————————————————————————————————————
Spark job运行原理
spark-submit提交Spark应用程序后,其执行流程如下:
1 创建SparkContext对象,然后SparkContext会向Clutser Manager(集群资源管理器),例如Yarn、Standalone、Mesos等申请资源
2 资源管理器在worker node上创建executor并分配资源(CPU、内存等),后期excutor会定时向资源管理器发送心跳信息
3 SparkContext启动DAGScheduler,将提交的作业(job)转换成若干Stage,各Stage构成DAG(Directed Acyclic Graph有向无环图),各个Stage包含若干相task,这些task的集合被称为TaskSet
4 TaskSet发送给TaskSet Scheduler,TaskSet Scheduler将Task发送给对应的Executor,同时SparkContext将应用程序代码发送到Executor,从而启动任务的执行
5 Executor执行Task,完成后释放相应的资源。
————————————————————————————————————————————————————————————
 
posted @ 2016-11-17 22:18  Uncle_Nucky  阅读(143)  评论(0编辑  收藏  举报