目录
RDD介绍
Resilient Distributed Datasets (弹性分布式数据集,简称RDD),特点是可以并行操作,并且是容错的。
有两种方法可以创建RDD:
1)执行Transform操作(变换操作);
2)读取外部存储系统的数据集,如HDFS,HBase,或任何与Hadoop有关的数据源。
RDD入门示例
val data = Array(1, 2, 3, 4, 5)
val r1 = sc.parallelize(data)
val r2 = sc.parallelize(data,2)
你可以这样理解RDD:它是spark提供的一个特殊集合类。诸如普通的集合类型,如传统的Array:(1,2,3,4,5)是一个整体,但转换成RDD后,我们可以对数据进行Partition(分区)处理,这样做的目的就是为了分布式。你可以让这个RDD有两个分区,那么有可能是这个形式:RDD(1,2) (3,4)。这样设计的目的在于:可以进行分布式运算。
注:创建RDD的方式有多种,比如案例一中是基于一个基本的集合类型(Array)转换而来,像parallelize这样的方法还有很多,此外,我们也可以在读取数据集时就创建RDD。
val distFile = sc.textFile("data.txt")
查看RDD
scala>rdd.collect
收集rdd中的数据组成Array返回,此方法将会把分布式存储的rdd中的数据集中到一台机器中组建Array。
在生产环境下一定要慎用这个方法,容易内存溢出。
查看RDD的分区数量:
scala>rdd.partitions.size
查看RDD每个分区的元素:
scala>rdd.glom.collect
此方法会将每个分区的元素以Array形式返回
RDD操作
针对RDD的操作,分两种,一种是Transformation(变换),一种是Actions(执行)。Transformation(变换)操作属于懒操作,不会真正触发RDD的处理计算。Actions(执行)操作才会真正触发任务执行。
一些常见的Transformation操作:
Transformation | Meaning |
map(func) | Return a new distributed dataset formed by passing each element of the source through a function func. 参数是函数,函数应用于RDD每一个元素,返回值是新的RDD。 |
flatMap(func) | Similar to map, but each input item can be mapped to 0 or more output items (so func should return a Seq rather than a single item). 扁平化map,对RDD每个元素转换, 然后再扁平化处理 注:map和flatMap有何不同? map: 对RDD每个元素转换 flatMap: 对RDD每个元素转换, 然后再扁平化(即去除集合),所以,一般我们在读取数据源后,第一步执行的操作是flatMap。 |
filter(func) | Return a new dataset formed by selecting those elements of the source on which func returns true. 参数是函数,函数会过滤掉不符合条件的元素,返回值是新的RDD。 |
mapPartitions(func) | Similar to map, but runs separately on each partition (block) of the RDD, so func must be of type Iterator<T> => Iterator<U> when running on an RDD of type T. 该函数和map函数类似,只不过映射函数的参数由RDD中的每一个元素变成了RDD中每一个分区的迭代器。
|
mapPartitionsWithIndex(func) | Similar to mapPartitions, but also provides func with an integer value representing the index of the partition, so func must be of type (Int, Iterator<T>) => Iterator<U> when running on an RDD of type T. 函数作用同mapPartitions,不过提供了两个参数,第一个参数为分区的索引。 |
union(otherDataset) | Return a new dataset that contains the union of the elements in the source dataset and the argument. 两个数据集的并集,不包括重复行,要求列数要一样,类型可以不同。 |
一些常见的actions操作:
Action | Meaning |
reduce(func) | Aggregate the elements of the dataset using a function func (which takes two arguments and returns one). The function should be commutative and associative so that it can be computed correctly in parallel. 并行整合所有RDD数据,例如求和操作。 |
collect() | Return all the elements of the dataset as an array at the driver program. This is usually useful after a filter or other operation that returns a sufficiently small subset of the data. 返回RDD所有元素,将rdd分布式存储在集群中不同分区的数据 获取到一起组成一个数组返回。注意 这个方法将会把所有数据收集到一个机器内,容易造成内存的溢出 ,在生产环境下千万慎用。 |
count() | Return the number of elements in the dataset. 统计RDD里元素个数。 |
first() | Return the first element of the dataset (similar to take(1)). |
take(n) | Return an array with the first n elements of the dataset. 获取前几个数据。 |
takeOrdered(n, [ordering]) | Return the first n elements of the RDD using either their natural order or a custom comparator. 先将rdd中的数据进行升序排序 ,然后取前n个。 |
top(n) | 先将rdd中的数据进行降序排序 ,然后取前n个。) |
saveAsTextFile(path) | Write the elements of the dataset as a text file (or set of text files) in a given directory in the local filesystem, HDFS or any other Hadoop-supported file system. Spark will call toString on each element to convert it to a line of text in the file. 按照文本方式保存分区数据。 |
DAG介绍
Directed Acyclic Graph(DAG)即有向无环图,Spark会根据用户提交的计算逻辑中的RDD的转换和动作来生成RDD之间的依赖关系,同时这个计算链也就生成了逻辑上的DAG。至于什么是有向无环图,简单的说就是数据处理的总体方向是单一的,不会出现首尾相接的情况。
我们接下来以“Word Count”为例,详细描述这个DAG生成的实现过程。
Spark Scala版本的Word Count程序如下:
val file=sc.textFile("hdfs://hadoop01:9000/hello1.txt")
val counts = file.flatMap(line => line.split(" "))
.map(word => (word, 1))
.reduceByKey(_ + _)
counts.saveAsTextFile("hdfs://...")
file和counts都是RDD,其中file是从HDFS上读取文件并创建了RDD,而counts是在file的基础上通过flatMap、map和reduceByKey这三个RDD转换生成的。最后,counts调用了saveAsTextFile,用户的计算逻辑就从这里开始提交的集群进行计算。那么上面这5行代码的具体实现是什么呢?
1)第1行:sc是org.apache.spark.SparkContext的实例,它是用户程序和Spark的交互接口,会负责连接到集群管理者,并根据用户设置或者系统默认设置来申请计算资源,完成RDD的创建等。
sc.textFile("hdfs://...")就完成了一个org.apache.spark.rdd.HadoopRDD的创建,并且完成了一次RDD的转换:通过map转换到一个org.apache.spark.rdd.MapPartitions-RDD。也就是说,file实际上是一个MapPartitionsRDD,它保存了文件的所有行的数据内容。
2)第2行:将file中的所有行的内容,以空格分隔为单词的列表,然后将这个按照行构成的单词列表合并为一个列表。最后,以每个单词为元素的列表被保存到MapPartitionsRDD。
3)第3行:将第2步生成的MapPartittionsRDD再次经过map将每个单词word转为(word,1)的元组。这些元组最终被放到一个MapPartitionsRDD中。
4)第4行:首先会生成一个MapPartitionsRDD,起到map端combiner的作用;然后会生成一个ShuffledRDD,它从上一个RDD的输出读取数据,作为reducer的开始;最后,还会生成一个MapPartitionsRDD,起到reducer端reduce的作用。
5)第5行:向HDFS输出RDD的数据内容。最后,调用org.apache.spark.SparkContext#runJob向集群提交这个计算任务。
最后我们来看一下这整个过程,实际上数据的处理方向是单一的,数据处理流程没有循环往复的情况。有人会说我们从HDFS读的数据,最后又把计算结果写回了HDFS,这不就是一个环了吗?这样理解是不对的,首先我们读取数据的目录和写入结果的目录不是同一个目录,数据的读取和计算结果不会相互影响。那么又有人会说,如果我把计算结果写入到读取数据的文件呢,那这样就是一个环了。这也是不能实现的,spark写入计算结果的目录必须是空目录,否则会抛出异常。并且我们正常的情况下,也不会去干这样的蠢事。
RDD的依赖关系
RDD和它依赖的parent RDD(s)的关系有两种不同的类型,即窄依赖(narrow dependency)和宽依赖(wide dependency)。
1)窄依赖指的是每一个parent RDD的Partition最多被子RDD的一个Partition使用,如下图所示。
2)宽依赖指的是多个子RDD的Partition会依赖同一个parent RDD的Partition。
我们可以从不同类型的转换来进一步理解RDD的窄依赖和宽依赖的区别,如下图所示。
窄依赖
对于窄依赖,它们只是将Partition的数据根据转换的规则进行转化,并不涉及其他的处理,可以简单地认为只是将数据从一个形式转换到另一个形式。
窄依赖底层的源码:
abstract class NarrowDependency[T](_rdd: RDD[T]) extends Dependency[T] {
//返回子RDD的partitionId依赖的所有的parent RDD的Partition(s)
def getParents(partitionId: Int): Seq[Int]
override def rdd: RDD[T] = _rdd
}
class OneToOneDependency[T](rdd: RDD[T]) extends NarrowDependency[T](rdd) {
override def getParents(partitionId: Int) = List(partitionId)
}
所以对于窄依赖,并不会引入昂贵的Shuffle。所以执行效率非常高。如果整个DAG中存在多个连续的窄依赖,则可以将这些连续的窄依赖整合到一起连续执行,中间不执行shuffle 从而提高效率,这样的优化方式称之为流水线优化。
此外,针对窄依赖,如果子RDD某个分区数据丢失,只需要找到父RDD对应依赖的分区,恢复即可。但如果是宽依赖,当分区丢失时,最糟糕的情况是要重算所有父RDD的所有分区。
宽依赖
对于groupByKey这样的操作,子RDD的所有Partition(s)会依赖于parent RDD的所有Partition(s),子RDD的Partition是parent RDD的所有Partition Shuffle的结果。
宽依赖的源码:
class ShuffleDependency[K, V, C](
@transient _rdd: RDD[_ <: Product2[K, V]],
val partitioner: Partitioner,
val serializer: Option[Serializer] = None,
val keyOrdering: Option[Ordering[K]] = None,
val aggregator: Option[Aggregator[K, V, C]] = None,
val mapSideCombine: Boolean = false)
extends Dependency[Product2[K, V]] {
override def rdd = _rdd.asInstanceOf[RDD[Product2[K, V]]]
//获取新的shuffleId
val shuffleId: Int = _rdd.context.newShuffleId()
//向ShuffleManager注册Shuffle的信息
val shuffleHandle: ShuffleHandle =
_rdd.context.env.shuffleManager.registerShuffle(
shuffleId, _rdd.partitions.size, this)
_rdd.sparkContext.cleaner.foreach(_.registerShuffleForCleanup(this))
}
Shuffle概述
spark中一旦遇到宽依赖就需要进行shuffle的操作,所谓的shuffle的操作的本质就是将数据汇总后重新分发的过程。这个过程数据要汇总到一起,数据量可能很大所以不可避免的需要进行数据落磁盘的操作,会降低程序的性能,所以spark并不是完全内存不读写磁盘,只能说它尽力避免这样的过程来提高效率 。
spark中的shuffle,在早期的版本中,会产生多个临时文件,但是这种多临时文件的策略造成大量文件的同时的读写,磁盘的性能被分摊给多个文件,每个文件读写效率都不高,影响spark的执行效率。所以在后续的spark中(1.2.0之后的版本)的shuffle中,只会产生一个文件,并且数据会经过排序再附加索引信息,减少了文件的数量并通过排序索引的方式提升了性能。