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等操作,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,带有颜色的矩形表示分区
-
+
区分这两种依赖是非常有用的:
-+ 首先,窄依赖允许在一个集群节点上以流水线的方式(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处理完成后,才能开始接下来的计算,因此遇到宽依赖就需要重新划分阶段。
-
+
@@ -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