spark-3.0.1源码阅读之文件数据计算
Spark作为分布式的计算引擎,本身并不存储要计算的数据源,需要使用外部的数据,所以这些外部数据接入spark的方式也不同.在接入数据后,spark使用自身的一套计算模式,对数据进行计算,并输出到目标目录中.
1 调试
使用core子工程下的测试类FileSuite,使用第一个test("text files”)就可以,打上三处断点,就可以开始调试了.
调试开始后,可以看到相关的spark ui界面
2 核心方法
2.1 makeRDD方法
这是第一个断点,在sc.makeRDD()方法上,其调用的方法树为
可以看到,主要就是创建了ParallelCollectionRDD实例,与java不同,scala的没有给出构造方法的调用树,需要进入进入该构造方法.
因为其都是一些赋值和初始化的操作,比较简单,这里不再给出这些方法,只说明一下主要设计逻辑.
首先会初始化RDD,其构造也比较简单,这里是要弄清楚RDD是什么以及其主要作用是什么.
Rdd是一种数据结构,定义了关于要计算的数据的抽象信息,类似元数据,但是要发挥的作用比元数据多,主要有:
1 每个算子构成一个RDD
2 依靠算子的依赖关系定义DAG
3 在执行算子的计算时监控计算是否成功,若失败则起到回滚的作用
这三条就把RDD在计算中的逻辑确定了,首先是每个算子定义一个RDD,该RDD记录了上游的算子依赖,形成DAG流程,该流程由一个action算子触发实际的计算,在计算过程中若失败则可以根据上游依赖重新计算,起到回滚的作用.
2.2 saveAsTextFile方法
该方法也是RDD的一个算子,是真正出发计算的action算子,其调用树为
可见,核心方法是saveAsHadoopFile,其调用树为
重要方法是createPathFromString,该方法判断了是哪种文件类型,比如是hdfs文件还是file的本地文件,此处是本地文件.可见,具体的文件判定类型是在该方法中完成的.
进入saveAsHadoopDataset方法,其调用的方法树为
重要方法为executeTask方法,因为spark的最小执行单元就是task,每个task和一个分区相对应,也是在线程内完成的.其调用方法树为
可见,这里有三个关键方法,initWriter,write和commitTask.
1 initWriter就是初始化写文件,包括找到输出路径等.
2 write,这是真正的写文件操作.
3 commitTask,这个方法有些是人迷惑,为什么在task开始执行后才提交,不是应该先提交再执行?其实,因为每个task可作为下一个task的输入,所以这里的提交其实是提交的该task的输出,从注释中也能看到
2.3 collect方法
该方法也是action方法,触发一个实际操作,其调用树为
可见重要方法是runJob方法,其调用方法树为
重要方法为submitJob方法,其调用的方法树为
可见,最终是把关于job提交的事件放入了DAGSchedulerEventProcessLoop的队列中,而负责从队列中取出的事件后进行处理的方法为doOnReceive.在本例,该方法调用handleJobSubmitted方法,其调用的方法树为
一头一尾的两个方法createResultStage和submitStage,分别是创建stage和提交stage.
createResultStage负责创建ShuffleMapStage和ResultStage,submitStage将提交stage,其调用方法树为
重要方法submitMissingTasks的调用方法树为
可见,根据不同的stage创建不同的task,有shuffle的task,也有result的task,每个stage包含一组task,最终调用了TaskScheduler的submitTasks方法,其调用的方法树为
此时,又见熟悉的CoarseGrainedSchedulerBackend,此类在1.5.2中已经存在,此处的逻辑也是一样的,发送ReviveOffers请求,收到后执行makeOffers方法,其调用的方法树为
首先resourceOffers是根据集群各个节点的情况,进行分配匹配,从其返回的结果也能看到,返回的是一个任务描述列表.
在任务匹配之后,自然就是任务加载执行了,此时调用launchTasks方法,其调用的方法树为
此时会调用CoarseGrainedExecutorBackend的receive方法,会向CachedThreadPool类型的线程池提交task的具体计算
而CoarseGrainedExecutorBackend的启动则是在sparkContext初始化时完成的.
在sparkContext的初始化时,就要启动executor进程,具体是向worker发送启动executor进程的请求,进入worker的receive方法,找到case LaunchExecutor(masterUrl, appId, execId, appDesc, cores_, memory_, resources_) =>,该方法体首先创建ExecutorRunner实例,再执行其start方法,可见,此方法就是启动executor的方法,其调用的方法树为
重要方法fetchAndRunExecutor的调用方法树为
此时启动进程传入的命令形如 java -cp … … CoarseGrainedExecutorBackend - - driver-url … - - hostname … - -cores … - -executor-id …, 执行了CoarseGrainedExecutorBackend的main方法,其调用方法树为
主要方法是run方法,这里不再展开,由读者自行查看.
3 关于executor的最大并行度的说明
如前所述,启动的executor是jvm进程,把task提交到CachedThreadPool类型的线程池,看上去应该并行度很大,但是实际的并行度是受core的数目限制的,不会超过core的数目.
在TaskSchedulerImpl类中有这么一个方法resourceOfferSingleTaskSet
,专门限制每个stage的task集合的资源使用量.
里面的CPUS_PER_TASK默认值是1,也可以设置.同时也更新了cpu的数量.
不仅如此,在endPoint端,也限制了最大的并行数.
因此,无论怎样,executor这个jvm进程都不可能同时并行计算多于core数目的task.
4 总结
整体上看,spark使用以资源量来限制并行计算的能力,这也是可以理解的.但是,对于可以共享的cpu核来说,不论是超线程的虚拟核还是物理核,spark的资源限制最终只是限制了同一时刻的线程数,并没有以绑定cpu或独占cpu的方式运行.
1 spark利用外部数据存储系统来读如数据,如果是hdfs,则使用相关的protocol来进行文件的读写,如果是本地文件,则直接读写.hdfs和本地目录没有实质的不同,它们都在固定的目录下,只是管理有区别而已.
2 spark的stage划分是在提交job之后进行的,由DagSchedular实例为每个job依据宽依赖来划分stage,每个stage由有多个task组成,这些task为一个组,由TaskSchedular实例向executor来提交执行.
3 executor只是一个jvm进程,把task提交到线程池来执行,只不过受限于资源的限制,同一时刻task的并行度不超过core的数量.