RDD

RDD初始參数:上下文和一组依赖

abstract class RDD[T: ClassTag](
@transient private var sc: SparkContext,
@transient private var deps: Seq[Dependency[_]]
) extends Serializable

下面须要细致理清:

A list of Partitions

Function to compute split (sub RDD impl)

A list of Dependencies

Partitioner for K-V RDDs (Optional)

Preferred locations to compute each spliton (Optional)

Dependency

Dependency代表了RDD之间的依赖关系。即血缘

RDD中的使用

RDD给子类提供了getDependencies方法来制定怎样依赖父类RDD

protected def getDependencies: Seq[Dependency[_]] = deps

其实,在获取first parent的时候,子类常常会使用以下这种方法

protected[spark] def firstParent[U: ClassTag] = {
dependencies.head.rdd.asInstanceOf[RDD[U]]
}

能够看到,Seq里的第一个dependency应该是直接的parent,从而从第一个dependency类里获得了rdd。这个rdd就是父RDD。

一般的RDD子类都会这么实现compute和getPartition方法,以SchemaRDD举例:

override def compute(split: Partition, context: TaskContext): Iterator[Row] =
firstParent[Row].compute(split, context).map(_.copy()) override def getPartitions: Array[Partition] = firstParent[Row].partitions

compute()方法调用了第一个父类的compute,把结果RDD copy返回

getPartitions返回的就是第一个父类的partitions

以下看一下Dependency类及其子类的实现。

宽依赖和窄依赖

abstract class Dependency[T](val rdd: RDD[T]) extends Serializable

Dependency里传入的rdd。就是父RDD本身。

继承结构例如以下:

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcGVsaWNr/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="">


NarrowDependency代表窄依赖。即父RDD的分区,最多被子RDD的一个分区使用。所以支持并行计算。

子类须要实现方法:

def getParents(partitionId: Int): Seq[Int]

OneToOneDependency表示父RDD和子RDD的分区依赖是一对一的。

RangeDependency表示在一个range范围内,依赖关系是一对一的,所以初始化的时候会有一个范围。范围外的partitionId,传进去之后返回的是Nil。

以下介绍宽依赖。

class ShuffleDependency[K, V](
@transient rdd: RDD[_ <: Product2[K, V]],
val partitioner: Partitioner,
val serializer: Serializer = null)
extends Dependency(rdd.asInstanceOf[RDD[Product2[K, V]]]) { // 上下文增量定义的Id
val shuffleId: Int = rdd.context.newShuffleId() // ContextCleaner的作用和实如今SparkContext章节叙述
rdd.sparkContext.cleaner.foreach(_.registerShuffleForCleanup(this))
}

宽依赖针对的RDD是KV形式的。须要一个partitioner指定分区方式(下一节介绍)。须要一个序列化工具类,序列化工具眼下的实现例如以下:



宽依赖和窄依赖对失败恢复时候的recompute有不同程度的影响,宽依赖可能是要所有计算的。

Partition

Partition详细表示RDD每一个数据分区。

Partition提供trait类,内含一个index和hashCode()方法,详细子类实现与RDD子类有关。种类例如以下:


在分析每一个RDD子类的时候再涉及。

Partitioner

Partitioner决定KV形式的RDD怎样依据key进行partition

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcGVsaWNr/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="">

abstract class Partitioner extends Serializable {
def numPartitions: Int // 总分区数
def getPartition(key: Any): Int
}

在ShuffleDependency里相应一个Partitioner,来完毕宽依赖下。子RDD怎样获取父RDD。

默认Partitioner

Partitioner的伴生对象提供defaultPartitioner方法,逻辑为:

传入的RDD(至少两个)中,遍历(顺序是partition数目从大到小)RDD,假设已经有Partitioner了,就使用。

假设RDD们都没有Partitioner,则使用默认的HashPartitioner。

而HashPartitioner的初始化partition数目,取决于是否设置了spark.default.parallelism,假设没有的话就取RDD中partition数目最大的值。

