一、什么是RDD
在Spark中,建议大家面向DF/DS编程,但是不管怎么滴,RDD的理解对于后续使用高级别的API使用会更好。
RDD:Resilent Distributed Dataset,弹性分布式数据集,是Spark中最基本的数据抽象(the basic abstraction in spark)
作用:让开发者大大降低开发分布式应用程序的门槛及提高执行效率。
直接GitHub上查看RDD的源码:
https://github.com/apache/spark/blob/master/core/src/main/scala/org/apache/spark/rdd/RDD.scala
A Resilient Distributed Dataset (RDD), the basic abstraction in Spark. Represents an immutable,
2、RDD中的元素可以被分区成很多部分,能够被并行的操作。
* partitioned collection of elements that can be operated on in parallel. This class contains the
* basic operations available on all RDDs, such as `map`, `filter`, and `persist`. In addition,
* [[org.apache.spark.rdd.PairRDDFunctions]] contains operations available only on RDDs of key-value
* pairs, such as `groupByKey` and `join`;
* [[org.apache.spark.rdd.DoubleRDDFunctions]] contains operations available only on RDDs of
* Doubles; and
* [[org.apache.spark.rdd.SequenceFileRDDFunctions]] contains operations available on RDDs that
* can be saved as SequenceFiles.
* All operations are automatically available on any RDD of the right type (e.g. RDD[(Int, Int)])
* through implicit.
===>上述源码剖析:
1、Distributed Dataset:分布式数据集,数据归属在大的机器上,以分区的形式存储;
2、Resilent(弹性)–>弹性表现在计算之上;比如某个节点挂了,某个部分的数据丢失了,能够通过某些机制进行修复,这个就提现了它的弹性;
3、Distributed也意味着数据可以存储在不同的节点之上;
4、Dataset(数据集):可以理解为HDFS中的block,一个文件就是一个Dataset、
-
Represents an immutable,代表了一个不可变的,–》val
意味着我们的RDD一旦产生就是不可变的,RDDA --> RDDB;RDDB必然是一个新的 -
Partitioned collection of elements ,RDD中的元素能够被分区,理解为HDFS中的block,Mapreduce中的Split
-
that can be operated on in parallel, RDD中的元素能够以一种并行的方式被操作,假设RDD中的元素有三个分区:一个RDD元素被拆分成了几块,每一块都在不同的节点上存储:举例:
RDDA:
Hadoop001:Partition1(1、2、3)
Hadoop002:Partition2(4、5、6)
Hadoop003:Partition3(7、8、9)
假设我们对这个RDDA进行了一个+1的操作,operated+1,三个分区中分别存放在三台机器上,operated + 1:是同时对这三个分区进行操作的。
输出:
Hadoop001:Partition1(2、3、4)
Hadoop002:Partition2(5、6、7)
Hadoop003:Partition3(8、9、10)
二、RDD的源码定义
abstract class RDD[T: ClassTag](
@transient private var _sc: SparkContext,
@transient private var deps: Seq[Dependency[_]]
) extends Serializable with Logging {
解析:
1、抽象类RDD不能直接new,必然是有子类实现的,我们使用时直接使用其子类即可,如下是jdbcRDD,都是继承自RDD类
class JdbcRDD[T: ClassTag](
sc: SparkContext,
getConnection: () => Connection,
sql: String,
lowerBound: Long,
upperBound: Long,
numPartitions: Int,
mapRow: (ResultSet) => T = JdbcRDD.resultSetToObjectArray _)
extends RDD[T](sc, Nil) with Logging {
2、序列化Serializable 网络的传输
3、logging 记录日志的,在spark1.6中是能直接使用的,在spark2.0以后被移除了。
4、T 泛型,支持字符串,自定义类型
5、sparkContext
6、@transient
三、RDD五大特性详解
- 五大特性在源码中有所描述:
Internally, each RDD is characterized by five main properties:
*
* - A list of partitions
* - A function for computing each split
* - A list of dependencies on other RDDs
* - Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned)
* - Optionally, a list of preferred locations to compute each split on (e.g. block locations for
* an HDFS file)
1、A list of partitions
- RDD是由一系列的分区所构成的
2、A function for computing each split
- 举例:+1这个操作就是对所有分区进行的
3、A list of dependencies on other RDDs
- RDDA --> RDDB --> RDDC --> RDDD,每一个分区间都有依赖关系
4、Optionally, a Partitioner for key-value RDDs(eg. to say that the RDD is hash-partiitoned)
- 在Spark中默认是一个hash还有一个range的分区
5、Optionnally,a list of preferred locations to compute each split on (bolck location for HDFS file)
- 数据本地化的概念,计算的数据在哪,就把task分配到数据所在的地方,因为对于hdfs来说是有多个副本的
–>RDD的这5大特性和Resilent Distributed Dataset之间的关系,如何与之对应。
小结:
在信息爆炸的今天,即使单台机器的配置再怎么高,也是应付不了大规模的数据处理的;在数据存储的时候选择分布式的,在计算的时候也要选择分布式,在数据存储的时候要进行分割,并且以多副本的方式进行保存,此举以便于容错。计算的时候也要进行切分,以一个task去处理2个block。传统的分布式存储(HDFS)+分布式计算(MapReduce)
四、RDD五大特性和源码的对应关系
1、def compute(split: Partition, context: TaskContext): Iterator[T]
- 对应RDD的第二个特性,做计算就是对RDD中的分区做计算;计算方法传入参数肯定有Partition,后面是
2、protected def getPartitions: Array[Partition]
- 一个RDD是由一系列的分区构成,那它返回的必然是一个数组或集合,集合中存放类型必然是Partition ⇒ 对应RDD中的第一个特性 a list of partitions
3、protected def getDependencies: Seq[Dependency[_]] = deps
- RDD中是有转换的,体现在依赖关系上,RDD --> RDDB --> RDDC
4、protected def getPreferredLocations(split: Partition): Seq[String] = Nil
- 定义了getPreferredLocations方法,得到最优先的位置,返回的是一个数组或集合的东西;对应RDD中的数据本地化特点。
5、@transient val partitioner: Option[Partitioner] = None
- 以键值对方式(key,value)的时候有一个partiitoner,—>对应第四大特点
打开JdbcRDD.scala
override def getPartitions: Array[Partition] = {
// bounds are inclusive, hence the + 1 here and - 1 on end
val length = BigInt(1) + upperBound - lowerBound
(0 until numPartitions).map { i =>
val start = lowerBound + ((i * length) / numPartitions)
val end = lowerBound + (((i + 1) * length) / numPartitions) - 1
new JdbcPartition(i, start.toLong, end.toLong)
}.toArray
} //获取到分区信息,操作JDBC的方式,代码中有一个起始和结束位置,eg:有10个分区,10000调数据,每个分区处理数据都有对应顺序,比如第一个分区处理第1~1000条数据,第二个分区处理1001~2000的数据。
每个分区做一个map,开始和结束构建出来一个JdbcParttion,就能得到partitions.
2、标准的Jdbc编程
override def compute(thePart: Partition, context: TaskContext): Iterator[T] = new NextIterator[T]
{
context.addTaskCompletionListener[Unit]{ context => closeIfNeeded() }
val part = thePart.asInstanceOf[JdbcPartition]
val conn = getConnection()
val stmt = conn.prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY)
val url = conn.getMetaData.getURL
if (url.startsWith("jdbc:mysql:")) {
// setFetchSize(Integer.MIN_VALUE) is a mysql driver specific way to force
// streaming results, rather than pulling entire resultset into memory.
// See the below URL
// dev.mysql.com/doc/connector-j/5.1/en/connector-j-reference-implementation-notes.html
stmt.setFetchSize(Integer.MIN_VALUE)
} else {
stmt.setFetchSize(100)
}
logInfo(s"statement fetch size set to: ${stmt.getFetchSize}")
stmt.setLong(1, part.lower)
stmt.setLong(2, part.upper)
val rs = stmt.executeQuery()
override def getNext(): T = {
if (rs.next()) {
mapRow(rs)
} else {
finished = true
null.asInstanceOf[T]
}
}
override def close() {
try {
if (null != rs) {
rs.close()
}
} catch {
case e: Exception => logWarning("Exception closing resultset", e)
}
try {
if (null != stmt) {
stmt.close()
}
} catch {
case e: Exception => logWarning("Exception closing statement", e)
}
try {
if (null != conn) {
conn.close()
}
logInfo("closed connection")
} catch {
case e: Exception => logWarning("Exception closing connection", e)
}
}
}
}
3、HadoopRDD.scala(读取hadoop文件的)
override def getPartitions: Array[Partition] = {
val jobConf = getJobConf()
// add the credentials here as this can be called before SparkContext initialized
SparkHadoopUtil.get.addCredentials(jobConf)
try {
val allInputSplits = getInputFormat(jobConf).getSplits(jobConf, minPartitions)
val inputSplits = if (ignoreEmptySplits) {
allInputSplits.filter(_.getLength > 0)
} else {
allInputSplits
}
val array = new Array[Partition](inputSplits.size)
for (i <- 0 until inputSplits.size) {
array(i) = new HadoopPartition(id, i, inputSplits(i))
}
array
//读文件借助于inputformat,对于RDD来说,inputformat.getsplits,拿到一堆inputsplits,partition大小就是inputsplits大小,构建一个数组,每个数组传一个partition,数组大小就是inptsplits.size;迭代inputsplits,每个构建hadoopPartition。
五、图解RDD
- 如下图:1个RDD中有5个分区,每个分区中有5个元素,这5个分区分别存储在不同的节点上(NODE1、NODE2、NODE3);数据极有可能存放在disk上也有可能存放在memory上,由于多副本策略,也有可能一个分区存多份;
- 此时有5个分区,必然会启动5个task,如果core够的话,5个task必然是会并行执行的;如果core不够的话,会先跑完一轮再跑下一轮。
eg:partition的数据分布在各个节点上,如果在hadoop002上运行partition1(需要先通过网络把Partition的数据拷贝到hadoop002上才能做响应的计算,提现的是数据的本地性)