Spark core 弹性式数据集RDD

目录

一.RDD简介

 二.RDD的特性:

 三.执行原理:

1.启动Yarn集群环境:

 2.Spark通过申请资源创建调度节点和计算节点:

 3.Spark框架根据需求将计算逻辑根据分区划分不同的任务:

 4.调度节点将任务根据计算节点状态发送到对应的计算节点进行计算:

四.创建RDD

4.1 由现有集合创建:

4.2 引用外部存储系统中的数据集:

4.3  textFile & wholeTextFiles

五.操作RDD

并行度和分区

六.缓存RDD

6.1 缓存级别

 6.2 使用缓存

6.3 移除缓存

七.理解shuffle:

7.1 shuffle介绍:

7.2 shuffle的影响:

7.3 导致Shuffle的操作


一.RDD简介

  RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,是 Spark 中最基本的数据 处理模型。代码中是一个抽象类,它代表一个弹性的、不可变可分区、里面的元素可并行计算的集合。

弹性

  • 存储的弹性:内存与磁盘的自动切换;
  • 容错的弹性:数据丢失可以自动恢复;
  • 计算的弹性:计算出错重试机制;
  • 分片的弹性:可根据需要重新分片。

分布式:数据存储在大数据集群不同节点上 ➢ 数据集:RDD 封装了计算逻辑,并不保存数据

数据抽象:RDD 是一个抽象类,需要子类具体实现

不可变:RDD 封装了计算逻辑,是不可以改变的,想要改变,只能产生新的 RDD,在 新的 RDD 里面封装计算逻辑

可分区、并行计算

 二.RDD的特性:

  • 分区列表:一个 RDD 由一个或者多个分区(Partitions)组成。对于 RDD 来说,每个分区会被一个计算任务所处理,用户可以在创建 RDD 时指定其分区个数,如果没有指定,则默认采用程序所分配到的 CPU 的核心数;
  • 分区计算函数:RDD 拥有一个用于计算分区的函数 compute;
  • 依赖关系:RDD 会保存彼此间的依赖关系,RDD 的每次转换都会生成一个新的依赖关系,这种 RDD 之间的依赖关系就像流水线一样。在部分分区数据丢失后,可以通过这种依赖关系重新计算丢失的分区数据,而不是对 RDD 的所有分区进行重新计算;
  • 窄依赖和宽依赖

  • 由于RDD存在父子依赖关系,在RDD间进行转换的分区对应关系也不同,分为两种类型:窄依赖和宽依赖

  • 窄依赖:父RDD的一个分区,在RDD转换过程中,最多被一个子RDD的分区使用。

  • 宽依赖:父RDD的一个分区,在RDD转换过程中,会被多个子RDD的分区使用。

  • 分区器:Key-Value 型的 RDD 还拥有 Partitioner(分区器),用于决定数据被存储在哪个分区中,目前 Spark 中支持 HashPartitioner(按照哈希分区) 和 RangeParationer(按照范围进行分区);
  • 首选位置:一个优先位置列表 (可选),用于存储每个分区的优先位置 (prefered location)。对于一个 HDFS 文件来说,这个列表保存的就是每个分区所在的块的位置,按照“移动数据不如移动计算“的理念,Spark 在进行任务调度的时候,会尽可能的将计算任务分配到其所要处理数据块的存储位置。

 三.执行原理:

从计算的角度来讲,数据处理过程中需要计算资源(内存 & CPU)和计算模型(逻辑)。 执行时,需要将计算资源和计算模型进行协调和整合。 Spark 框架在执行时,先申请资源,然后将应用程序的数据处理逻辑分解成一个一个的计算任务。然后将任务发到已经分配资源的计算节点上, 按照指定的计算模型进行数据计算。最后得到计算结果。 RDD 是 Spark 框架中用于数据处理的核心模型,接下来我们看看,在 Yarn 环境中,RDD 的工作原理:

1.启动Yarn集群环境:

 2.Spark通过申请资源创建调度节点和计算节点:

 3.Spark框架根据需求将计算逻辑根据分区划分不同的任务:

 4.调度节点将任务根据计算节点状态发送到对应的计算节点进行计算:

 上面可以看出RDD 在整个流程中主要用于将逻辑进行封装,并生成 Task 发送给 Executor 节点执行计算。

四.创建RDD

RDD有两种创建方式:

4.1 由现有集合创建:

启动Spark-shell后,程序会自动创建应用上下文,相当于执行了下面的 Scala 语句:

val conf = new SparkConf().setAppName("Spark shell").setMaster("local[4]")
val sc = new SparkContext(conf)

由现有集合创建 RDD,你可以在创建时指定其分区个数,如果没有指定,则采用程序所分配到的 CPU 的核心数:

