Spark Core 核心知识--RDD

本文深入探讨了Spark中的核心概念RDD(弹性分布式数据集),解析其特性、创建方式及编程API,对比窄依赖与宽依赖,阐述DAG生成与RDD缓存机制,为高效数据处理提供全面指导。

RDD

1、RDD 概述

1.1、什么是 RDD 

RDD(Resilient Distributed Dataset)叫做分布式数据集,是 Spark 中最基本的数据抽象,它 代表一个不可变、可分区、里面的元素可并行计算的集合。RDD 具有数据流模型的特点: 自动容错、位置感知性调度和可伸缩性。RDD 允许用户在执行多个查询时显式地将工作集缓 存在内存中,后续的查询能够重用工作集,这极大地提升了查询速度。 
 
可以从三个方面来理解: 
 
1、数据集 DataSet:故名思议,RDD 是数据集合的抽象,是复杂物理介质上存在数据的一 种逻辑视图。从外部来看,RDD 的确可以被看待成经过封装,带扩展特性(如容错性)的数 据集合。 
 
2、分布式 Distributed:RDD 的数据可能在物理上存储在多个节点的磁盘或内存中,也就是 所谓的多级存储。 
 
3、弹性 Resilient:虽然 RDD 内部存储的数据是只读的,但是,我们可以去修改(例如通 过 repartition 转换操作)并行计算计算单元的划分结构,也就是分区的数量。 
 
你将 RDD 理解为一个大的集合,将所有数据都加载到内存中,方便进行多次重用。第一, 它是分布式的,可以分布在多台机器上,进行计算。第二,它是弹性的,我认为它的弹性体 现在每个 RDD 都可以保存内存中,如果某个阶段的 RDD 丢失,不需要从头计算,只需要提 取上一个 RDD,再做相应的计算就可以了 

1.2、RDD 的属性 

1、A list of partitions:一组分片(Partition),即数据集的基本组成单位 

1、一个分区通常与一个计算任务关联,分区的个数决定了并行的粒度;

2、分区的个数可以在创建 RDD 的时候进行设置。如果没有设置,默认情况下由节点的 cores 个数决定;

3、每个 Partition 最终会被逻辑映射为 BlockManager 中的一个 Block,而这个 Block 会被下一 个 Task(ShuffleMapTask/ResultTask)使用进行计算  

2、A function for computing each split:一个计算每个分区的函数,也就是算子 

分区处理函数-compute

1、每个 RDD 都会实现 compute,用于对分区进行计算;

2、compute 函数会对迭代器进行复合,不需要保存每次计算结果;

3、该方法负责接收 parent RDDs 或者 data block 流入的 records 并进行计算,然后输出加工 后的 records。 

3、A list of dependencies on other RDDs:RDD 之间的依赖关系:宽依赖和窄依赖 

RDD 的每次转换都会生成一个新的 RDD,所以 RDD 之间就会形成类似于流水线一样的前后 依赖关系。在部分分区数据丢失时,Spark 可以通过这个依赖关系重新计算丢失的分区数据, 而不是对 RDD 的所有分区进行重新计算。 

RDDx 依赖的 parent RDD 的个数由不同的转换操作决定,例如二元转换操作 x = a.join(b),RDD x 就会同时依赖于 RDD a 和 RDD b。而具体的依赖关系可以细分为完全依赖和部分依赖,详 细说明如下: 

1、完全依赖:一个子 RDD 中的分区可以依赖于父 RDD 分区中一个或多个完整分区

例如,map 操作产生的子 RDD 分区与父 RDD 分区之间是一对一的关系;对于 cartesian 操作产生的子 RDD 分区与父 RDD 分区之间是多对多的关系。 

2、部分依赖:父 RDD 的一个 partition 中的部分数据与 RDD x 的一个 partition 相关,而另一 部分数据与 RDD x 中的另一个 partition 有关。 

例如,groupByKey 操作产生的 ShuffledRDD 中的每个分区依赖于父 RDD 的所有分区中的部分 元素。 

在 Spark 中,完全依赖是 NarrowDependency(黑色箭头),部分依赖是 ShuffleDependency ( 红 色 箭 头 ),而 NarrowDependency 又 可 以 细 分 为 [1:1]OneToOneDependency 、 [N:1]NarrowDependency 和[N:N]NarrowDependency,还有特殊的 RangeDependency (只在 UnionRDD 中使用)。

需要注意的是,对于[N:N]NarrowDependency很少见,最后生成的依赖图和ShuffleDependency 没什么两样。只是对于父 RDD 来说,有一部分是完全依赖,有一部分是部分依赖。所以也 只有[1:1]OneToOneDependency 和[N:1]NarrowDependency 两种情况。 