假设上面这段文字看起来费解。代码例如以下:

  def defaultPartitioner(rdd: RDD[_], others: RDD[_]*): Partitioner = {
val bySize = (Seq(rdd) ++ others).sortBy(_.partitions.size).reverse
for (r <- bySize if r.partitioner.isDefined) {
return r.partitioner.get
}
if (rdd.context.conf.contains("spark.default.parallelism")) {
new HashPartitioner(rdd.context.defaultParallelism)
} else {
new HashPartitioner(bySize.head.partitions.size)
}
}

HashPartitioner

HashPartitioner基于java的Object.hashCode。会有个问题是Java的Array有自己的hashCode,不基于Array里的内容,所以RDD[Array[_]]或RDD[(Array[_], _)]使用HashPartitioner会有问题。

顾名思义。getPartition方法实现例如以下

  def getPartition(key: Any): Int = key match {
case null => 0
case _ => Utils.nonNegativeMod(key.hashCode, numPartitions)
}

RangePartitioner

RangePartitioner处理的KV RDD要求Key是可排序的,即满足Scala的Ordered[K]类型。所以它的构造例如以下:

class RangePartitioner[K <% Ordered[K]: ClassTag, V](
partitions: Int,
@transient rdd: RDD[_ <: Product2[K,V]],
private val ascending: Boolean = true)
extends Partitioner {

内部会计算一个rangBounds(上界),在getPartition的时候。假设rangBoundssize小于1000,则逐个遍历获得;否则二分查找获得partitionId。

Persist

默认cache()过程是将RDD persist在内存里,persist()操作能够为RDD又一次指定StorageLevel。

class StorageLevel private(
private var useDisk_ : Boolean,
private var useMemory_ : Boolean,
private var useOffHeap_ : Boolean,
private var deserialized_ : Boolean,
private var replication_ : Int = 1)
object StorageLevel {
val NONE = new StorageLevel(false, false, false, false)
val DISK_ONLY = new StorageLevel(true, false, false, false)
val DISK_ONLY_2 = new StorageLevel(true, false, false, false, 2)
val MEMORY_ONLY = new StorageLevel(false, true, false, true)
val MEMORY_ONLY_2 = new StorageLevel(false, true, false, true, 2)
val MEMORY_ONLY_SER = new StorageLevel(false, true, false, false)
val MEMORY_ONLY_SER_2 = new StorageLevel(false, true, false, false, 2)
val MEMORY_AND_DISK = new StorageLevel(true, true, false, true)
val MEMORY_AND_DISK_2 = new StorageLevel(true, true, false, true, 2)
val MEMORY_AND_DISK_SER = new StorageLevel(true, true, false, false)
val MEMORY_AND_DISK_SER_2 = new StorageLevel(true, true, false, false, 2)
val OFF_HEAP = new StorageLevel(false, false, true, false) // Tachyon

RDD的persist()和unpersist()操作,都是由SparkContext运行的(SparkContext的persistRDD和unpersistRDD方法)。

Persist过程是把该RDD存在上下文的TimeStampedWeakValueHashMap里维护起来。也就是说,事实上persist并非action。并不会触发不论什么计算。

Unpersist步骤例如以下。会交给SparkEnv里的BlockManager处理。

  private[spark] def unpersistRDD(rddId: Int, blocking: Boolean = true) {
env.blockManager.master.removeRdd(rddId, blocking)
persistentRdds.remove(rddId)
listenerBus.post(SparkListenerUnpersistRDD(rddId))
}

Checkpoint

RDD Actions api里提供了checkpoint()方法,会把本RDD save到SparkContext CheckpointDir

文件夹下。建议该RDD已经persist在内存中,否则须要recomputation。

假设该RDD没有被checkpoint过,则会生成新的RDDCheckpointData。

RDDCheckpointData类与一个RDD关联,记录了checkpoint相关的信息,而且记录checkpointRDD的一个状态,

[ Initialized --> marked for checkpointing-->
checkpointing in progress --> checkpointed ]

内部有一个doCheckpoint()方法(会被以下调用)。

运行逻辑

真正的checkpoint触发,在RDD私有方法doCheckpoint()里。doCheckpoint()会被DAGScheduler调用。且是在此次job里使用这个RDD完成之后,此时这个RDD就已经被计算或者物化过了。能够看到。会对RDD的父RDD进行递归。

  private[spark] def doCheckpoint() {
if (!doCheckpointCalled) {
doCheckpointCalled = true
if (checkpointData.isDefined) {
checkpointData.get.doCheckpoint()
} else {
dependencies.foreach(_.rdd.doCheckpoint())
}
}
}

RDDCheckpointData的doCheckpoint()方法关键代码例如以下:

// Create the output path for the checkpoint
val path = new Path(rdd.context.checkpointDir.get, "rdd-" + rdd.id)
val fs = path.getFileSystem(rdd.context.hadoopConfiguration)
if (!fs.mkdirs(path)) {
throw new SparkException("Failed to create checkpoint path " + path)
} // Save to file, and reload it as an RDD
val broadcastedConf = rdd.context.broadcast(
new SerializableWritable(rdd.context.hadoopConfiguration))
// 这次runJob终于调的是dagScheduler的runJob
rdd.context.runJob(rdd,
CheckpointRDD.writeToFile(path.toString, broadcastedConf) _)
// 此时rdd已经记录到磁盘上
val newRDD = new CheckpointRDD[T](rdd.context, path.toString)
if (newRDD.partitions.size != rdd.partitions.size) {
throw new SparkException("xxx")
}

runJob终于调的是dagScheduler的runJob。

做完后。生成一个CheckpointRDD。

详细CheckpointRDD相关内容能够參考其它章节。

API

子类须要实现的方法

// 计算某个分区
def compute(split: Partition, context: TaskContext): Iterator[T] protected def getPartitions: Array[Partition]
// 依赖的父RDD,默认就是返回整个dependency序列
protected def getDependencies: Seq[Dependency[_]] = deps protected def getPreferredLocations(split: Partition): Seq[String] = Nil

Transformations

略。


Actions

略。



SubRDDs

部分RDD子类的实现分析,包含下面几个部分:

1)  子类本身构造參数

2)  子类的特殊私有变量