val data = Array(1, 2, 3, 4, 5)
// 由现有集合创建 RDD,默认分区数为程序所分配到的 CPU 的核心数
val dataRDD = sc.parallelize(data) 
// 查看分区数
dataRDD.getNumPartitions
// 明确指定分区数
val dataRDD = sc.parallelize(data,2)

4.2 引用外部存储系统中的数据集:

引用外部存储系统中的数据集,例如本地文件系统,HDFS,HBase 或支持 Hadoop InputFormat 的任何数据源。

val fileRDD = sc.textFile("/usr/file/emp.txt")
// 获取第一行文本
fileRDD.take(1)

使用外部存储系统时需要注意以下两点:

  • 如果在集群环境下从本地文件系统读取数据,则要求该文件必须在集群中所有机器上都存在,且路径相同;
  • 支持目录路径,支持压缩文件,支持使用通配符。

4.3  textFile & wholeTextFiles

两者都可以用来读取外部文件,但是返回格式是不同的:

  • textFile:其返回格式是 RDD[String] ,返回的是就是文件内容,RDD 中每一个元素对应一行数据;
  • wholeTextFiles:其返回格式是 RDD[(String, String)],元组中第一个参数是文件路径,第二个参数是文件内容;
  • 两者都提供第二个参数来控制最小分区数;
  • 从 HDFS 上读取文件时,Spark 会为每个块创建一个分区。
def textFile(path: String,minPartitions: Int = defaultMinPartitions): RDD[String] = withScope {...}
def wholeTextFiles(path: String,minPartitions: Int = defaultMinPartitions): RDD[(String, String)]={..}

五.操作RDD

  RDD 支持两种类型的操作lazy算子和no-lazy算子:transformations(转换,从现有数据集创建新数据集)和 actions(在数据集上运行计算后将值返回到驱动程序)。RDD 中的所有转换操作都是惰性的,它们只是记住这些转换操作,但不会立即执行,只有遇到 action 操作后才会真正的进行计算,这类似于函数式编程中的惰性求值。

val list = List(1, 2, 3)
// map 是一个 transformations 操作,而 foreach 是一个 actions 操作
sc.parallelize(list).map(_ * 10).foreach(println)
// 输出: 10 20 30

并行度和分区

默认情况下,Spark 可以将一个作业切分多个任务后,发送给 Executor 节点并行计算,而能 够并行计算的任务数量我们称之为并行度。这个数量可以在构建 RDD 时指定。记住,这里 的并行执行的任务数量,并不是指的切分任务的数量,不要混淆了。

val sparkConf =
 new SparkConf().setMaster("local[*]").setAppName("spark")
val sparkContext = new SparkContext(sparkConf)
val dataRDD: RDD[Int] =
 sparkContext.makeRDD(
 List(1,2,3,4),
 4)
val fileRDD: RDD[String] =
 sparkContext.textFile(
 "input",
 2)
fileRDD.collect().foreach(println)
sparkContext.stop()

读取内存数据时,数据可以按照并行度的设定进行数据的分区操作,数据分区规则的 

