RDD
- RDD基本概念
- RDD五大属性
- A list of partitions
- A function for computing each split
- A list of dependencies on other RDDs
- Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned)
- Optionally, a list of preferred locations to compute each split on (e.g. block locations for an HDFS file)
- 基于spark的单词统计程序剖析rdd的五大属性
- RDD创建方式
- RDD算子
- RDD算子运行地点
- RDD的依赖关系
- RDD的血统
- RDD的缓存机制
- RDD的checkpoint机制
- 数据读取顺序
RDD基本概念
RDD是spark core的核心,是spark的基本数据抽象,它代表一个不可变、可分区、里面元素可并行计算的集合
- RDD(Resilent Distributed Dataset)也称为弹性分布式数据集,是spark中最基本的数据抽象。它具有以下特点
- Dataset:表示一个集合,用来存储数据
- Distributed :表示RDD内部数据元素进行了分布式存储
- Resilent:表示存储位置弹性,可以存储在内存或者磁盘中
- 不可变:当前RDD的元素不可以改变
- 可分区:对于(K,V)类型的元素,可以蛇者不同的元素去往不同分区
RDD五大属性
RDD源码注释
A list of partitions
一个分区列表:一个RDD的数据集的基本组成单位为分区
- 一个RDD有很多个分区(RDD是分布式存储),每一个分区包含了该RDD的部分数据
- spark任务以task线程为基本单位运行,一个分区对应一个task(同一个stage的不同RDD的相同分区也对应一个task,详情参考Spark DAG)
RDD分区数
- 对于读取HDFS文件: RDD的分区数=max(文件的block个数,defaultMinPartitions)
- 当只有一个block时,RDD分区为2
- 对于读取本地文件:RDD分区数=max(文件的split个数,defaultMinPartitions)
- 当只有一个split时,RDD分区为2
更多分区数相关参考下面的链接:
Spark分区数
A function for computing each split
spark中RDD的计算是以分区为单位的,每个RDD都会实现compute计算函数
A list of dependencies on other RDDs
一个RDD会依赖于其他多个RDD。
- spark的容错机制利用了这个特性(血统)
示例
# 后面的RDD依赖于前面的RDD
val rdd1=sc.textFile("/words.txt")
val rdd2=rdd1.flatMap(x=>x.split(" ")) ------>x=>x.split(" ")
val rdd3=rdd2.map(x=>(x,1)) ------>x=>(x,1)
val rdd6=rdd4.join(rdd5)
Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned)
对于K-V类型的数据,可以选择使用分区函数对其进行分区
- spark支持两种分区函数(默认使用哈希分区)
- 基于哈希的HashPartitioner,(key.hashcode % 分区数= 分区号)。
- 基于范围的RangePartitioner
Optionally, a list of preferred locations to compute each split on (e.g. block locations for an HDFS file)
spark同样采用了移动计算的思想,即在启动任务计算时,会优先考虑在存有数据的本地节点开启任务,来达到减小网络传输,提升计算效率的目的。
基于spark的单词统计程序剖析rdd的五大属性
-
需求
HDFS上有一个大小为300M的文件,通过spark实现文件单词统计,最后把结果数据保存到HDFS上
-
代码
sc.textFile("/words.txt").flatMap(_.split(" ")).map((_,1)).reduceByKey(_+_).saveAsTextFile("/out")
-
流程分析
RDD创建方式
-
通过已经存在的scala集合构建,一般用于前期测试。关键字
parallelize
val rdd1=sc.parallelize(List(1,2,3,4,5)) val rdd2=sc.parallelize(Array("hadoop","hive","spark")) val rdd3=sc.makeRDD(List(1,2,3,4))
-
加载外部的数据源构建
val rdd1=sc.textFile("/words.txt")
-
从已经存在的RDD进行转换生成**新的RDD
val rdd2=rdd1.flatMap(_.split(" ")) val rdd3=rdd2.map((_,1))
RDD算子
RDD的算子可以分为两类
- transformation
- action
transformation算子
transformation算子根据已经存在的RDD转换生成新的RDD,延迟加载,不会立即执行
transformation算子
转换 | 含义 |
---|---|
map(func) | 返回一个新的RDD,该RDD由每一个输入元素经过func函数转换后组成 |
filter(func) | 返回一个新的RDD,该RDD由经过func函数计算后返回值为true的输入元素组成 |
flatMap(func) | 类似于map,但是每一个输入元素可以被映射为0或多个输出元素(所以func应该返回一个序列,而不是单一元素) |
mapPartitions(func) | 类似于map,但独立地在RDD的每一个分片上运行,因此在类型为T的RDD上运行时,func的函数类型必须是Iterator[T] => Iterator[U] |
mapPartitionsWithIndex(func) | 类似于mapPartitions,但func带有一个整数参数表示分片的索引值,因此在类型为T的RDD上运行时,func的函数类型必须是(Int, Interator[T]) => Iterator[U] |
union(otherDataset) | 对源RDD和参数RDD求并集后返回一个新的RDD |
intersection(otherDataset) | 对源RDD和参数RDD求交集后返回一个新的RDD |
distinct([numTasks])) | 对源RDD进行去重后返回一个新的RDD |
groupByKey([numTasks]) | 在一个(K,V)的RDD上调用,返回一个(K, Iterator[V])的RDD |
reduceByKey(func, [numTasks]) | 在一个(K,V)的RDD上调用,返回一个(K,V)的RDD,使用指定的reduce函数,将相同key的值聚合到一起,与groupByKey类似,reduce任务的个数可以通过第二个可选的参数来设置 |
sortByKey([ascending], [numTasks]) | 在一个(K,V)的RDD上调用,K必须实现Ordered接口,返回一个按照key进行排序的(K,V)的RDD |
sortBy(func,[ascending], [numTasks]) | 与sortByKey类似,但是更灵活 |
join(otherDataset, [numTasks]) | 在类型为(K,V)和(K,W)的RDD上调用,返回一个相同key对应的所有元素对在一起的(K,(V,W))的RDD |
cogroup(otherDataset, [numTasks]) | 在类型为(K,V)和(K,W)的RDD上调用,返回一个(K,(Iterable,Iterable))类型的RDD |
coalesce(numPartitions) | 减少 RDD 的分区数到指定值。 |
repartition(numPartitions) | 重新给 RDD 分区 |
repartitionAndSortWithinPartitions(partitioner) | 重新给 RDD 分区,并且每个分区内以记录的 key 排序 |
Action算子
action算子触发任务执行。一个action触发的任务对应一个job。
action算子
动作 | 含义 |
---|---|
reduce(func) | reduce将RDD中元素前两个传给输入函数,产生一个新的return值,新产生的return值与RDD中下一个元素(第三个元素)组成两个元素,再被传给输入函数,直到最后只有一个值为止。 |
collect() | 在驱动程序中,以数组的形式返回数据集的所有元素 |
count() | 返回RDD的元素个数 |
first() | 返回RDD的第一个元素(类似于take(1)) |
take(n) | 返回一个由数据集的前n个元素组成的数组 |
takeOrdered(n, [ordering]) | 返回自然顺序或者自定义顺序的前 n 个元素 |
saveAsTextFile(path) | 将数据集的元素以textfile的形式保存到HDFS文件系统或者其他支持的文件系统,对于每个元素,Spark将会调用toString方法,将它装换为文件中的文本 |
saveAsSequenceFile(path) | 将数据集中的元素以Hadoop sequencefile的格式保存到指定的目录下,可以使HDFS或者其他Hadoop支持的文件系统。 |
saveAsObjectFile(path) | 将数据集的元素,以 Java 序列化的方式保存到指定的目录下 |
countByKey() | 针对(K,V)类型的RDD,返回一个(K,Int)的map,表示每一个key对应的元素个数。 |
foreach(func) | 在数据集的每一个元素上,运行函数func |
foreachPartition(func) | 在数据集的每一个分区上,运行函数func |
RDD算子运行地点
- 所有涉及RDD内部元素计算的数据全部在executor端运行
- 需要汇总所有RDD数据的计算或者对RDD自身的操作返回driver端运行,如collect。
- 因为要汇总大量的数据,需要driver端有足够的内存,如10G。否则容易OOM
RDD的依赖关系
RDD的依赖关系有两种:
- 窄依赖(naroow dependecies)
- 宽依赖(wide dependecies)
窄依赖
- 正确定义:每一个父RDD的
partition
最多被子RDD的一个partition
使用 - 错误定义:一个子RDD的任意一个
partition
至多只同时依赖同一个RDD的一个partition
常见的窄依赖算子
- map
- flatMap
- Filter
- union
- mapValues
- mapPartitions
- mapPartitionsWithIndex
所有的窄依赖都不会产生shuffle
宽依赖
- 正确定义:多个子RDD的
partition
依赖同一个父RDD的partition
- 错误定义:存在一个子RDD的至少一个
partition
同时依赖一个父RDD的多个partition
常见的宽依赖算子
- reduceByKey
- sortByKey
- groupBy
- groupByKey
所有的宽依赖都会产生shuffle
关于宽依赖和债依赖定义的补充(重要)
之前我一直对RDD依赖关系,一直是按照错误定义理解的。直到考虑到一种极端情况,子RDD只有一个且只有一个分区。可以使用colaesce
算子实现。
- colaesce算子不产生shuffle,是一个导致
窄依赖
的算子。 - 这种情况下,会导致多个同一父RDD的不同
partition
被同一个子RDD的算子使用。是宽/窄依赖错误定义的反例
join算子的依赖关系
join算子既可能产生宽依赖也可能产生窄依赖,产生的结果主要看子RDD依赖的父RDD的是否有相同的分区函数,即分区规则。
生成依赖的源码
- 可以在此处加断点看生成的是什么依赖
- 当然也可以直接看生成的DAG图
override def getDependencies: Seq[Dependency[_]] = {
rdds.map { rdd: RDD[_ <: Product2[K, _]] =>
if (rdd.partitioner == Some(part)) {
logDebug("Adding one-to-one dependency with " + rdd)
new OneToOneDependency(rdd)
} else {
logDebug("Adding shuffle dependency with " + rdd)
new ShuffleDependency[K, Any, CoGroupCombiner](rdd, part, serializer)
}
}
}
join的窄依赖
- 子RDD3依赖父RDD1和父RDD2
- 父RDD1和父RDD2有
相同
的分区函数
join的宽依赖
- 子RDD3依赖父RDD1和父RDD2
- 父RDD1和父RDD2有
不同
的分区函数
RDD的血统
RDD的血统(Lineage),是一种依赖关系。
- 血统会记录RDD的元数据信息和转换行为(因此可以根据最原始的根数据加上一系列的转换行为),血统保存了RDD的依赖关系。
- RDD只支持粗粒度转换。即只记录单个块上的单个操作
血统的生存周期
当一个job结束时,其对应的血统就消失了
- 一个
application
对应多个job
- 每个
action
触发一个Job
RDD的缓存机制
可以把一个RDD的数据缓存起来,后续有其他job需要用到该RDD的数据,可以直接从缓存中获取。
没有缓存的数据需要根据血统从最源头的RDD重新计算。
RDD支持两种缓存:
- persist
- cache
/**
* Persist this RDD with the default storage level (`MEMORY_ONLY`).
*/
def persist(): this.type = persist(StorageLevel.MEMORY_ONLY)
/**
* Persist this RDD with the default storage level (`MEMORY_ONLY`).
*/
def cache(): this.type = persist()
- persist和cache都不是设置后立刻调用,都是在action算子触发任务执行时才被调用。
- cache实际上调用的是persist,两种缓存的默认缓存级别都是memory_only
缓存级别
- 带2的表示缓存两份
- 如
DISK_ONLY_2
。表示仅仅保存在磁盘中,在磁盘中保留2份
- 如
- MEMORY_AND_DISK :表示优先存在内存中,内存存不下才存在disk
object StorageLevel {
val NONE = new StorageLevel(false, false, false, false)
val DISK_ONLY = new StorageLevel(true, false, false, false)
val DISK_ONLY_2 = new StorageLevel(true, false, false, false, 2)
val MEMORY_ONLY = new StorageLevel(false, true, false, true)
val MEMORY_ONLY_2 = new StorageLevel(false, true, false, true, 2)
val MEMORY_ONLY_SER = new StorageLevel(false, true, false, false)
val MEMORY_ONLY_SER_2 = new StorageLevel(false, true, false, false, 2)
val MEMORY_AND_DISK = new StorageLevel(true, true, false, true)
val MEMORY_AND_DISK_2 = new StorageLevel(true, true, false, true, 2)
val MEMORY_AND_DISK_SER = new StorageLevel(true, true, false, false)
val MEMORY_AND_DISK_SER_2 = new StorageLevel(true, true, false, false, 2)
val OFF_HEAP = new StorageLevel(true, true, true, false, 1)
缓存级别 释义
如何选择缓存级别
Spark的多个存储级别意味着在内存利用率和cpu利用效率间的不同权衡。我们推荐通过下面的过程选择一个合适的存储级别:
-
如果你的RDD适合默认的存储级别(
MEMORY_ONLY
),就选择默认的存储级别。因为这是cpu利用率最高的选项,会使RDD上的操作尽可能的快。 -
如果不适合用默认的级别,选择
MEMORY_ONLY_SER
。选择一个更快的序列化库提高对象的空间使用率,但是仍能够相当快的访问。 -
除非函数计算RDD的花费较大或者它们需要过滤
大量的数据
,不要将RDD存储到磁盘
上,否则,重复计算一个分区就会和重磁盘上读取数据一样慢。 -
如果你希望更快的错误恢复,可以利用重复(replicated)存储级别。所有的存储级别都可以通过重复计算丢失的数据来支持完整的容错,但是重复的数据能够使你在RDD上继续运行任务,而不需要重复计算丢失的数据。
-
在拥有大量内存的环境中或者多应用程序的环境中,OFF_HEAP具有如下优势:
- 它运行多个执行者共享Tachyon中相同的内存池
- 它显著地减少垃圾回收的花费
- 如果单个的执行者崩溃,缓存的数据不会丢失
缓存设置时机
-
当某个RDD的数据后期被使用多次时,可以考虑设置缓存
-
某个RDD的结果数据经过大量计算
val rdd2=rdd1.flatMap(函数).map(函数).reduceByKey(函数).xxx.xxx.xxx.xxx.xxx
如上图所示
- 上图有2个Job,处在同一个application中。两个job分别计算得到RDD3和RDD4。
- 第一次使用RDD2做相应的计算得到RDD3,会先从HDFS读取文件,然后从RDD1开始计算得到RDD2,RDD2计算得到RDD3。此时RDD3对应job完成,其对应的血统消失
- 计算RDD4的时候如果没有做缓存,会从读取HDFS上的文件开始重新走一遍流程。计算得出RDD4后,其对应job完成,其对应的血统消失
- 此时可以考虑对RDD2做缓存,计算RDD4时直接从RDD2获取结果。
缓存设置示例
val rdd1=sc.textFile("/words.txt")
val rdd2=rdd1.flatMap(_.split(" "))
val rdd3=rdd2.cache
rdd3.collect
val rdd4=rdd3.map((_,1))
val rdd5=rdd4.persist(缓存级别)
rdd5.collect
cache和persist的区别
- cache: 默认是把数据缓存在内存中,其本质就是调用persist方法;
- persist:可以把数据缓存在内存或者是磁盘,有丰富的缓存级别,这些缓存级别都被定义在StorageLevel这个object中。
缓存数据的清除
- 自动清除
- 一个application结束后,对应的缓存数据,无论是内存还是磁盘都会清除
- 手动清除
-
调用RDD的unpersist方法
rdd..unpersist()
-
RDD的checkpoint机制
- RDD的checkpoint类似于快照,可以将目标数据进行缓存,一般存储在HDFS上
- 每次运行application生成的checkpoint之间是相互隔离的。
- 即使不同的application指定了同一个checkpoint目录,各个application会在指定目录下生成各自对应的子目录,达到相互隔离的效果。
如何设置checkpoint
-
在HDFS上设置一个checkpoint目录
sc.setCheckpointDir("hdfs://node01:8020/checkpoint")
-
对需要做checkpoint的RDD调用checkpoint方法
# 一般会先设置cache,然后设置checkpoint提上效率 val rdd1=sc.textFile("/words.txt") rdd1.checkpoint val rdd2=rdd1.flatMap(_.split(" ")) 配置了hadoop和spark整合 路径直接写"/"接口 不需要写node
-
调用action操作触发任务运行
rdd2.collect 只有执行了算子操作,checkpoint才会生效,缓存才会产生 checkpoint会单独触发一个单独的Job,从源头计算一遍 action触发任务之后,cache只有一个job,
spark streaming的checkpoint
本文主要讨论的是RDD,上面的例子也是举的RDD的列子。
checkpoint除了应用于保存RDD数据,也可以用来保存spark streaming的业务处理逻辑。
即 Metadata checkpointing,当driver端提交的查程序运行失败时,会自动从checkpoint读取程序逻辑运行。
Metadata checkpointing
将流式计算的信息保存到具备容错性的存储上比如HDFS,Metadata Checkpointing适用于当streaming应用程序Driver所在的节点出错时能够恢复,元数据包括:
- Configuration(配置信息) : 创建streaming应用程序的配置信息
- Dstream operations : 在streaming应用程序中定义的DStreaming操作
- Incomplete batches : 在队列中没有处理完的作业
注意:
如果更改了driver程序,不会读取新的程序,还是会按照之前备份的逻辑运行
checkpoint的删除
需要手动删除HDFS上的文件: hdfs df rm -r
官网地址
cache persist checkpoint三者的区别
- cache和persist
- cache默认数据保存在内存中
- persist可以保存在内存或者磁盘中
- cache和persist需要action操作触发,但是不会触发新的job
- cache和persist不会改变RDD的依赖关系
- 程序运行完 ,缓存的数据自动消失
- checkpoint
- 把数据持久化写入到HDFS上
- 同样需要actionc啊哦做触发,但是会新启动一个Job,从最开始的数据源重新计算一遍得到缓存
- checkpoint会改变血统,即RDD的依赖关系
数据读取顺序
当后续用到之前的某个RDD时,会按照以下顺序尝试读取
- 从缓存中读取
- 从checkpoint读取
- 都没有读取到,且血统没被checkpoint破坏。根据血统重新计算
示例
sc.setCheckpointDir("/checkpoint")
val rdd1=sc.textFile("/words.txt")
val rdd2=rdd1.cache
rdd2.checkpoint
val rdd3=rdd2.flatMap(_.split(" "))
rdd3.collect
# checkpoint操作要执行需要有一个action操作,一个action操作对应后续的一个job。
# 该job执行完成之后,它会再次单独开启另外一个job来执行 rdd1.checkpoint操作。
# 对checkpoint在使用的时候进行优化,在调用checkpoint操作之前,可以先来做一个cache操作,缓存对应rdd的结果数据
# 后续就可以直接从cache中获取到rdd的数据写入到指定checkpoint目录中
sc.setCheckpointDir("/checkpoint")
val rdd1=sc.textFile("/words.txt")
val rdd2=rdd1.cache
rdd2.cache
rdd2.checkpoint
rdd2.flatMap(_.split(" ")).map((_,1))
# 当rdd2.checkpoint之后,直接从rdd2的缓存中拿到数据,不用从头计算了。提升了性能
# 后续用到rdd2时,优先从cache中找,找不到再从checkpoint存储路径找,最后实在找不到利用血统从源头重新计算