RDD(Resilient Distributed Datasets,弹性分布式数据集),是Spark最为核心的概念,自然也是理解Apache Spark 工作原理的最佳入口之一。
RDD的特点:
RDD之所以为“弹性”的特点
查阅了很多资料基本都没有介绍RDD长什么样子的,什么样的结构,都说里面有依赖、有分区,但是长什么样呢?对它没有一点头绪,我想初学者一定是和我一样的。
没有结构图,怎么理解RDD?上图!(自己瞎做的图,基本借鉴这位博主,不准确的地方请指正)
具体参考分区的具体分析+源代码分析
RDD是一个只读的有属性的数据集。属性用来描述当前数据集的状态,数据集是由数据的分区(partition)组成。
RDD 内部的数据集合在逻辑上和物理上被划分成多个小子集合,这样的每一个子集合我们将其称为分区(partitions),分区的个数会决定并行计算的粒度,而每一个分区数值的计算都是在一个单独的任务中进行,因此并行任务的个数,也是由 RDD分区的个数决定的。
但事实上,RDD 只是数据集的抽象,分区内部并不会存储具体的数据。Partition 类内包含一个 index 成员,表示该分区在 RDD 内的编号,通过 RDD 编号 + 分区编号可以唯一确定该分区对应的块编号,利用底层数据存储层提供的接口,就能从存储介质(如:HDFS、Memory)中提取出分区对应的数据。下面是Partition 类的代码:
trait Partition extends Serializable {
/**
* Get the partition's index within its parent RDD
*/
def index: Int
// A better default implementation of HashCode
override def hashCode(): Int = index
}
Partitioner决定RDD的分区方式。
RDD的分区方式主要包含两种(HashPartitioner和RangePartitioner),这两种分区类型都是针对Key-Value类型的数据。如是非Key-Value类型,则分区为None。 Hash是以key作为分区条件的散列分布,分区数据不连续,极端情况也可能散列到少数几个分区上,导致数据不均等;Range按Key的排序平衡分布,分区内数据连续,大小也相对均等。
图中最显眼的一定是Dependencies(依赖),它扩展出了一个箭头到前面一个块。Parents在很多面向对象的计算机语言可以知道它表示“继承”,在RDD中的Dependencies意思略有不同。看一段实际操作的Spark代码:
lines = spark.textFile("hdfs://...")
errors = lines.filter(_.startsWith("ERROR"))
errors.cache()// Count errors mentioning MySQL:
errors.filter(_.contains("MySQL")).count()
// Return the time fields of errors mentioning
// HDFS as an array (assuming time is field
// number 3 in a tab-separated format):
errors.filter(_.contains("HDFS"))
.map(_.split('\t')(3))
.collect()
这段代码可以化为如下图的流程(图和代码都是盗来的):
在每次transformations操作时,都是重新创建了一个新的RDD2,这个RDD2时基于原有的RDD1,RDD1是RDD2的Parents,也就是说这个RDD2依赖于RDD1。这些依赖描述了RDD的Lineage(血统)。
如果父RDD的每个分区最多只能被子RDD的一个分区使用,我们称之为(narrow dependency)窄依赖;
若一个父RDD的每个分区可以被子RDD的多个分区使用,我们称之为(wide dependency)宽依赖,在源代码中方法名为ShuffleDependency,顾名思义这之中还需要Shuffle操作。
窄依赖每个child RDD 的partition的生成操作都是可以并行的,而宽依赖则需要所有的parent partition shuffle结果得到后再进行。
NarrowDependency也还有两个子类,一个是 OneToOneDependency,一个是 RangeDependency
OneToOneDependency,可以看到getParents实现很简单,就是传进一个partitionId: Int,再把partitionId放在List里面传出去,即去parent RDD 中取与该RDD 相同 partitionID的数据
RangeDependency,用于union。与上面不同的是,这里我们要算出该位置,设某个parent RDD 从 inStart 开始的partition,逐个生成了 child RDD 从outStart 开始的partition,则计算方式为: partitionId - outStart + inStart
那么为什么要把依赖分为窄依赖和宽依赖呢?
来源:https://www.jianshu.com/p/dd7c7243e7f9?from=singlemessage
首先,从计算过程来看,窄依赖是数据以管道方式经一系列计算操作可以运行在了一个集群节点上,如(map、filter等),宽依赖则可能需要将数据通过跨节点传递后运行(如groupByKey),有点类似于MR的shuffle过程。
其次,从失败恢复来看,窄依赖的失败恢复起来更高效,因为它只需找到父RDD的一个对应分区即可,而且可以在不同节点上并行计算做恢复;宽依赖则牵涉到父RDD的多个分区,恢复起来相对复杂些。
Stage可以简单理解为是由一组RDD组成的可进行优化的执行计划。如果RDD的衍生关系都是窄依赖,则可放在同一个Stage中运行,若RDD的依赖关系为宽依赖,则要划分到不同的Stage。这样Spark在执行作业时,会按照Stage的划分, 生成一个完整的最优的执行计划。下面引用一张比较流行的图片辅助大家理解Stage,如图RDD-A到RDD-B和RDD-F到RDD-G均属于宽依赖,所以与前面的父RDD划分到了不同的Stage中。
尽管当一个RDD出现问题可以由它的依赖也就是Lineage信息可以用来故障恢复,但对于那些Lineage链较长的RDD来说,这种恢复可能很耗时。
Checkpoint是Spark提供的一种缓存机制,当需要计算的RDD过多时,为了避免重新计算之前的RDD,可以对RDD做Checkpoint处理,检查RDD是否被物化或计算,并将结果持久化到磁盘或HDFS。
本节参考来源:http://blog.csdn.net/liben2007/article/details/53700399
Iterator和Compute都是来表示该RDD如何通过父RDD计算得到。
Iterator用来查找当前RDD Partition与父RDD中Partition的血缘关系。并通过StorageLevel确定迭代位置,直到确定真实数据的位置。
Iterator函数实现大体是这么个流程:
用来记录RDD的存储级别,在官网中可以看到RDD的存储级别表,这里不多解释:
它是一个列表,用于存储每个Partition的优先位置的一个列表。对于每个HDFS文件来说,这个列表保存的是每个Partition所在的块的位置,也就是对这个文件的”划分点“。
SparkContext为Spark job的入口,由Spark driver创建在client端,包括集群连接,RddID,创建抽样,累加器,广播变量等信息。
配置信息,即sc.conf
Spark参数配置信息
提供三个位置用来配置系统:
Spark api:控制大部分的应用程序参数,可以用SparkConf对象或者Java系统属性设置
环境变量:可以通过每个节点的conf/spark-env.sh脚本设置。例如IP地址、端口等信息
日志配置:可以通过log4j.properties配置
根据原有的RDD创建一个新的RDD。
注意:
RDD的所有转换操作都是lazy模式,即Spark不会立刻计算结果,而只是简单的记住所有对数据集的转换操作。这些转换只有遇到action操作的时候才会开始计算。这样的设计使得Spark更加的高效。
例如,对一个输入数据做一次map操作后进行reduce操作,只有reduce的结果返回给driver,而不是把数据量更大的map操作后的数据集传递给driver。
*略
对RDD操作后把结果返回给driver
*略
原文链接:https://blog.csdn.net/u011094454/article/details/78992293