SparkRDD深度解析-RDD计算流程

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

SparkRDD深度解析-RDD计算流程
Spark RDD深度解析-RDD计算流程
摘要 RDD()是Spark的核⼼数据结构,所有数据计算操作均基于该结构进⾏,包括Spark sql 、Spark Streaming。

理解RDD有助于了解分布式计算引擎的基本架构,更好地使⽤Spark进⾏批处理与流计算。

本⽂以Spark2.0源代码为主,对RDD的⽣成、计算流程、加载顺序等作深⼊的解析。

RDD印象
直观上,RDD可理解为下图所⽰结构,即RDD包含多个Partition(分区),每个Partition代表⼀部分数据并位于⼀个计算节点。

RDD本质上是Spark中的⼀个抽象类,所有⼦RDD(HadoopRDD、MapPartitionRDD、JdbcRDD等)都要继承并实现其中的⽅法。

abstract class RDD[T: ClassTag](
@transient private var _sc: SparkContext,
@transient private var deps: Seq[Dependency[_]]
) extends Serializable with Logging {
RDD包含以下成员⽅法或属性:
1、compute⽅法
提供在计算过程中Partition元素的获取与计算⽅式
2、partition的列表
每⼀个partition代表⼀个并⾏的最⼩划分单元;
3、dependencies列表
描述RDD依赖哪些⽗RDD⽣成,即RDD的⾎缘关系;
4、partition的位置列表
定义如何最快速的获取partition的数据,加快计算,这个是可选的,可作为本地化计算的优化选项;
5、partitioner⽅法
定义如何对数据进⾏分区。

RDD⽣成⽅式
1、scala集合
Partition的默认值:defaultParallelism
defaultParallelism与spark的部署模式相关:
Local 模式:本机 cpu cores 的数量
Mesos 模式:8
Yarn:max(2, 所有 executors 的 cpu cores 个数总和)
2、物理数据载⼊
默认为min(defaultParallelism, 2)
3、其他RDD转换
根据具体的转换算⼦⽽定
Partition
Partiton不直接持有数据,仅仅代表了分区的位置(index的值)。

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
override def equals(other: Any): Boolean = super.equals(other)
}
Dependency
从名字可以猜想,他描述了RDD之间的依赖关系。

成员rdd就是⽗RDD,会在构造RDD时被赋值。

abstract class Dependency[T] extends Serializable {
def rdd: RDD[T]
}
由上述RDD、Dependcy关系可画出下图,通过这种⽅式,⼦RDD能轻易找到⽗RDD的位置等信息,从⽽构建出RDD的转换路径,
为DAGScheduler的任务划分及任务执⾏时寻找依赖的数据提供依据。

到此应该能⼤致明⽩RDD中涉及的各个概念的含义及其之间的联系。

但是仔细思考,会发现存在很多问题,⽐如:
既然RDD不携带数据,那么数据是何时加载的?怎么加载的?怎么分布到不同计算节点的?
不同类型的RDD是怎么完成转换的?
RDD计算流程
以下⾯⼏⾏代码为例,解答上述问题。

var sc = new SparkContext();
var hdfs_rdd = sc.textFile(hdfs://master:9000/examples/people.txt); // 加载数据
var rdd = hdfs_rdd.map(_.split(“,”)); // 对每⾏数据按逗号分隔
print(rdd.count()); // 打印数据的条数
RDD的转换
⾸先从直观上了解上述代码执⾏过程中RDD的转换,如下图,Spark按照HDFS中⽂件的block将数据加载到内存,成为初始RDD1,经过每⼀步操作后转换为相应RDD。

⾸先分析textFile⽅法的作⽤,源码如下:
def textFile(
path: String,
minPartitions: Int = defaultMinPartitions): RDD[String] = withScope {
assertNotStopped()
hadoopFile(path, classOf[TextInputFormat], classOf[LongWritable], classOf[Text],
minPartitions).map(pair => pair._2.toString).setName(path)
}
着重看红⾊语句,textFile⽅法实际上是先调⽤了hadoopFile⽅法,再利⽤其返回值调⽤map⽅法,HadoopFile执⾏了什么,返回了什么呢?
def hadoopFile[K, V](
path: String,
inputFormatClass: Class[_ <: InputFormat[K, V]],
keyClass: Class[K],
valueClass: Class[V],
minPartitions: Int = defaultMinPartitions): RDD[(K, V)] = withScope {
assertNotStopped()
// This is a hack to enforce loading hdfs-site.xml.
// See SPARK-11227 for details.
FileSystem.getLocal(hadoopConfiguration)
// A Hadoop configuration can be about 10 KB, which is pretty big, so broadcast it.
val confBroadcast = broadcast(new SerializableConfiguration(hadoopConfiguration))
val setInputPathsFunc = (jobConf: JobConf) => FileInputFormat.setInputPaths(jobConf, path)
new HadoopRDD(
this,
confBroadcast,
Some(setInputPathsFunc),
inputFormatClass,
keyClass,
valueClass,
minPartitions).setName(path)
}
很明显,hadoopFile实际上是获取了HADOOP的配置,然后构造并返回了HadoopRDD对象,HadoopRDD是RDD的⼦类。

因此textFile最后调⽤的是HadoopRDD对象的map⽅法,其实RDD接⼝中定义并实现了map⽅法,所有继承了RDD的类调⽤的map⽅法都来⾃于此。

观察RDD的map⽅法:
def map[U: ClassTag](f: T => U): RDD[U] = withScope {
val cleanF = sc.clean(f)
new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.map(cleanF))
}
map⽅法很简单,⾸先包装⼀下传进来的函数,然后返回MapPartitionsRDD对象。