4、Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned): 一个 Partitioner,即 RDD 的分片函数

当前 Spark 中实现了两种类型的分片函数,一个是基于哈希的 HashPartitioner,另外一个是 基于范围的 RangePartitioner。只有对于于 key-value 的 RDD,才会有 Partitioner,非 key-value的 RDD 的 Parititioner 的值是 None。Partitioner 函数不但决定了 RDD 本身的分片数量,也决 定了 parent RDD Shuffle 输出时的分片数量。 

1、只有键值对 RDD,才会有 Partitioner。其他非键值对的 RDD 的 Partitioner 为 None;

2、它定义了键值对 RDD 中的元素如何被键分区,能够将每个键映射到对应的分区 ID,从 0 到”numPartitions- 1”上;

3、Partitioner 不但决定了 RDD 本身的分区个数,也决定了 parent RDD shuffle 输出的分区个 数。

4、在分区器的选择上,默认情况下,如果有一组 RDDs(父 RDD)已经有了 Partitioner,则 从中选择一个分区数较大的 Partitioner;否则,使用默认的 HashPartitioner。

5、对于 HashPartitioner 分区数的设置,如果配置了 spark.default.parallelism 属性,则将分区 数设置为此值,否则,将分区数设置为上游 RDDs 中最大分区数。  

5、Optionally, a list of preferred locations to compute each split on (e.g. block locations for an HDFS file):一个列表,存储存取每个 Partition 的优先位置(preferred location)。 

1、对于一个 HDFS 文件来说,这个列表保存的就是每个 Partition 所在的块的位置。

2、按照”移动数据不如移动计算”的理念,Spark 在进行任务调度的时候,会尽可能地将计算 任务分配到其所要处理数据块的存储位置。

3、每个子 RDDgetPreferredLocations 的实现中,都会优先选择父 RDD 中对应分区的 preferedLocation,其次才选择自己设置的优先位置。 

2、创建 RDD 

创建 RDD 主要有两种方式:官网解释 

There are two ways to create RDDs: parallelizing an existing collection in your driver program, or referencing a dataset in an external storage system, such as a shared filesystem, HDFS, HBase, or any data source offering a Hadoop InputFormat. 

1、由一个已经存在的 Scala 数据集合创建

val rdd = sc.parallelize(Array(1,2,3,4,5,6,7,8))

val rdd = sc.makeRDD(Array(1,2,3,4,5,6,7,8)) 

2、由外部存储系统的数据集创建,包括本地的文件系统,还有所有 Hadoop 支持的数据集, 比如 HDFS、Cassandra、HBase 等

val rdd = sc.textFile("hdfs://myha01/spark/wc/input/words.txt") 

3、扩展

从 HBase 当中读取

从 ElasticSearch 中读取

3、RDD 的编程 API 

官网: 

http://spark.apache.org/docs/latest/rdd-programming-guide.html#resilient-distributed-datasetsrdds 

3.1、Transformation 

官网:http://spark.apache.org/docs/latest/rdd-programming-guide.html#transformations 
 
RDD 中的所有转换(Transformation)都是延迟加载的,也就是说,它们并不会直接计算结 果。相反的,它们只是记住这些应用到基础数据集(例如一个文件)上的转换动作。只有当 发生一个要求返回结果给 Driver 的动作时,这些转换才会真正运行。这种设计让 Spark 更加 有效率地运行。 
 
常用的 Transformation: 

 

总结: Transformation 返回值还是一个 RDD。它使用了链式调用的设计模式,对一个 RDD 进行计 算后,变换成另外一个 RDD,然后这个 RDD 又可以进行另外一次转换。这个过程是分布式 的

3.2、Action 

官网:http://spark.apache.org/docs/latest/rdd-programming-guide.html#actions 

 

总结: Action 返回值不是一个 RDD。它要么是一个 Scala 的普通集合,要么是一个值,要么是空, 最终或返回到 Driver 程序,或把 RDD 写入到文件系统中 

3.3、WordCount 中的 RDD 

  那么问题来了,请问在下面这一句标准的 wordcount 中到底产生了几个 RDD 呢??? sc.textFile("hdfs://myha01/wc/input/words.txt").flatMap(_.split(" ")).map((_,1)).reduceByKey(_+_).collect 

3.4、RDD 练习

启动 spark-shell $SPARK_HOME/bin/spark-shell --master spark://hadoop02:7077 

