很多人觉得Spark编程就是根据官方的框架照葫芦画瓢,但实际上这么粗浅的去理解它并不合适,仅仅保证程序能运行不是一个合格的程序员对自己的要求。本文不打算讨论Spark全画幅执行流程,只说明一下关键的RDD,以及逻辑执行计划到物理执行计划的转变,和整个分布式程序在集群上的运行情况。
整个Spark能够顺利运行的基石是RDD:Resilient Distributed Datasets。 Spark设计者们最成功的地方就在于此,它是一种分布式的内存抽象,表示一个只读的记录分区的集合,它只能通过其他RDD转换而创建,为此,RDD支持丰富的转换操作(如map, join, filter, groupBy等),通过这种转换操作,新的RDD则包含了如何从其他RDDs衍生所必需的信息,所以说RDDs之间是有依赖关系的。基于RDDs之间的依赖,RDDs会形成一个有向无环图DAG,该DAG描述了整个流式计算的流程,实际执行的时候,RDD是通过血缘关系(Lineage)一气呵成的,即使出现数据分区丢失,也可以通过血缘关系重建分区,总结起来,基于RDD的流式计算任务可描述为:从稳定的物理存储(如分布式文件系统)中加载记录,记录被传入由一组确定性操作构成的DAG,然后写回稳定存储。另外RDD还可以将数据集缓存到内存中,使得在多个操作之间可以重用数据集,基于这个特点可以很方便地构建迭代型应用(图计算、机器学习等)或者交互式数据分析应用。可以说Spark最初也就是实现RDD的一个分布式系统,后面通过不断发展壮大成为现在较为完善的大数据生态系统,简单来讲,Spark-RDD的关系类似于Hadoop-MapReduce关系。
总的来说,RDD有以下特点:分区(RDD是逻辑分区的),只读(想要改变数据只能通过算子对RDD进行转换),依赖,缓存以及checkpoint。
缓存和checkpoint是有区别的,前者的目的是起到数据复用的作用,避免重复计算,如果在应用程序中多次使用同一个RDD,可以将该RDD缓存起来,该RDD只有在第一次计算的时候会根据血缘关系得到分区的数据,在后续其他地方用到该RDD的时候,会直接从缓存处取而不用再根据血缘关系计算,这样就加速后期的重用。这也是Spark计算速度快的一个原因。
而checkpoint是为了容错而设计的。虽然RDD的血缘关系天然地可以实现容错,当RDD的某个分区数据失败或丢失,可以通过血缘关系重建。但是对于长时间迭代型应用来说,随着迭代的进行,RDDs之间的血缘关系会越来越长,一旦在后续迭代过程中出错,则需要通过非常长的血缘关系去重建,势必影响性能。为此,RDD支持checkpoint将数据保存到持久化的存储中,这样就可以切断之前的血缘关系,因为checkpoint后的RDD不需要知道它的父RDDs了,它可以从checkpoint处拿到数据。
以上是对RDD有一个详细的认知,下面我们再详细的来看看Spark任务调度情况。
由于Spark用到的数据是分布式的,所以它需要一个强有力的任务调度器。SparkScheduler就起到了这个作用,它的使命就是如何组织任务去处理RDD中每个分区的数据(RDD的储存状况也需要研究一下),根据RDD的依赖关系构建DAG,基于DAG划分Stage,将每个Stage中的任务发到指定的节点node运行,而用户在使用Spark框架的时候需要编写的就是Driver程序。一个Spark程序包括Job,Stage,以及Task三个概念。
- Job是以Action方法为界,遇到一个Action方法则触发一个Job
- Stage是Job的子集,以RDD的宽依赖为界,遇到shuffle做一次划分
- Task是Stage的子集,以并行度(分区数)来衡量,分区数是多少,则有多少个Task
因时间原因,暂时贴上一篇分析的比较好的链接:Spark Scheduler内部原理剖析