一、简介
RDD(Resilient Distributed Datasets) ,弹性分布式数据集, 是分布式内存的一个抽象概念。
RDD是只读的记录分区的集合,只能通过在其他RDD执行确定的转换操作(如map、join和group by)而创建。
RDD可以看作是Spark的一个对象,运行于内存中,例如读文件是一个RDD,对文件计算是一个RDD,结果集也是一个RDD ,不同的分片、 数据之间的依赖 、key-value类型的map数据都可以看做RDD。
作用:提供了一种高度受限共享内存模型
二 、特点
RDD 代表者一个不可变、带有分区的集合,可以被并行操作。
1 A list of partitions(分区列表)
2 A function for computing each split(每个分区都有一个计算函数。每个分区都是独立运行function操作的,继而实现并行)
3 A list of dependencies on other RDDs(依赖于其他RDD的列表。因为RDD是不可变的,因此RDD存在一种转换依赖关系,将这种转换依赖关系称为RDD的血统-lineage,可以实现RDD的故障恢复)
4 Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned)(可以对Key-Value类型的数据,指定Partitioner策略,默认系统使用Hash-Partitioned)
5 Optionally, a list of preferred locations to compute each split on (e.g. block locations for an HDFS file)(每一个分区都有一个优先位置的列表。Spark在计算HDFS的时候,可以考虑最优计算策略。)
2.1分区列表:
- 为啥要分区:
做这个分区之后,每个分区里面的数据 都会被一个计算任务(Task)执行处理。有几个分区到时候并行执行计算的数量就有多少个。RDD的并行度默认就是:上一个RDD有几个他就有几个。总的来说就是:RDD分区(分片)数决定了并行计算的力度 - 如何分区:
默认情况下,一个HDFS上的数据分片就是一个分区( partiton),也可以在创建RDD时指定RDD分片个数(分区)。
如果不指定,默认分区数量为:这个程序在执行的时候分配到的电脑的CPU个数。每个Core可以承载2~4个 partition。
如果是第一个RDD是从HDFS文件上面 读取的,则为 文件的block个数
2.2每个分区都有一个计算函数
- 上面说到每个分区里面的数据都会被一个计算任务执行,那执行的这个东西就得知道怎么 执行这个 数据,那怎么执行就是依靠计算函数执行的。这个叫做 compute函数,也叫算子。那每个RDD都会在每个分片上执行相应的算子,由于分片是并行计算的,所以叫做分布式并行计算。
2.3只读
- RDD是只读的,要想改变RDD中的数据,只能在现有的RDD基础上创建新的RDD。
- 由一个RDD转换到另一个RDD,可以通过丰富的操作算子实现,不再像MapReduce那样只能写map和reduce了
- RDD的操作算子包括两类,一类叫做transformations(转换算子),它是用来将RDD进行转化,构建RDD的血缘关系;另一类叫做actions(行动算子),它是用来触发RDD的计算,得到RDD的相关计算结果或者将RDD保存的文件系统中
2.4依赖
- RDD之间的依赖有两种:窄依赖( Narrow Dependency)和宽依赖( Wide Dependency)。
- 窄依赖指的是每一个 parent RDD的 Partition最多被 child rdd的一个 Partition所使用。
- 宽依赖指的是多个 child RDD的Partion会依赖于同一个 parent RDD的 Partition。
- 宽依赖典型的操作有 group by key, sort by key等
- 宽依赖意味着Shuffle操作,这是 Spark划分Stage的边界的依据。
- Spark中的宽依赖支持两种 Shuffle Manager,即 Hash Shufflemanager和 Sort shufflemanager,前者是基于Hash的 Shuffle机制,后者是基于排序的 Shuffle机制。
2.5缓存
- 如果一个RDD在程序中会被多次使用,就应该将这个RDD放在缓存中,只有在第一次计算 的时候会得到分区里面的数据,后续再使用的时候就直接从缓存中拿出,不会在根据依赖从新计算,这样整体的计算速度就会变快 。
2.5CheckPoint
- 一个RDD的数据在分区中可能丢失,虽然说丢失以后还可以使用血缘关系从建,但是随着血缘关系的边长,如果丢失后再通过血缘关系从建对的时间就会变得很慢影响整个程序的性能。为了解决这个问题RDD支持ChickPoint将数据持久化到硬盘上,这样就不用再使用血缘关系从头在计算出这个RDD了,直接重储存中拿取就可以。
三、RDD中名词解释
1、RDD编程中的转化算子(transformation 转换)
map:lines.map(word => (word,1))
filter: lines.filter(word => word.cotains(“hello”))
flatmap 扁平化映射:一个输入对应多个输出
val inputs=sc.textFile("/user/test.txt")
val lines=inputs.flatMap(line=>line.split(" "))
集合运算
rdd1.distinct() 去重
rdd1.union(rdd2) 合并
rdd1.intersection(rdd2) 交集
rdd1.subtract(rdd2) rdd1 有 rdd2没有
2、RDD编程中的行动算子(action)
在RDD上计算出一个结果 把结果返回给driver program:
reduce()
接收一个函数作用在RDD两个类型相同的元素上返回一个新元素。可以实现RDD中元素的累加、计数 和其他类型的聚集操作
val rdd = sc.parallelize(Array(1,2,3,3))
rdd.reduce((x,y)=>x+y)
collect()
遍历整个RDD,向driver program返回RDD内容。
在数据量比较小做测试的时候可以使用,但是数量大的时候就得使用输出到HDFS上保存查看。(saveAsTextFile() action)
take(n)
返回RDD的前n个元素
top(): 排序
foreach():
遍历RDD中的每个元素,可以配合println()打印出数据
lines.foreach(println)
3、延迟计算(lazy Evaluation)
- Transformation变换/转换算子:这种变换是延迟计算的,也就是说从一个RDD转换生成另一个RDD的转换操作不是马上执行,需要等到有Action操作的时候才会真正触发运算。
- Action行动算子:这类算子会触发Spark提交作业(Job),并将数据输出。
4、RDD的依赖关系
RDD依赖关系也称为RDD的血统,描述了RDD间的转换关系 。Spark将RDD间依赖关系分为了宽依赖|ShuffleDependency
、窄依赖|NarrowDependency
,Spark在提交任务的时候会根据转换算子逆向推导出所有的Stage。然后计算推导的stage的分区用于表示该Stage执行的并行度
。
说白了就是看转换算子在计算的时候做不做shuffer,如果做了就需要重新分区,就将一个分区中的数据分到不同的分区中了这就叫做宽依赖,其他的没有做shuffer的下一个rdd的分区就和上一个rdd的分区一模一样,那就是窄依赖
窄依赖:
- 父RDD和子RDD partition之间的关系是一对一的。或者父RDD一个partition只对应一个子RDD的partition情况下的父RDD和子RDD partition关系是多对一的。不会有shuffle的产生。父RDD的一个分区去到子RDD的一个分区。
宽依赖:
- 父RDD与子RDD partition之间的关系是一对多。会有shuffle的产生。父RDD的一个分区的数据去到子RDD的不同分区里面。
5.有向无环图(DAG)
AG(Directed Acyclic Graph)叫做有向无环图,原始的RDD通过一系列的转换就就形成了DAG,根据RDD之间的依赖关系的不同将DAG划分成不同的Stage,对于窄依赖,partition的转换处理在Stage中完成计算。对于宽依赖,由于有Shuffle的存在,只能在parent RDD处理完成后,才能开始接下来的计算,因此宽依赖是划分stage的依据。
说白了就是如果是窄依赖就不会产生新的阶段(Stage),如果涉及到宽依赖,就会Shuffer,就会产生一个新的阶段(Stage)
6.任务的划分
一个RDD任务会切分分为:Application、Job、Stage和Task
-
Application:初始化一个SparkContext即生成一个Application
-
Job:一个Action算子就会生成一个Job
-
Stage:根据RDD之间的依赖关系的不同将Job划分成不同的Stage,遇到一个宽依赖则划分一个Stage。
-
Task:Stage是一个TaskSet,将Stage划分的结果发送到不同的Executor执行即为一个Task。
7.RDD的缓存
RDD通过persist方法或cache方法可以将前面的计算结果缓存,默认情况下 persist() 会把数据缓存在 JVM 的堆空间中。
但是并不是这两个方法被调用时立即缓存,而是触发后面的action时,该RDD将会被缓存在计算节点的内存中,并供后面重用。
通过查看源码发现cache最终也是调用了persist方法,默认的存储级别都是仅在内存存储一份,Spark的存储级别还有好多种,存储级别在object StorageLevel中定义的。
7.RDD的checkpoint
Spark中对于数据的保存除了持久化操作之外,还提供了一种检查点的机制,检查点(本质是通过将RDD写入Disk做检查点)是为了通过lineage做容错的辅助,lineage过长会造成容错成本过高,这样就不如在中间阶段做检查点容错,如果之后有节点出现问题而丢失分区,从做检查点的RDD开始重做Lineage,就会减少开销。检查点通过将数据写入到HDFS文件系统实现了RDD的检查点功能。
为当前RDD设置检查点。该函数将会创建一个二进制的文件,并存储到checkpoint目录中,该目录是用SparkContext.setCheckpointDir()设置的。在checkpoint的过程中,该RDD的所有依赖于父RDD中的信息将全部被移除。对RDD进行checkpoint操作并不会马上被执行,必须执行Action操作才能触发。
sc.setCheckpointDir("hdfs://hadoop10:9000/rdd-checkpoint")
val rdd1: RDD[String] = sc.makeRDD(List("zhangsan"))
val rdd2: RDD[String] = rdd1.map(_ + System.currentTimeMillis())
rdd2.checkpoint() //此处可以通过查看checkpoint方法的注释了解它的运行机制
rdd2.collect().foreach(println) //zhangsan1588656910532
rdd2.collect().foreach(println) //zhangsan1588656910641
rdd2.collect().foreach(println) //zhangsan1588656910641