⾄此,textFile结束,他最终只是返回
了MapPartitionsRDD,并没有执⾏数据读取、计算操作。

接着看下⼀语句:var rdd = hdfs_rdd.map(_.split(“,”));
由上⾯的分析可知hdfs_rdd是⼀个MapPartitionsRDD对象,于是其map⽅法内容与上⽂的⼀模⼀样,也只是返回⼀个包含⽤户函数
的MapPartitionsRDD对象。

⽬前为⽌每个⽅法的调⽤只是返回不同类型的RDD对象,还未真正执⾏计算。

接着看var cnt = rdd.count();
count是⼀种action类型的操作,会触发RDD的计算,为什么说count会触发RDD的计算呢?需要看看count的实现:
def count(): Long = sc.runJob(this, Utils.getIteratorSize _).sum
可以看到,count⽅法中调⽤了sc(sparkContext)的runJob⽅法,该操作将触发DagScheduler去分解任务并提交到集群执⾏。

count⽅法会返回Array[U]类型的结果,数组中每个值代表了当前RDD每个分区中包含的元素个数,因此sum的结果就是RDD中所有元素的个数,本例的结果就是HDFS⽂件中存在⼏⾏数据。

RDD的计算
下⾯介绍任务提交后RDD是怎么计算出来的。

任务分解并提交后开始执⾏,task会在最后⼀个RDD上执⾏compute⽅法。

以上述代码为例,最后⼀个RDD的类型是MapPartitionsRDD,看其compute⽅法:
override def compute(split: Partition, context: TaskContext): Iterator[U] =
f(context, split.index, firstParent[T].iterator(split, context))
其中split是RDD的分区,firstParent是⽗RDD;最外层的f其实是构造MapPartitionsRDD时传⼊的⼀个参数,改参数是⼀个函数对象,接收三个参数并返回Iterator
private[spark] class MapPartitionsRDD[U: ClassTag, T: ClassTag](
var prev: RDD[T],
f: (TaskContext, Int, Iterator[T]) => Iterator[U], // (TaskContext, partition index, iterator)
preservesPartitioning: Boolean = false)
f是何时⽣成的呢?就看何时⽣成的MapPartitionsRDD,参考上⽂可知MapPartitionsRDD是在map⽅法⾥构造的,其第⼆个构造参数就是f的具体实现。

new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.map(cleanF))
综上可知,MapPartitionsRDD的compute中f的作⽤就是就是对f的第三个参数iter执⾏iter.map(cleanF),其中cleanF就是⽤户调⽤map时传⼊的函数,⽽iter⼜是firstParent[T].iterator(split, context)的返回值。

firstParent[T].iterator(split, context)⼜是什么呢?他是对⽗RDD执⾏iterator⽅法,该⽅法是RDD接⼝的final⽅法,因此所有⼦RDD调⽤的都是该⽅法。

final def iterator(split: Partition, context: TaskContext): Iterator[T] = {
if (storageLevel != StorageLevel.NONE) {
getOrCompute(split, context)
} else {
computeOrReadCheckpoint(split, context)
}
}
通过进⼀步查看可知,iterator先判断 RDD 的 storageLevel 是否为 NONE,若不是,则尝试从缓存中读取,读取不到则通过计算来获取该Partition 对应的数据的迭代器;若是,尝试从 checkpoint 中获取 Partition 对应数据的迭代器,若 checkpoint 不存在则通过计算来获取。

Iterator⽅法将返回⼀个迭代器,通过迭代器可以访问⽗RDD的某个分区的每个元素,如果内存中不存在⽗RDD的数据,则调⽤
⽗RDD的compute⽅法进⾏计算。

RDD真正的计算由RDD的action 操作触发,对于action 操作之前的所有Transformation 操作,Spark只记录Transformation的RDD⽣成轨迹,即各个RDD之间的相互依赖关系。

总结
Spark RDD的计算⽅式为:spark是从最后⼀个RDD开始计算(调⽤compute),计算时寻找⽗RDD,若⽗RDD在内存就直接使⽤,否则调⽤⽗RDD的compute计算得出,以此递归,过程可抽象为下图:
从对象产⽣的顺序看,先⽣成了HadoopRDD,调⽤两次map⽅法后依次产⽣两个MapPartitionsRDD;从执⾏的⾓度看,先执⾏最后⼀
个RDD的compute⽅法,在计算过程中递归执⾏⽗RDD的compute,以⽣成对应RDD的数据;从数据加载⾓度看,第⼀个构造出来
的RDD在执⾏compute时才会将数据载⼊内存(本例中为HDFS读⼊内存),然后在这些数据上执⾏⽤户传⼊的⽅法,依次⽣成⼦RDD的内存数据。

相关文档
最新文档