从另一个角度观察Spark Shuffle过程

大部分将Spark Shuffle的文章,都是从读写过程,与MapReduce对比等角度来阐述的,容易割裂开各模块在Shuffle过程中的关系及相互配合的联系。

本人在读这部分源码的过程中,认为理清Applicatoin, Job, Stage, Task, ApplicationMaster, Executor这些模块间的协作,及如何完成Shuffle中间文件的定位,是比较重要的,可以让人有整体方向性的了解和把握。

一张图,解释了各模块间的关系:

从另一个角度观察Spark Shuffle过程_第1张图片


上图是shuffle阶段各模块协作关系图,也阐述了Shuffle Read的Stage查找定位Shuffle中间文件的流程。

做几点解释:

  1 shuffle_id是一个Application中全局唯一的,标识了一个源stage和shuffle后的目标stage的对应关系(源码中, 用

Dependency这个数据结构类表示)。特别地,设当Stage1在App生命周期内,会进行两次shuffle(两个不同的job的情况),分别对应Stage2和Stage3,那么这两次的shuffle过程会用不同的shuffle_id.

  2 MapStatus: 上面已经说过, shuffle_id是标识进行shuffle操作的前后stage的关系; 设stage1为源stage,stage2为目标stage; stage1有若干task,每个task作为一个单元独立进行计算,然后会将结果以MapStatus的数据结果的形式回传给MapOutputTracker; 所以, 一个shuffle_id下对应一个MapStatus数组,MapStatus在数据中的位置,就是这个MapStatus对应的task的编号;

  3 BlockManager: 一个MapStatus对应一个BlockManager; BlockManager是什么呢?它是executor中负责管理I/O相关操作的,也就是说, 一个executor对应一个BlockManager; 而一个NodeManager中会启动多个executor, 这些executor正常情况下的生命周期是整个Application(多个Job会共享executor)。所以呢, BlockManager中, 不但要有标识是哪个executor的executor_id,也要有标识executor位于哪个NodeManager的host+port信息。再回过来说, 如何理解一个MapStatus对应一个BlockManager呢? Stage1的Task在executor中执行,执行完毕后, 会将block文件写入当前executor程序执行所在的目录中, 并且将执行结果(是状态, 非数据)以MapStatus的形式返回给Driver的MapOutputTracker。所以说, 一个MapStatus标识了所对应的task,是在哪个executor中执行的,并且对应的是源Stage的第几个task。这样大家能明白了为什么说一个MapStatus对应一个BlockManager(即executor)了吧!但是如果反过来就不可以了, 一个BlockManager会对应多个MapStatus!因为一个BlockManager对应的executor,可能会执行同个stage的多个task啊!图中, MapStatus1和MapStatus2就对应同一个BlockManager。

  4 不同的Job, 会复用同一个executor, 即executor会执行不同的Job;

  5 同一个executor,会执行不同阶段的shuffle, 包括同一个job中的不同阶段的shuffle;

  6 一个Application生命周期中, 只会有一个App Master; 从源码上看, 当driver注册为App Master后, 才会运行用户自己写的spark计算代码;

  7 上面的4,5,6, 我认为是Spark比MapReduce快的一个原因, 因为执行了多个计算,只用启动一次App Master, 计算单元Executor也只会启动一次; Task只会在Executor的线程中执行,大大减少了进程冷启动和资源分配的时间.

  8 下面给说说Shuffle Write的中间结果文件的落盘逻辑了。设Stage1的task1执行计算部分,开始写shuffle的输出文件, 输出partition分成了10份;Stage2的task1要读取Stage1的输出partition1的数据; Stage1的task1在写输出文件时,会将10个partition的结果数据,写入到同一个输出文件中,这个文件叫做"block"文件, 文件的命名方式为: shuffleId_mapId_10data。同时,生成另一个index文件,记录每个分区的数据,在block文件中的起始偏移量。mapId就是task1在Stage1的序号,这里当然是1。

      这样,Stage2的task1,就能知道去哪里读取Stage1的task1的block文件了: Stage2从Dependency数据结构中,拿到shuffle_id,然后通过shuffle_id获得所有的MapStatus数据结构;然后就可以知道Stage1的第1个task(这里再强调一下,MapStatus数组元素对应的数组index,就是Mapstatise对应task在Stage中的序号; 这对于一个Stage的多个task跑在一个Executor上的情况,区分不同task的输出block文件,比较有用),对应在Executor上的block文件名是什么,然后通过远端读取,就可以拿到shuffle的block文件了。

 9 说一说我对RDD, cache, shuffle, Stage这几个名词的关系的理解。一开始学习Spark时,我对RDD cache很不理解,我认为,一个RDD被cache,相当于这个RDD的shuffle的结果被cache了。但是这就解释不了, 如果这个RDD被两个RDD都使用到,而且这两个使用的RDD的分区数不同,那么RDD的cache,是cache哪一种分区的结果呢?而且,shuffle的输出结果本来就已经写入磁盘了,为什么还需要做cache呢?读了源码之后,明白了,其实RDD的cache和Stage的shuffle是完全没关系的,即使RDD做了cache,还是需要启动一个ShuffleMapStage,这个Stage负责做Shuffle write的操作。从模块结构上来说,Stage包在了RDD的外面,读取RDD的数据,然后进行Shuffle Write的操作。

你可能感兴趣的:(yarn,spark)