def positions(length: Long, numSlices: Int): Iterator[(Int, Int)] = {
 (0 until numSlices).iterator.map { i =>
 val start = ((i * length) / numSlices).toInt
 val end = (((i + 1) * length) / numSlices).toInt
 (start, end)
 }

读取文件数据时,数据是按照 Hadoop 文件读取的规则进行切片分区,而切片规则和数 据读取的规则有些差异,具体 Spark 核心源码如下 

public InputSplit[] getSplits(JobConf job, int numSplits)
 throws IOException {
 long totalSize = 0; // compute total size
 for (FileStatus file: files) { // check we have valid files
 if (file.isDirectory()) {
 throw new IOException("Not a file: "+ file.getPath());
 }
 totalSize += file.getLen();
 }
 long goalSize = totalSize / (numSplits == 0 ? 1 : numSplits);
 long minSize = Math.max(job.getLong(org.apache.hadoop.mapreduce.lib.input.
 FileInputFormat.SPLIT_MINSIZE, 1), minSplitSize);

 ...

 for (FileStatus file: files) {

 ...

 if (isSplitable(fs, path)) {
 long blockSize = file.getBlockSize();
 long splitSize = computeSplitSize(goalSize, minSize, blockSize);
 ...
 }
 protected long computeSplitSize(long goalSize, long minSize,
 long blockSize) {
 return Math.max(minSize, Math.min(goalSize, blockSize));
 }

六.缓存RDD

6.1 缓存级别

Spark 速度非常快的一个原因是 RDD 支持缓存。成功缓存后,如果之后的操作使用到了该数据集,则直接从缓存中获取。虽然缓存也有丢失的风险,但是由于 RDD 之间的依赖关系,如果某个分区的缓存数据丢失,只需要重新计算该分区即可。

Spark 支持多种缓存级别 :

Storage Level
(存储级别)
Meaning(含义)
MEMORY_ONLY默认的缓存级别,将 RDD 以反序列化的 Java 对象的形式存储在 JVM 中。如果内存空间不够,则部分分区数据将不再缓存。
MEMORY_AND_DISK将 RDD 以反序列化的 Java 对象的形式存储 JVM 中。如果内存空间不够,将未缓存的分区数据存储到磁盘,在需要使用这些分区时从磁盘读取。
MEMORY_ONLY_SER将 RDD 以序列化的 Java 对象的形式进行存储(每个分区为一个 byte 数组)。这种方式比反序列化对象节省存储空间,但在读取时会增加 CPU 的计算负担。仅支持 Java 和 Scala 。
MEMORY_AND_DISK_SER类似于 MEMORY_ONLY_SER,但是溢出的分区数据会存储到磁盘,而不是在用到它们时重新计算。仅支持 Java 和 Scala。
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,需要设置为正值。

 6.2 使用缓存

缓存数据的方法有两个:persist和cache。cache内部调用的也是persist,它是persist的特殊化形式,等价于persist(StorageLevel.MEMORY_ONLY)。示例如下:

// 所有存储级别均定义在 StorageLevel 对象中
fileRDD.persist(StorageLevel.MEMORY_AND_DISK)
fileRDD.cache()

6.3 移除缓存

Spark 会自动监视每个节点上的缓存使用情况,并按照最近最少使用(LRU)的规则删除旧数据分区。当然,你也可以使用RDD.unpersist()方法进行手动删除。

七.理解shuffle:

7.1 shuffle介绍:

  在 Spark 中,一个任务对应一个分区,通常不会跨分区操作数据。但如果遇到reduceByKey等操作,Spark 必须从所有分区读取数据,并查找所有键的所有值,然后汇总在一起以计算每个键的最终结果 ,这称为 Shuffle。

Spark Shuffle是发生在宽依赖情况下,上游stage和下游stage传递数据的一种机制。

Shuffle将数据重新组织,使其在上游和下游task之间进行传递和计算。

父类RDD中同一分区的我数据会按照算子要求重新进入RDD的不同分区中

他产生的中间结果会写入磁盘

由于子RDD拉取数据,默认情况下huffle不会改变分区数量。

细讲一下:

shuffle写入阶段的数据操作需要分区,聚合和排序三个功能。

Shuffle写入的总体设计框架:map()输出->数据聚合(combine)->排序(sort)->分区

第一种:不需要聚合和排序

第二种,不需要聚合但是需要排序

 

 第三种,需要聚合,需要或者不需要排序

Shuffle Write框架需要执行的3个步骤是“数据聚合→排序→分区”。

如果应用中的数据操作不需要聚合,也不需要排序,而且分区个数很少,那么可以采用直

接输出模式,即BypassMergeSortShuffleWriter。

为了克服BypassMergeSortShuffleWriter打开文件过多、buffer分配过多的缺点,也为了支持需要按Key进行排序的操作,Spark提供了SortShuffleWriter,使用基于Array排序的方法,以

分区id或分区id+Key进行排序,只输出单一的分区文件即可。

最后,为了支持map()端combine操作,Spark提供了基于Map的SortShuffleWriter,将Array替换为类似HashMap的操作来支持聚合操作,在聚合后根据partitionId或分区id+Key对record进行排序,并输出分区文件。因为SortShuffleWriter按分区id进行了排序,所以被称为sort-based Shuffle Write。

7.2 shuffle的影响:

Shuffle 是一项昂贵的操作,因为它通常会跨节点操作数据,这会涉及磁盘 I/O,网络 I/O,和数据序列化。某些 Shuffle 操作还会消耗大量的堆内存,因为它们使用堆内存来临时存储需要网络传输的数据。Shuffle 还会在磁盘上生成大量中间文件,从 Spark 1.3 开始,这些文件将被保留,直到相应的 RDD 不再使用并进行垃圾回收,这样做是为了避免在计算时重复创建 Shuffle 文件。如果应用程序长期保留对这些 RDD 的引用,则垃圾回收可能在很长一段时间后才会发生,这意味着长时间运行的 Spark 作业可能会占用大量磁盘空间,通常可以使用 spark.local.dir 参数来指定这些临时文件的存储目录。

7.3 导致Shuffle的操作

由于 Shuffle 操作对性能的影响比较大,所以需要特别注意使用,以下操作都会导致 Shuffle:

  • 涉及到重新分区操作: 如 repartition 和 coalesce
  • 所有涉及到 ByKey 的操作:如 groupByKey 和 reduceByKey,但 countByKey 除外;
  • 联结操作:如 cogroup 和 join

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值