与Hadoop不同,Spark一开始就瞄准性能,将数据放在内存,在内存中计算。
用户将重复利用的数据缓存在内存中,提高下次的计算效率,因此Spark尤其适合迭代型和交互型任务。
RDD(resilient distributed dataset,RDD)。
RDD提供了一种高度受限的共享内存,RDD是只读的、分区记录的集合。
RDD是Spark的核心数据结构,通过RDD的依赖关系形成Spark的调度顺序。
RDD只能基于稳定物理存储中的数据集和其它已有的RDD上执行确定性操作来创建,这些确定性操作称之为转换,如map、filter、groupBy、join。
RDD有4中创建方式:
1)、从Hadoop文件系统输入(如HDFS)创建。(或Hadoop兼容的其他持久化存储系统,如Hive、Cassandra、HBase);
2)、从父RDD转化得到新的RDD;
3)、调用SparkContext方法parallelize,将Driver上的数据集并行化,转化为分布式的RDD;
4)、更改RDD的持久性(persistence),例如cache()函数。默认RDD计算后会在内存中清除,通过cache函数将计算后的RDD缓存在内存中;
用户通过程序对RDD进行操作,将RDD进行转换。
BlockManager管理RDD的物理分区,每个Block就是节点上对应的一个数据块,可以存储在内存或者磁盘。而RDD的partition就是一个逻辑数据块,对应相应的物理块Block。
本质上,一个RDD在代码中相当于是数据的一个元数据结构,存储着逻辑分区及其逻辑结构映射关系,存储着RDD之前的依赖转换关系。
RDD1有5个分区(p1、p2、p3、p4、p5),分别存储在(Node1、Node2、Node3、Node4)中。如图所示:
RDD2有3个分区,分别存储在(Node2、Node3、Node4)中。
RDD是一种分布式的内存抽象,不仅可以通过批量转换创建RDD,还可以对任意位置的内存位置读写。RDD限制应用执行批量写操作,这样有利于实现有效的容错。特别是,由于RDD可以使用Lineage(血统)来恢复分区,基本没有检查点开销。实效时只需要重新计算丢失的那些RDD分区,就可以在不同节点上并行执行,而不需要回滚(Roll Back)整个程序。
分布式共享内存(Distributed Shared Memory,DSM),应用可以向全局地址空间的任意位置进行读写操作。DSM是一种通用的内存数据抽象,但这种通用性同时也使其在商业集群上实现有效的容错和一致性更加困难。
RDD相比DSM的优势:
1)、对于RDD中的批量操作,运行时将根据数据存放的位置来调度任务,从而提高性能。
2)、对于扫描类型操作,如果内存不足以缓存整个RDD,就进行部分缓存,将内存容纳不下的分区存储到磁盘上。
RDD有2种操作算子:Transformation(变换)和Action(行动)
1)、Transformation操作是延迟计算的,也就是说从一个RDD转换生成另一个RDD的转换操作不是马上执行,需要等到有Actions操作时,才真正触发运算。
2)、Actions算子会触发SparkContext提交作业(Job),并将数据输出到Spark系统。
在Transformation算子中再将数据类型维度细分为:
Value数据类型和Key-Value对数据类型的Transformation算子。
Value型数据的算子封装在RDD类中可以直接使用。
Key-Value对数据类型的算子封装与PairRDDFunctions类中,用户需要引入import org.apache.spark.SparkContext._才能使用。
RDD的重要内部属性:
分区列表。
计算每个分片的函数。
对父RDD的依赖列表。
对Key-Value对数据类型RDD的分区器,控制分区策略和分区数。
每个数据分区的地址列表(如HDFS上的数据块的地址)。
DD默认是存储于内存,但当内存不足时,会spill到disk(设置StorageLevel来控制)。
Spark数据存储的核心是弹性分布式数据集RDD,它可以抽象的理解为一个大数组Array,但是这个数组是分布在集群上的,逻辑上每个分区叫一个Partition。
Spark执行过程中,RDD经历一个个的Transformation算子之后,最后通过Action算子进行触发提交。逻辑上每经历一次变换,就会将RDD转换为一个新RDD,RDD之间通过Lineage产生依赖关系,这个关系在容错中很重要。
变换的输入输出都是RDD,RDD会被划分成很多的分区分布在集群的多个节点中,分区是一个逻辑概念,变换前后的新旧分区在物理上可能是同一块内存存储。
有些RDD是计算的中间结果,如果要迭代使用数据,可以调用cache()函数缓存数据。
在物理上,RDD实质上是一个元数据结构,存储着Block、Node等的映射关系以及其他的元数据信息。
一个RDD就是一组分区,在物理数据存储上,RDD的每个分区对应的就是一个Block,Block可以存储在内存,当内存不够的时候可以存储在磁盘上。
如果数据从HDFS等外部存储作为输入数据源,数据按照HDFS中的数据分区策略进行数据分区,HDFS中的一个Block对应Spark的一个分区。同时Spark支持重分区,数据通过Spark默认的或者用户自定义的分区器决定数据块分布在哪些节点。
比如支持Hash分区(按照数据项的Key值取hash值,Hash值相同的元素放入同一个分区之内),Range分区(将属于同一数据范围的数据放入同一分区)。
Word Count Stage and RDD
val wc =textFile(..).flatMap(line=>line.split(“\t”)).map(word=>(word,1)).reduceByKey(_+_)
下图描述了Spark的输入、运行转换、输出。
在运行过程中通过算子对RDD进行转换。算子是RDD中定义的函数,可以对RDD中的数据进行转换和操作。
1)、 输入:数据从外部数据空间输入Saprk(如:分布式存储:textFile读取HDFS等,parallelize方法输入Scala集合或数据),数据进入Spark运行时数据空间,转化为Spark中的数据块,通过BlockManager进行管理。
2)、运行:在Spark数据输入形成RDD后便可以通过变换算子(如filter)对数据进行操作并将RDD转化为新的RDD,通过Action算子,触发Spark提交作业。如果数据需要复用,可以通过Cache算子,将数据缓存到内存。
3)、输出:程序运行结束后数据会输出Spark运行时空间,存储到分布式文件系统(saveAsTextFile输出到HDFS),或Scala数据或集合中(collect输出到Scala集合,count返回Scala int型数据)。
Spark核心数据模型是RDD,但RDD是个抽象类,具体由各子类实现,如MappedRDD、ShuffleRDD等子类,Spark将常用的大数据操作都转换为RDD子类。
Value型Transformation算子:
map、flatMap、mapPartitions、glom、union、cartesian、groupBy、filter、distinct、subtract、sample、takeSample、cache、persist...
Key-Value型Transformation算子:
mapValues、combineByKey、reduceByKey、partitionBy、join、leftOutJoin、rightOutJoin...
Actions算子:
foreach、saveAsTextFile、saveAsObjectFile、collect、collectAsMap、reduceByKeyLocally、lookup、count、top、reduce、fold、arrregate...
Transformation算子:
Actions算子:
RDD实现了基于Lineage的容错机制,在部分计算结果丢失时,只需要根据这个Lineage重算即可。
下图为RDD的部分缓存丢失的逻辑图:
假如RDD2所在的计算作业先计算的话,那么计算完成后RDD1的结果就会被缓存起来,缓存的作用是结果会被后续的计算使用。也就是供RDD3使用。但是现在的RDD1的Partition2缓存丢失,Spark会从RDD0的Partition2开始,重新计算。
RDD的依赖关系:
RDD在Lineage依赖的方面有两种:
Narrow Dependency(窄依赖)和Wide Dependency(宽依赖)
窄依赖是指每一个parent RDD的Partition最多被子RDD的一个Partition使用。
宽依赖是指多个子RDD的Partition会依赖同一个parent RDD的Partition。
本质理解:根据父RDD分区是对应1个还是多个子RDD分区来区分。
Narrow Dependency和Wide Dependency的概念主要用于这两个地方:
一个是容错中相当于Redo日志,另一个在调度中构建DAG作为不同Stage的划分点。
如果一个节点死了,而且运算Narrow Dependency,则只需要把父RDD分区重算即可,不依赖其它节点。而Shuffle Dependency需要父RDD的所有分区都存在,重算就很昂贵了。
在Shuffle Dependency情况下,丢失一个子RDD分区重算的每个父RDD的每个分区的所有数据并不是都是给丢失的子RDD分区使用,会有一部分数据相当于对应的是未丢失的子RDD分区中的数据,这样就产生冗余计算开销,这也是Shuffle Dependency开销更大的原因。因此如果使用Checkpoint算子来做检查点,不仅考虑Lineage是否足够长,也要考虑否有宽依赖,对Shuffle Dependency加Checkpoint是最物有所值的。
Checkpoint机制:
RDD需要加检查点有以下两种情况:
1)、DAG中的Lineage过长,如果重算,则开销太大(如在Pagerank中)。
2)、在Shuffle Dependency上做Checkpoint获得的收益更大。
血统是通过相对粗粒度的记录更新操作来实现容错的。
检查点(本质是通过将RDD写入Disk做检查点)是为了通过血统做容错的辅助,lineage过长会造成容错成本过高,这样就不如在中间阶段做检查点容错,如果之后有节点出现问题而丢失分区,从做检查点的RDD开始重做血统,就会减少开销。