From d755797b439968cdc90dcc887e52d20e4da748b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E7=A5=A5?= <1366971433@qq.com> Date: Thu, 16 May 2019 15:12:46 +0800 Subject: [PATCH] =?UTF-8?q?RDD=E8=AF=A6=E8=A7=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- notes/RDD详解.md | 0 notes/Spark-RDD.md | 190 ++++++++++++++++++++++++++++++----- pictures/scala-分区数.png | Bin 0 -> 15902 bytes 3 files changed, 164 insertions(+), 26 deletions(-) delete mode 100644 notes/RDD详解.md create mode 100644 pictures/scala-分区数.png diff --git a/notes/RDD详解.md b/notes/RDD详解.md deleted file mode 100644 index e69de29..0000000 diff --git a/notes/Spark-RDD.md b/notes/Spark-RDD.md index 260ae52..30d9676 100644 --- a/notes/Spark-RDD.md +++ b/notes/Spark-RDD.md @@ -1,37 +1,175 @@ -## 弹性式数据集RDDs +# 弹性式数据集RDDs + + ## 一、RDD简介 -RDD,全称为 Resilient Distributed Datasets,是Spark最基本的数据抽象,它是只读的、分区记录的集合,支持并行操作。RDD可以由物理存储中的数据集创建或从其他RDD转换而来。RDD具备高度的容错性,允许开发人员在大型集群上执行基于内存的并行计算。它具有以下特性: +RDD,全称为 Resilient Distributed Datasets,是Spark最基本的数据抽象,它是只读的、分区记录的集合,支持并行操作。RDD可以由外部数据集或其他RDD转换而来。其具备高度的容错性,允许开发人员在大型集群上执行基于内存的并行计算。它具有以下特性: + ++ 一个RDD由一个或者多个分区(Partitions)组成。对于RDD来说,每个分区会被一个计算任务所处理,用户可以在创建RDD时指定其分区个数,如果没有指定,则采用程序所分配到的CPU的核心数; ++ RDD拥有一个用于计算分区的函数compute; ++ RDD会保存彼此间的依赖关系,RDD的每次转换都会生成一个新的依赖关系,这种RDD之间的依赖关系就像流水线一样。在部分分区数据丢失后,可以通过这种依赖关系重新计算丢失的分区数据,而不是对RDD的所有分区进行重新计算; ++ Key-Value型的RDD还拥有Partitioner(分区器),用于决定数据被存储在哪个分区中,目前Spark中支持HashPartitioner(按照哈希分区)和RangeParationer(按照范围进行分区); ++ 一个优先位置列表(可选),用于存储每个分区的优先位置(prefered location)。对于一个HDFS文件来说,这个列表保存的就是每个分区所在的块的位置,按照“移动数据不如移动计算“的理念,Spark在进行任务调度的时候,会尽可能的将计算任务分配到其所要处理数据块的存储位置。 + +RDD[T]抽象类的部分相关代码如下: + +```scala +// 由子类实现以计算给定分区 +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 + +// 分区器 由子类重写以指定它们的分区方式 +@transient val partitioner: Option[Partitioner] = None +``` + -+ 一个RDD由一个或者多个分区(Partitions)组成,对于RDD来说,每个分区会被一个计算任务所处理,用户可以在创建RDD是指定其分区个数,如果没有指定,则采用程序所分配到的CPU的核心数; -+ 一个用于计算所有分区的函数compute; -+ RDD之间的依赖关系,RDD的每次转换都会生成一个新的依赖关系,这种RDD之间的依赖关系就像流水线一样。在部分分区数据丢失后,可以通过这种依赖关系重新计算丢失的分区数据,不是对RDD的所有分区进行重新计算; -+ 对于Key-Value型的RDD,还有Partitioner,即分区函数。目前Spark中支持HashPartitioner(按照哈希分区)和RangeParationer(按照范围进行分区); -+ 一个列表,存储每个Partition的优先位置(prefered location)。对于一个HDFS文件来说,这个列表保存的就是每个分区所在的块的位置,按照“移动数据不如移动计算“的理念,Spark在进行任务调度的时候,会尽可能的将计算任务分配到其所要处理数据块的存储位置。 ## 二、创建RDD -RDD是一个的集合。创建RDD有两种方法: +RDD有两种创建方式,分别介绍如下: + +### 2.1 由现有集合创建 + +这里使用`spark-shell`的本地模式作为测试,指定使用4个CPU 核心,启动命令如下: + +```shell +spark-shell --master local[4] +``` + +启动`spark-shell`后,程序会自动创建应用上下文,相当于程序自动执行了下面的语句: + +```scala +val conf = new SparkConf().setAppName("Spark shell").setMaster("local[4]") +val sc = new SparkContext(conf) +``` + +由现有集合创建RDD,你可以在创建时指定其分区个数,如果没有指定,则采用程序所分配到的CPU的核心数: + +```scala +val data = Array(1, 2, 3, 4, 5) +// 由现有集合创建RDD,默认分区数为程序所分配到的CPU的核心数 +val dataRDD = sc.parallelize(data) +// 查看分区数 +dataRDD.getNumPartitions +// 明确指定分区数 +val dataRDD = sc.parallelize(data,2) +``` + +执行结果如下: + +
+ +### 2.2 引用外部存储系统中的数据集 + +引用外部存储系统中的数据集,例如共享文件系统,HDFS,HBase或支持Hadoop InputFormat的任何数据源。 + +```scala +val fileRDD = sc.textFile("/usr/file/emp.txt") +// 获取第一行文本 +fileRDD.take(1) +``` + +使用外部存储系统有以下三点需要注意: + ++ 支持本地文件系统,也支持HDFS,s3a等文件系统; + ++ 如果Spark是以集群的方式运行,且需要从本地文件系统读取数据,则该文件必须在所有节点机器上都存在,且路径相同; + ++ 文件格式支持目录,压缩文件,文件路径支持通配符。 + +### 2.3 textFile & wholeTextFiles + +两者都可以用来读取外部文件,但是返回格式是不同的: + ++ textFile:其返回格式是RDD[String] ,返回的是就是文件内容,RDD中每一个元素对应一行数据; ++ wholeTextFiles:其返回格式是RDD[(String, String)],元组中第一个参数是文件路径,第二个参数是文件内容; ++ 两者都提供第二个参数来控制最小分区数; ++ 默认情况下,Spark为文件的每个块创建一个分区(HDFS中默认为128MB)。 + +```scala +def textFile(path: String,minPartitions: Int = defaultMinPartitions): RDD[String] = withScope {...} +def wholeTextFiles(path: String,minPartitions: Int = defaultMinPartitions): RDD[(String, String)]={..} +``` + -+ 由现有集合创建; -+ 引用外部存储系统中的数据集,例如共享文件系统,HDFS,HBase或支持Hadoop InputFormat的任何数据源。 ## 三、操作RDD RDD支持两种类型的操作:*transformations*(转换,从现有数据集创建新数据集)和*actions*(在数据集上运行计算后将值返回到驱动程序)。RDD中的所有转换操作都是惰性的,它们只是记住这些转换操作,但不会立即执行,只有遇到action操作后才会真正的进行计算,这类似于函数式编程中的惰性求值。 +```scala +val list = List(1, 2, 3) +// map 是一个transformations操作,而foreach是一个actions操作 +sc.parallelize(list).map(_ * 10).foreach(println) +// 输出: 10 20 30 +``` + ## 四、缓存RDD -Spark速度非常快的一个原因是其支持将RDD缓存到内存中。当缓存一个RDD到内存中后,如果之后的操作使用到了该数据集,则使用内存中缓存的数据。 +### 4.1 缓存级别 -缓存有丢失的风险,但是由于RDD之间的依赖关系,如果RDD上某个分区的数据丢失,只需要重新计算该分区即可,这是Spark高容错性的基础。 +Spark速度非常快的一个原因是RDD支持缓存。当缓存一个RDD到内存中后,如果之后的操作使用到了该数据集,则从缓存获取。虽然缓存也有丢失的风险,但是由于RDD之间的依赖关系,如果某个分区的缓存数据丢失,只需要重新计算该分区即可。 +Spark支持多种缓存级别,见下表: +| Storage Level(存储级别) | Meaning(含义) | +| ----------------------------------------------- | ------------------------------------------------------------ | +| MEMORY_ONLY | 默认的缓存级别,将 RDD以反序列化的Java对象的形式存储在 JVM 中。如果内存空间不够,则部分分区数据将不再缓存。 | +| MEMORY_AND_DISK | 将 RDD 以反序列化的Java对象的形式存储JVM中。如果内存空间不够,将未缓存的分区数据存储到磁盘,在需要使用这些分区时从磁盘读取。 | +| MEMORY_ONLY_SER
(仅支持 Java and Scala) | 将 RDD 以序列化的Java对象的形式进行存储(每个分区为一个 byte 数组)。这种方式比反序列化对象节省存储空间,但在读取时会增加CPU的计算负担。 | +| MEMORY_AND_DISK_SER
(仅支持 Java and Scala) | 类似于MEMORY_ONLY_SER,但是溢出的分区数据会存储到磁盘,而不是在用到它们时重新计算。 | +| DISK_ONLY | 只在磁盘上缓存RDD | +| MEMORY_ONLY_2,
MEMORY_AND_DISK_2, etc | 与上面的对应级别功能相同,但是会为每个分区在集群中两个节点上建立副本。 | +| OFF_HEAP | 与MEMORY_ONLY_SER类似,但将数据存储在堆外内存中。这需要启用堆外内存。 | + +> 启动堆外内存需要配置两个参数: +> +> + spark.memory.offHeap.enabled :是否开启堆外内存,默认值为false,需要设置为true; +> + spark.memory.offHeap.size : 堆外内存空间的大小,默认值为0,需要设置为正值。 + +### 4.2 使用缓存 + +RDD上有两个可选的方法用于缓存数据:`persist`和`cache` ,cache内部调用的也是persist,其等价于`persist(StorageLevel.MEMORY_ONLY)`。 + +```scala +// 所有存储级别均定义在StorageLevel对象中 +fileRDD.persist(StorageLevel.MEMORY_AND_DISK) +fileRDD.cache() +``` + +### 4.3 移除缓存 + +Spark会自动监视每个节点上的缓存使用情况,并按照最近最少使用(LRU)的规则删除旧数据分区。当然,你也可以使用`RDD.unpersist()`方法进行手动删除。 @@ -39,21 +177,19 @@ Spark速度非常快的一个原因是其支持将RDD缓存到内存中。当缓 ### 5.1 shuffle介绍 -通常在Spark中,一个任务遇到对应一个分区,但是如果reduceByKey等操作,Spark必须从所有分区读取以查找所有键的所有值,然后将分区中的值汇总在一起以计算每个键的最终结果 ,这称为shuffle。 - -![spark-reducebykey](D:\BigData-Notes\pictures\spark-reducebykey.png) - +Spark中,一个任务对应一个分区,通常不会跨分区操作数据。但如果遇到reduceByKey等操作,Spark必须从所有分区读取数据,并查找所有键的所有值,然后汇总在一起以计算每个键的最终结果 ,这称为shuffle。 +
### 5.2 Shuffle的影响 -Shuffle是一项昂贵的操作,因为它涉及磁盘I/O,网络I/O,和数据序列化。某些shuffle操作还会消耗大量的堆内存,因为它们使用内存中的数据结构来组织传输数据。Shuffle还会在磁盘上生成大量中间文件,从Spark 1.3开始,这些文件将被保留,直到相应的RDD不再使用并进行垃圾回收。这样做是为了避免在计算时重复创建shuffle文件。如果应用程序保留对这些RDD的引用则垃圾回收可能在很长一段时间后才会发生,这意味着长时间运行的Spark作业可能会占用大量磁盘空间。可以使用`spark.local.dir`参数指定临时存储目录。 +Shuffle是一项昂贵的操作,因为它通常会跨节点操作数据,这必然会涉及磁盘I/O,网络I/O,和数据序列化。某些shuffle操作还会消耗大量的堆内存,因为它们使用内存中的数据结构来组织数据并传输。Shuffle还会在磁盘上生成大量中间文件,从Spark 1.3开始,这些文件将被保留,直到相应的RDD不再使用并进行垃圾回收,这样做是为了避免在计算时重复创建shuffle文件。如果应用程序长期保留对这些RDD的引用,则垃圾回收可能在很长一段时间后才会发生,这意味着长时间运行的Spark作业可能会占用大量磁盘空间,通常可以使用`spark.local.dir`参数指定这些文件临时存储目录。 ### 5.3 导致Shuffle的操作 -以下操作都会导致Shuffle: +由于Shuffle操作对性能的影响比较大,所以需要特别注意使用,以下操作都会导致Shuffle: + 涉及到重新分区操作: 如`repartition` 和 `coalesce`; + 所有涉及到ByKey的操作(counting除外):如`groupByKey`和`reduceByKey`; @@ -70,27 +206,27 @@ RDD和它的父RDD(s)之间的依赖关系分为两种不同的类型: 如下图:每一个方框表示一个 RDD,带有颜色的矩形表示分区 -![spark-窄依赖和宽依赖](D:\BigData-Notes\pictures\spark-窄依赖和宽依赖.png) +
区分这两种依赖是非常有用的: -+ 首先,窄依赖允许在一个集群节点上以流水线的方式(pipeline)计算所有父分区的数据。例如,逐个元素地执行map、然后filter操作;而宽依赖则需要首先计算好所有父分区数据,然后在节点之间进行Shuffle,这与MapReduce类似。 -+ 窄依赖能够更有效地进行失效节点的恢复,即只需重新计算丢失RDD分区的父分区,且不同节点之间可以并行计算;而对于一个宽依赖关系的Lineage图,子RDD部分分区数据的丢失都需要对父RDD的所有分区数据进行再次计算。 ++ 首先,窄依赖允许在一个集群节点上以流水线的方式(pipeline)对父分区数据进行计算。例如,先执行map操作、然后执行filter操作;而宽依赖则需要首先计算好所有父分区的数据,然后在节点之间进行Shuffle,这与MapReduce类似。 ++ 窄依赖能够更有效地进行数据恢复,因为只需重新对丢失分区的父分区进行计算,且不同节点之间可以并行计算;而对于宽依赖而言,如果数据丢失,则需要对所有父分区数据进行计算并Shuffle。 ## 六、DAG的生成 -RDD(s)及其之间的依赖关系组成了DAG(有向无环图),DAG定义了这些RDD(s)之间的Lineage(血统)关系,通过这些关系,如果一个RDD的部分或者全部计算结果丢失了,也可以重新进行计算。 +RDD(s)及其之间的依赖关系组成了DAG(有向无环图),DAG定义了这些RDD(s)之间的Lineage(血统)关系,通过血统关系,如果一个RDD的部分或者全部计算结果丢失了,也可以重新进行计算。 -那么Spark是如何根据DAG来生成计算任务呢?首先,根据依赖关系的不同将DAG划分为不同的阶段(Stage)。 +那么Spark是如何根据DAG来生成计算任务呢?主要是根据依赖关系的不同将DAG划分为不同的计算阶段(Stage): + 对于窄依赖,由于分区的依赖关系是确定的,其转换操作可以在同一个线程执行,所以可以划分到同一个执行阶段; -+ 对于宽依赖,由于Shuffle的存在,只能在父RDD(s)Shuffle处理完成后,才能开始接下来的计算,因此遇到宽依赖就需要重新划分开始新的阶段。 ++ 对于宽依赖,由于Shuffle的存在,只能在父RDD(s)被Shuffle处理完成后,才能开始接下来的计算,因此遇到宽依赖就需要重新划分阶段。 -![spark-DAG](D:\BigData-Notes\pictures\spark-DAG.png) +
@@ -98,7 +234,9 @@ RDD(s)及其之间的依赖关系组成了DAG(有向无环图),DAG定义了这 ## 参考资料 -1. [RDD:基于内存的集群计算容错抽象](http://shiyanjun.cn/archives/744.html) +1. 张安站 . Spark技术内幕:深入解析Spark内核架构设计与实现原理[M] . 机械工业出版社 . 2015-09-01 +2. [RDD Programming Guide](https://spark.apache.org/docs/latest/rdd-programming-guide.html#rdd-programming-guide) +3. [RDD:基于内存的集群计算容错抽象](http://shiyanjun.cn/archives/744.html) diff --git a/pictures/scala-分区数.png b/pictures/scala-分区数.png new file mode 100644 index 0000000000000000000000000000000000000000..91fd6a23de78360950fbc8058a858c0bd5f96572 GIT binary patch literal 15902 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 IIu6hNFWs+tUH||9 literal 0 HcmV?d00001