练习 1: //通过并行化生成 rdd 
val rdd1 = sc.parallelize(List(5, 6, 4, 7, 3, 8, 2, 9, 1, 10)) 
//对 rdd1 里的每一个元素乘 2 然后排序 
val rdd2 = rdd1.map(_ * 2).sortBy(x => x, true) 
//过滤出大于等于十的元素 
val rdd3 = rdd2.filter(_ >= 10) 
//将元素以数组的方式在客户端显示 
rdd3.collect 
 
练习 2: 
val rdd1 = sc.parallelize(Array("a b c", "d e f", "h i j")) 
//将 rdd1 里面的每一个元素先切分在压平 
val rdd2 = rdd1.flatMap(_.split(' ')) 
rdd2.collect 

练习 3: 
val rdd1 = sc.parallelize(List(5, 6, 4, 3)) 
val rdd2 = sc.parallelize(List(1, 2, 3, 4)) 
//求并集 
val rdd3 = rdd1.union(rdd2) 
//求交集 
val rdd4 = rdd1.intersection(rdd2) 
//去重 
rdd3.distinct.collect 
rdd4.collect 

练习 4: 
val rdd1 = sc.parallelize(List(("tom", 1), ("jerry", 3), ("kitty", 2))) 
val rdd2 = sc.parallelize(List(("jerry", 2), ("tom", 1), ("shuke", 2))) 
//求 jion 
val rdd3 = rdd1.join(rdd2) rdd3.collect 
//求并集 
val rdd4 = rdd1 union rdd2 
//按 key 进行分组 
rdd4.groupByKey 
rdd4.collect 

练习 5: 
val rdd1 = sc.parallelize(List(("tom", 1), ("tom", 2), ("jerry", 3), ("kitty", 2))) 
val rdd2 = sc.parallelize(List(("jerry", 2), ("tom", 1), ("shuke", 2))) 
//cogroup 
val rdd3 = rdd1.cogroup(rdd2) 
//注意 cogroup 与 groupByKey 的区别 
rdd3.collect 

练习 6: 
val rdd1 = sc.parallelize(List(1, 2, 3, 4, 5)) 
//reduce 聚合 
val rdd2 = rdd1.reduce(_ + _) 
rdd2.collect

练习 7: 
val rdd1 = sc.parallelize(List(("tom", 1), ("jerry", 3), ("kitty", 2),  ("shuke", 1))) 
val rdd2 = sc.parallelize(List(("jerry", 2), ("tom", 3), ("shuke", 2), ("kitty", 5))) 
val rdd3 = rdd1.union(rdd2) 
//按 key 进行聚合 
val rdd4 = rdd3.reduceByKey(_ + _) rdd4.collect 
//按 value 的降序排序 
val rdd5 = rdd4.map(t => (t._2, t._1)).sortByKey(false).map(t => (t._2, t._1)) 
rdd5.collect 

 //想要了解更多,访问下面的地址 http://homepage.cs.latrobe.edu.au/zhe/ZhenHeSparkRDDAPIExamples.html 

4、RDD 的依赖关系 

RDD 和它依赖的父 RDD(s)的关系有两种不同的类型,即窄依赖(narrow dependency)和 宽依赖(wide dependency) 

4.1、窄依赖和宽依赖对比 

窄依赖指的是每一个父 RDD 的 Partition 最多被子 RDD 的一个 Partition 使用

总结:窄依赖我们形象的比喻为独生子女,窄依赖的函数有:map, filter, union, join(父 RDD 是 hash-partitioned ), mapPartitions, mapValues 
 
宽依赖指的是多个子 RDD 的 Partition 会依赖同一个父 RDD 的 Partition

总结:窄依赖我们形象的比喻为超生,宽依赖的函数有:groupByKey、partitionBy、reduceByKey、 sortByKey、join(父 RDD 不是 hash-partitioned )  

4.2、窄依赖和宽依赖总结 

在这里我们是从父 RDD 的 partition 被使用的个数来定义窄依赖和宽依赖,因此可以用一句 话概括下:如果父 RDD 的一个 Partition 被子 RDD 的一个 Partition 所使用就是窄依赖,否则 的话就是宽依赖。因为是确定的 partition 数量的依赖关系,所以 RDD 之间的依赖关系就是 窄依赖;由此我们可以得出一个推论:即窄依赖不仅包含一对一的窄依赖,还包含一对固定 个数的窄依赖。 
 
一对固定个数的窄依赖的理解:即子 RDD 的 partition 对父 RDD 依赖的 Partition 的数量不会 随着 RDD 数据规模的改变而改变;换句话说,无论是有 100T 的数据量还是 1P 的数据量, 在窄依赖中,子 RDD 所依赖的父 RDD 的 partition 的个数是确定的,而宽依赖是 shuffle 级别 的,数据量越大,那么子 RDD 所依赖的父 RDD 的个数就越多,从而子 RDD 所依赖的父 RDD 的 partition 的个数也会变得越来越多。 