3)  子类的Partitioner实现

4)  子类的父类函数实现

def compute(split: Partition, context: TaskContext): Iterator[T]
protected def getPartitions: Array[Partition]
protected def getDependencies: Seq[Dependency[_]] = deps
protected def getPreferredLocations(split: Partition): Seq[String] = Nil

CheckpointRDD

class CheckpointRDD[T: ClassTag](sc: SparkContext, val checkpointPath: String)
extends RDD[T](sc, Nil)

CheckpointRDDPartition继承自Partition。没有什么添加。

有一个被广播的hadoop conf变量,在compute方法里使用(readFromFile的时候用)

val broadcastedConf = sc.broadcast(
new SerializableWritable(sc.hadoopConfiguration))

getPartitions: Array[Partition]方法:

依据checkpointPath去查看Path下有多少个partitionFile,File个数为partition数目。

getPartitions方法返回的Array[Partition]内容为New CheckpointRDDPartition(i),i为[0, 1, …, partitionNum]

getPreferredLocations(split:Partition): Seq[String]方法:

文件位置信息,借助hadoop core包。获得block location,把得到的结果依照host打散(flatMap)并过滤掉localhost,返回。

compute(split: Partition, context:TaskContext): Iterator[T]方法:

调用CheckpointRDD.readFromFile(file,
broadcastedConf,context)方法,当中file为hadoopfile path,conf为广播过的hadoop conf。

Hadoop文件读写及序列化

伴生对象提供writeToFile方法和readFromFile方法。主要用于读写hadoop文件,而且利用env下的serializer进行序列化和反序列化工作。两个方法详细实现例如以下:

def writeToFile[T](
path: String,
broadcastedConf: Broadcast[SerializableWritable[Configuration]],
blockSize: Int = -1
)(ctx: TaskContext, iterator: Iterator[T]) {

创建hadoop文件的时候会若存在会抛异常。把hadoop的outputStream放入serializer的stream里。serializeStream.writeAll(iterator)写入。

writeToFile的调用在RDDCheckpointData类的doCheckpoint方法里。例如以下:

rdd.context.runJob(rdd,
CheckpointRDD.writeToFile(path.toString, broadcastedConf) _)
def readFromFile[T](
path: Path,
broadcastedConf: Broadcast[SerializableWritable[Configuration]],
context: TaskContext
): Iterator[T] = {

打开Hadoop的inutStream,读取的时候使用env下的serializer得到反序列化之后的流。返回的时候,DeserializationStream这个trait提供了asIterator方法,每次next操作能够进行一次readObject。

在返回之前,调用了TaskContext提供的addOnCompleteCallback回调。用于关闭hadoop的inputStream。

NewHadoopRDD

class NewHadoopRDD[K, V](
sc : SparkContext,
inputFormatClass: Class[_ <: InputFormat[K, V]],
keyClass: Class[K],
valueClass: Class[V],
@transient conf: Configuration)
extends RDD[(K, V)](sc, Nil)
with SparkHadoopMapReduceUtil
private[spark] class NewHadoopPartition(
rddId: Int,
val index: Int,
@transient rawSplit: InputSplit with Writable)
extends Partition { val serializableHadoopSplit = new SerializableWritable(rawSplit) override def hashCode(): Int = 41 * (41 + rddId) + index
}

getPartitions操作:

依据inputFormatClass和conf,通过hadoop InputFormat实现类的getSplits(JobContext)方法得到InputSplits。(ORCFile在此处的优化)

这样获得的split同RDD的partition直接相应。

compute操作:

针对本次split(partition),调用InputFormat的createRecordReader(split)方法,

得到RecordReader<K,V>。这个RecordReader包装在Iterator[(K,V)]类内,复写Iterator的next()和hasNext方法,让compute返回的InterruptibleIterator[(K,V)]可以被迭代获得RecordReader取到的数据。

getPreferredLocations(split: Partition)操作:

theSplit.serializableHadoopSplit.value.getLocations.filter(_ != "localhost")

在NewHadoopPartition里SerializableWritable将split序列化,然后调用InputSplit本身的getLocations接口,得到有数据分布节点的nodes name列表。


WholeTextFileRDD

NewHadoopRDD的子类

private[spark] class WholeTextFileRDD(
sc : SparkContext,
inputFormatClass: Class[_ <: WholeTextFileInputFormat],
keyClass: Class[String],
valueClass: Class[String],
@transient conf: Configuration,
minSplits: Int)
extends NewHadoopRDD[String, String](sc, inputFormatClass, keyClass, valueClass, conf) {

复写了getPartitions方法:

NewHadoopRDD有自己的inputFormat实现类和recordReader实现类。在spark/input package下专门写了这两个类的实现。感觉是种參考。


InputFormat

WholeTextFileRDD在spark里实现了自己的inputFormat。

读取的File以K,V的结构获取。K为path。V为整个file的content。

复写createRecordReader以使用WholeTextFileRecordReader

复写setMaxSplitSize方法,因为用户能够传入minSplits数目,计算平均大小(splits files总大小除以split数目)的时候就变了。


RecordReader

复写nextKeyValue方法。会读出指定path下的file的内容,生成new Text()给value,结果是String。假设文件正在被别的进行打开着,会返回false。否则把file内容读进value里。


使用场景

在SparkContext下提供wholeTextFile方法,

def wholeTextFiles(path: String, minSplits: Int = defaultMinSplits):
RDD[(String, String)]

用于读取一个路径下的全部text文件,以K。V的形式返回,K为一个文件的path,V为文件内容。比較适合小文件。


全文完  :)



Spark Core源代码分析: RDD基础的更多相关文章

  1. Spark Core源代码分析: Spark任务运行模型

    DAGScheduler 面向stage的调度层,为job生成以stage组成的DAG,提交TaskSet给TaskScheduler运行. 每个Stage内,都是独立的tasks,他们共同运行同一个 ...

  2. Spark Core源代码分析: Spark任务模型

    概述 一个Spark的Job分为多个stage,最后一个stage会包含一个或多个ResultTask,前面的stages会包含一个或多个ShuffleMapTasks. ResultTask运行并将 ...

  3. Spark SQL 源代码分析之Physical Plan 到 RDD的详细实现

    /** Spark SQL源代码分析系列文章*/ 接上一篇文章Spark SQL Catalyst源代码分析之Physical Plan.本文将介绍Physical Plan的toRDD的详细实现细节 ...

  4. Spark SQL 源代码分析之 In-Memory Columnar Storage 之 in-memory query

    /** Spark SQL源代码分析系列文章*/ 前面讲到了Spark SQL In-Memory Columnar Storage的存储结构是基于列存储的. 那么基于以上存储结构,我们查询cache ...

  5. Spark SQL 源代码分析系列

    从决定写Spark SQL文章的源代码分析,到现在一个月的时间,一个又一个几乎相同的结束很快,在这里也做了一个综合指数,方便阅读,下面是读取顺序 :) 第一章 Spark SQL源代码分析之核心流程 ...

  6. Spark SQL源代码分析之核心流程

    /** Spark SQL源代码分析系列文章*/ 自从去年Spark Submit 2013 Michael Armbrust分享了他的Catalyst,到至今1年多了,Spark SQL的贡献者从几 ...

  7. Spark Core Runtime分析: DAGScheduler, TaskScheduler, SchedulerBackend

    Spark Runtime里的主要层次分析,梳理Runtime组件和运行流程, DAGScheduler Job=多个stage,Stage=多个同种task, Task分为ShuffleMapTas ...

  8. Spark 源码分析 -- RDD

    关于RDD, 详细可以参考Spark的论文, 下面看下源码 A Resilient Distributed Dataset (RDD), the basic abstraction in Spark. ...

  9. 【Spark Core】任务运行机制和Task源代码浅析1

    引言 上一小节<TaskScheduler源代码与任务提交原理浅析2>介绍了Driver側将Stage进行划分.依据Executor闲置情况分发任务,终于通过DriverActor向exe ...

随机推荐

  1. iOS 调H5方法不执行没反应的坑

    调用H5的方法需要给H5传一些参数,参数中包括图片的base64字符串. 错误一: 图片转base64,后面参数不能随便写,正确做法如下 NSData *imageData = UIImageJPEG ...

  2. HDU 3032 Nim or not Nim?(博弈,SG打表找规律)

    Nim or not Nim? Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)T ...

  3. ZooKeeper服务器是用Java创建的,它在JVM上运行。

    ZooKeeper服务器是用Java创建的,它在JVM上运行. 创建配置文件 使用命令 vi conf/zoo.cfg 和所有以下参数设置为起点,打开名为 conf/zoo.cfg 的配置文件. $ ...

  4. SublimeText3插件Emmet自定义HTML

    工作上总使用Sublime,Emmet默认生成html格式不太满足需要,生成后还需要额外添加一些元素,如下是自定义html格式的步骤,供大家参考: 1. 安装Sublime Text3.Emmet插件 ...

  5. docker 实现redis集群搭建

    摘要:接触docker以来,似乎养成了一种习惯,安装什么应用软件都想往docker方向做,今天就想来尝试下使用docker搭建redis集群. 首先,我们需要理论知识:Redis Cluster是Re ...

  6. Thunderbird使用发邮件模板

    Thunderbird的强大之处是可以使用多种第三方插件,其中有个插件SmartTemplate4,是用来设置Thunderbird发件模板的. 然后,模板设置内容如下: <p>:< ...

  7. 'dict_values' object does not support indexing, Python字典dict中由value查key

    Python字典dict中由value查key 众所周知,字典dict最大的好处就是查找或插入的速度极快,并且不想列表list一样,随着key的增加越来越复杂.但是dict需要占用较大的内存空间,换句 ...

  8. centos7服务器基本的安全设置

    在使用云服务器的过程中经常会遇到很多非法的入侵试图登录服务器,所以我们需要对服务器进行安全防范 关闭ping扫描,虽然没什么卵用 先切换到root echo 1 > /proc/sys/net/ ...

  9. D3.js系列——布局:饼状图和力导向图

    一.饼状图 在布局的应用中,最简单的就是饼状图. 1.数据 有如下数据,需要可视化: , , , , ]; 这样的值是不能直接绘图的.例如绘制饼状图的一个部分,需要知道一段弧的起始角度和终止角度,这些 ...

  10. float:center???

    老规矩,先上图(请忽略图中文字^V^): 乍一看感觉是对中间的图片使用了float:center;可是细致一想float属性是没有center这个值的. 那是怎么实现的呢?我一步一步拆给大家看. 1. ...