Spark的一些基本概念

目录

RDD介绍

RDD入门示例

查看RDD

RDD操作

DAG介绍

RDD的依赖关系

窄依赖

宽依赖

Shuffle概述


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使用,如下图所示。

uploading.4e448015.gif转存失败重新上传取消uploading.4e448015.gif转存失败重新上传取消

 

2)宽依赖指的是多个子RDD的Partition会依赖同一个parent RDD的Partition。

我们可以从不同类型的转换来进一步理解RDD的窄依赖和宽依赖的区别,如下图所示。

uploading.4e448015.gif转存失败重新上传取消

窄依赖

对于窄依赖,它们只是将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中,只会产生一个文件,并且数据会经过排序再附加索引信息,减少了文件的数量并通过排序索引的方式提升了性能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值