4.3、Lineage 

RDD 只支持粗粒度转换,即在大量记录上执行的单个操作。将创建 RDD 的一系列 Lineage(即 血统)记录下来,以便恢复丢失的分区。RDD 的 Lineage 会记录 RDD 的元数据信息和转换行 为,当该 RDD 的部分分区数据丢失时,它可以根据这些信息来重新运算和恢复丢失的数据 分区。

5、DAG 生成 

DAG(Directed Acyclic Graph)叫做有向无环图,原始的RDD通过一系列的转换就就形成了DAG, 根据 RDD 之间的依赖关系的不同将 DAG 划分成不同的 Stage,对于窄依赖,partition 的转换 处理在 Stage 中完成计算。对于宽依赖,由于有 Shuffle 的存在,只能在 parent RDD 处理完 成后,才能开始接下来的计算,因此宽依赖是划分 Stage 的依据。 

 在 spark 中,会根据 RDD 之间的依赖关系将 DAG 图(有向无环图)划分为不同的阶段,对 于窄依赖,由于 partition 依赖关系的确定性,partition 的转换处理就可以在同一个线程里完 成,窄依赖就被 spark 划分到同一个 stage 中,而对于宽依赖,只能等父 RDD shuffle 处理完 成后,下一个 stage 才能开始接下来的计算。 
 
因此 spark 划分 stage 的整体思路是:从后往前推,遇到宽依赖就断开,划分为一个 stage; 遇到窄依赖就将这个 RDD 加入该 stage 中。因此在上图中 RDD C,RDD D,RDD E,RDD F 被 构建在一个 stage 中,RDD A 被构建在一个单独的 Stage 中,而 RDD B 和 RDD G 又被构建在 同一个 stage 中。 
 
在 spark 中,Task 的类型分为 2 种:ShuffleMapTask 和 ResultTask 

简单来说,DAG 的最后一个阶段会为每个结果的 partition 生成一个 ResultTask,即每个 Stage 里面的 Task 的数量是由该 Stage 中最后一个 RDD 的 Partition 的数量所决定的!而其余所有 阶段都会生成 ShuffleMapTask;之所以称之为 ShuffleMapTask 是因为它需要将自己的计算结 果通过 shuffle 到下一个 stage 中;也就是说上图中的 stage1 和 stage2 相当于 MapReduce 中 的 Mapper,而 ResultTask 所代表的 stage3 就相当于 MapReduce 中的 reducer。 
 
在之前动手操作了一个 WordCount 程序,因此可知,Hadoop 中 MapReduce 操作中的 Mapper 和 Reducer 在 spark 中的基本等量算子是 map 和 reduceByKey;不过区别在于:Hadoop 中的 MapReduce 天生就是排序的;而 reduceByKey 只是根据 Key 进行 reduce,但 spark 除了这两 个算子还有其他的算子;因此从这个意义上来说,Spark 比 Hadoop 的计算算子更为丰富。 

6、RDD 缓存 

Spark 速度非常快的原因之一,就是在不同操作中可以在内存中持久化或缓存个数据集。当 持久化某个 RDD 后,每一个节点都将把计算的分片结果保存在内存中,并在对此 RDD 或衍 生出的 RDD 进行的其他动作中重用。这使得后续的动作变得更加迅速。RDD 相关的持久化 和缓存,是 Spark 最重要的特征之一。可以说,缓存是 Spark 构建迭代式算法和快速交互式 查询的关键。 

6.1、RDD 的缓存方式

RDD 通过 persist 方法或 cache 方法可以将前面的计算结果缓存,但是并不是这两个方法被 调用时立即缓存,而是触发后面的 action 时,该 RDD 将会被缓存在计算节点的内存中,并 供后面重用。 

通过查看源码发现 cache 最终也是调用了 persist 方法,默认的存储级别都是仅在内存存储一 份,Spark 的存储级别还有好多种,存储级别在 object StorageLevel 中定义的。 

 

缓存有可能丢失,或者存储存储于内存的数据由于内存不足而被删除,RDD 的缓存容错机制 保证了即使缓存丢失也能保证计算的正确执行。通过基于 RDD 的一系列转换,丢失的数据 会被重算,由于 RDD 的各个 Partition 是相对独立的,因此只需要计算丢失的部分即可,并 不需要重算全部 Partition。  

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值