【Spark算子】Spark RDD编程

1.1 什么是RDD

RDD是 Spark 的基石,是实现 Spark 数据处理的核心抽象。
RDD 是一个抽象类,它代表一个不可变、可分区、里面的元素可并行计算的集合
在这里插入图片描述

RDD(Resilient Distributed Dataset)是 Spark 中的核心概念,它是一个容错、可以并行执行的分布式数据集。

RDD包含5个特征

  1. 一个分区的列表
  2. 一个计算函数compute,对每个分区进行计算
  3. 对其他RDDs的依赖(宽依赖、窄依赖)列表
  4. 对key-value RDDs来说,存在一个分区器(Partitioner)【可选的】
  5. 对每个分区有一个优先位置的列表【可选的】

有一组分区(Partitions),即数据集的基本组成单位。对于RDD来说,每个分片都会被一个计算任务处理,并决定并行计算的粒度。用户可以在创建RDD时指定RDD的分片个数,如果没有指定,那么就会采用默认值;
有一个对分区数据进行计算的函数。Spark中RDD的计算是以分区为单位的,每个RDD都会实现 compute 函数以达到该目的。compute函数会对迭代器进行组合,不需要保存每次计算的结果;
RDD之间存在依赖关系。RDD的每次转换都会生成一个新的RDD,RDD之间形成类似于流水线一样的前后依赖关系(lineage)。在部分分区数据丢失时,Spark可以通过这个依赖关系重新计算丢失的分区数据,而不是对RDD的所有分区进行重新计算;
对于 key-value 的RDD而言,可能存在分区器(Partitioner)。Spark 实现了两种类型的分片函数,一个是基于哈希的HashPartitioner,另外一个是基于范围的RangePartitioner。只有key-value的RDD,才可能有Partitioner, 非key-value的RDD的Parititioner的值是None。Partitioner函数决定了RDD本身的分区数量,也决定了parent RDD Shuffle输出时的分区数量;
一个列表,存储每个Partition的优先位置(preferred location)。对于一个HDFS文件来说,这个列表保存的就是每个Partition所在的块的位置。按照 “移动计算不移动数据” 的理念,Spark在任务调度的时候,会尽可能地将计算任务分配到其所要处理数据块的存储位置。

1.2 RDD的特点

1、分区

RDD逻辑上是分区的,每个分区的数据是抽象存在的,计算的时候会通过一个compute 函数得到每个分区的数据。如果RDD是通过已有的文件系统构建,则compute函数是读取指定文件系统中的数据,如果RDD是通过其他RDD转换而来,则compute函数是执行转换逻辑将其他RDD的数据进行转换。
在这里插入图片描述

题外思考: 为什么RDD要分区?
简而言之是为了分布式计算,分区数决定了并行度.
每个RDD会被划分成一个或多个分区,这些分区可能是保存在Spark集群中的多个节点上的(但是分区就是最小单位了,Spark保证同一个分区的数据是在同一个机器上执行的,不会跨机器处理), 在执行任务时,会并行的在RDD各个分区上进行计算,然后再把结果进行整合得到最后的结果, 同时分区可以减少网络IO, 尤其对于有shuffle操作时,相同key的value会进入同一个分区,这样如果key相似或在同一范围内的数据尽量在同一分区的话, 可以减少网络直接的传输,大大提升效率

分区的特点
spark的分区有以下特点:
1.在Spark集群中每个工作节点,可能都包含一个或多个分区。
2.Spark中使用的分区数是可配置的,但要注意,分区太少或分区太多都不好。

  • 分区太少,会导致较少的并发、数据倾斜、或不正确的资源利用。
  • 分区太多,导致任务调度花费比实际执行时间更多的时间。
  • 若没有配置分区数,默认的分区数是:所有执行程序节点上的内核总数。

3.Spark保证同一个分区的数据位于同一个机器上,不会跨多台机器保存。
4.Spark为每个分区分配一个任务,每个worker一次可以处理一个任务。

我们知道,任务是在worker节点上执行,而分区也保存在worker节点上,
而无论任务做什么计算都是基于分区数据进行的

这就意味着:每个阶段的基础任务数等于分区数。
也就是说:每个阶段的任务不会大于分区数。
由于分区数决定了并行度,因此这也是进行性能调优时需要考虑的重要的方面。
选择适当的分区属性可以大大提高应用程序的性能

2、只读

RDD是只读的(也就意味着不可变),要想改变RDD中的数据,
只能在现有的RDD基础上创建新的RDD;
一个RDD转换为另一个RDD,通过丰富的操作算子(map、filter、union、join、
reduceByKey… …)实现,不再像MR那样只能写map和reduce了。

在这里插入图片描述

RDD的操作算子包括两类:
transformation。用来对RDD进行转化,延迟执行(Lazy);
action。用来触发RDD的计算;得到相关计算结果或者将RDD保存的文件系统中;

3、依赖

RDDs通过操作算子进行转换,转换得到的新RDD包含了从其他RDDs衍生所必需的
信息,RDDs之间维护着这种血缘关系(lineage),也称之为依赖。依赖包括两种:

  • 窄依赖。RDDs之间分区是一一对应的(1:1 或 n:1)
  • 宽依赖。子RDD每个分区与父RDD的每个分区都有关,是多对多的关系(即n:m)。有shuffle发生

什么是shuffle?

之前在Mapreduce中了解到, shuffle阶段一般指从Map阶段产生输出到Reduce阶段取得输出结果作为输入的这个过程称作shuffle.
在RDD中,shuffle是把父RDD中的KV对按照Key重新分区,从而得到一个新的RDD。
也就是说原本同属于父RDD同一个分区的数据需要进入到子RDD的不同的分区的过程就是shuffle.

为什么会产生shuffle ?

在分布式计算框架中,数据本地化是一个很重要的考虑,即计算需要被分发到数据所在的位置,从而减少数据的移动,提高运行效率
Map-Reduce的输入数据通常是HDFS中的文件,所以数据本地化要求map任务尽量被调度到保存了输入文件的节点执行。
但是,有一些计算逻辑是无法简单地获取本地数据的,reduce的逻辑都是如此。
对于reduce来说,处理函数的输入是key相同的所有value,但是这些value所在的数据集(即map的输出)位于不同的节点上,
因此需要对map的输出进行重新组织,使得同样的key进入相同的reducer。
shuffle移动了大量的数据,对计算、内存、网络和磁盘都有巨大的消耗,
因此,只有确实需要shuffle的地方才应该进行shuffle,否则尽可能避免shuffle

什么时候shuffle?

去重,聚合,byKey,排序(sortByKey),重分区,集合或者表操作(join,cogroup)

再回看什么是宽依赖?

父RDD的分区被子RDD的多个分区使用,例如 groupByKey、reduceByKey、sortByKey等操作会产生宽依赖,会产生shuffle

在这里插入图片描述

4、缓存

可以控制存储级别(内存、磁盘等)来进行缓存。
如果在应用程序中多次使用同一个RDD,可以将该RDD缓存起来,该RDD只有在第一次计算的时候会根据血缘关系得到分区的数据,在后续其他地方用到该RDD的时候,会直接从缓存处取而不用再根据血缘关系计算,这样就加速后期的重用
在这里插入图片描述

5、checkpoint

虽然RDD的血缘关系天然地可以实现容错,当RDD的某个分区数据失败或丢失,可以通过血缘关系重建。
是于长时间迭代型应用来说,随着迭代的进行,RDDs之间的血缘关系会越来越长,一旦在后续迭代过程中出错,则需要通过非常长的血缘关系去重建,势必影响性能。
RDD支持 checkpoint 将数据保存到持久化的存储中, 这样就可以切断之前的血缘关系,因为checkpoint后的RDD不需要知道它的父RDDs了,它可以从 checkpoint 处拿到数据。

1.3 Spark编程模型

spark编程模型

RDD表示数据对象

  • 通过对象上的方法调用来对RDD进行转换
  • 最终显示结果 或 将结果输出到外部数据源
  • RDD转换算子称为Transformation是Lazy的(延迟执行)
  • 只有遇到Action算子,才会执行RDD的转换操作

创建一个SparkContext把外部的数据源转换成RDD,然后调用各种算子,把数据做一个转换得到输出结果

要使用Spark,需要编写 Driver 程序(里面有个main方法,会创建SparkContext),它被提交到集群运行

  • Driver中定义了一个或多个 RDD ,并调用 RDD 上的各种算子
  • Worker则执行RDD分区计算任务
    在这里插入图片描述

1.4 RDD的创建

1、SparkContext

SparkContext是编写Spark程序用到的第一个类,是Spark的主要入口点,它负责和整个集群的交互;
在这里插入图片描述

如把Spark集群当作服务端,那么Driver就是客户端,SparkContext 是客户端的核心;

SparkContext是Spark的对外接口,负责向调用者提供 Spark 的各种功能;
SparkContext用于连接Spark集群、创建RDD、累加器、广播变量;

在 spark-shell 中 SparkContext 已经创建好了,可直接使用;
在这里插入图片描述
编写Spark Driver程序第一件事就是:创建SparkContext;

2、从集合创建RDD

从集合中创建RDD,主要用于测试。Spark 提供了以下函数:parallelize、makeRDD、range
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

val rdd1 = sc.parallelize(Array(1,2,3,4,5))
val rdd2 = sc.parallelize(1 to 100)

// 检查 RDD 分区数
rdd2.getNumPartitions
rdd2.partitions.length

// 创建 RDD,并指定分区数
val rdd2 = sc.parallelize(1 to 100)
rdd2.getNumPartitions

val rdd3 = sc.makeRDD(List(1,2,3,4,5))
val rdd4 = sc.makeRDD(1 to 100)
rdd4.getNumPartitions

val rdd5 = sc.range(1, 100, 3)
rdd5.getNumPartitions
val rdd6 = sc.range(1, 100, 2 ,10)
rdd6.getNumPartitions

备注:rdd.collect 方法在生产环境中不要使用,会造成Driver OOM

3、从文件系统创建RDD

用 textFile() 方法来从文件系统中加载数据创建RDD。方法将文件的 URI 作为参数,这个URI可以是:

  • 本地文件系统
    • 使用本地文件系统要注意:该文件是不是在所有的节点存在(在Standalone模式下)
  • 分布式文件系统HDFS的地址
  • Amazon S3的地址
// 从本地文件系统加载数据
val lines = sc.textFile("file:///root/wc.txt")

// 从分布式文件系统加载数据
val lines = sc.textFile("hdfs://linux01:9000/wcinput/wc.txt")
val lines = sc.textFile("/user/root/data/uaction.dat")
val lines = sc.textFile("data/uaction.dat")

小细节: 当从本地文件系统加载数据时,明明文件存在,却报文件不存在的错误, 并且最后还是计算出了结果, 原因是在Standalone模式下,会在所有节点扫描这个文件是否存在,而我们并没有在所有节点有这个文件,此时如果分发到各个节点,那么错误就会消失
在这里插入图片描述

4、从RDD创建RDD

本质是将一个RDD转换为另一个RDD。详细信息参见 Transformation

1. 5 Transformation【重要】

RDD的操作算子分为两类

  • Transformation。用来对RDD进行转化,这个操作时延迟执行的(或者说是Lazy 的);
  • Action。用来触发RDD的计算;得到相关计算结果 或者 将结果保存的外部系统中;
  • Transformation:返回一个新的RDD
  • Action:返回结果int、double、集合(不会返回新的RDD)

每一次 Transformation 操作都会产生新的RDD,供给下一个“转换”使用;
转换得到的RDD是惰性求值的

也就是说,整个转换过程只是记录了转换的轨迹,并不会发生真正的计算,
只有遇到 Action 操作时,才会发生真正的计算,开始从血缘关系(lineage)源头开始,进行物理的转换操作;
在这里插入图片描述

常见的 Transformation 算子:
官方文档:http://spark.apache.org/docs/latest/rdd-programming-guide.html#transformations

常见转换算子1

map(func):对数据集中的每个元素都使用func,然后返回一个新的RDD

filter(func):对数据集中的每个元素都使用func,然后返回一个包含使func为true的元素构成的RDD

flatMap(func):与 map 类似,每个输入元素被映射为0或多个输出元素
flatmap就类似于把一堆叠在一起的数据平铺展开

mapPartitions(func):和 map 很像,但是map是将func作用在每个元素上,而mapPartitions是func作用在整个分区上。假设一个RDD有N个元素,M个分区(N>> M), 那么map的函数将被调用N次,而mapPartitions中的函数仅被调用M次,一次处理一个分区中的所有元素

mapPartitionsWithIndex(func):与 mapPartitions 类似,多了分区索引值信息全部都是窄依赖 (父RDD的每个分区都只被子RDD的一个分区使用)

val rdd1 = sc.parallelize(1 to 10)
val rdd2 = rdd1.map(_*2)
val rdd3 = rdd2.filter(_>10)


// 以上都是 Transformation 操作,没有被执行. 如何证明这些操作按预期执行,此时需要引入Action算子
rdd2.collect
rdd3.collect
// collect 是Action算子, 触发Job的执行,将RDD的全部元素从 Executor搜集到 Driver 端。生产环境中禁用

// flatMap 使用案例
val rdd4 = sc.textFile("/wcinput/wc.txt")
rdd4.collect
rdd4.flatMap(_.split("\\s+")).collect

在这里插入图片描述

// RDD 是分区,rdd1有几个区,每个分区有哪些元素
rdd1.getNumPartitions
rdd1.partitions.length
rdd1.mapPartitions{iter =>
  Iterator(s"${iter.toList}")
}.collect

rdd1.mapPartitions{iter =>
  Iterator(s"${iter.toArray.mkString("-")}")
}.collect

rdd1.mapPartitionsWithIndex{(idx, iter) =>
  Iterator(s"$idx:${iter.toArray.mkString("-")}")
}.collect

// 每个元素 * 2
val rdd5 = rdd1.mapPartitions(iter => iter.map(_*2))
rdd5.collect

在这里插入图片描述

map 与 mapPartitions 的区别
map:每次处理一条数据
mapPartitions:每次处理一个分区的数据,分区的数据处理完成后,数据才能释放,资源不足时容易导致OOM

最佳实践:当内存资源充足时,建议使用mapPartitions,以提高处理效率

常见转换算子2

groupBy(func):按照传入函数的返回值进行分组。将key相同的值放入一个迭代器

glom():将每一个分区形成一个数组,形成新的RDD类型 RDD[Array[T]], 里面是一个Array数组,其中sliding函数可以作用于数组

sample(withReplacement, fraction, seed):采样算子。以指定的随机种子(seed)随机抽样出数量为fraction的数据,withReplacement表示是抽出的数据是否放回,true为有放回的抽样,false为无放回的抽样

distinct([numTasks])):对RDD元素去重后,返回一个新的RDD。可传入numTasks参数改变RDD分区数,这里distinct算子底层用到reduceByKey,所以是有shuffle的
在这里插入图片描述

coalesce(numPartitions):缩减分区数,无shuffle(可以设置第二个参数为true,就等同于下面的repartition算子)
在这里插入图片描述

repartition(numPartitions):增加或减少分区数,有shuffle
可以看到其实就是调用了coalesce方法,只不过把shuffle设为了true
在这里插入图片描述

sortBy(func, [ascending], [numTasks]):使用 func 对数据进行处理,对处理后的结果进行排序

宽依赖的算子(意味着都有shuffle):groupBy、distinct、repartition、sortBy…

比如groupBy,要把相同key的value数据汇聚到一个分区,但是数据可能会在不同的分区里,那就要涉及到一个数据的移动,
这个阶段就是shuffle阶段,以上的宽依赖算子都涉及到shuffle, 例如还有distinct去重,底层是使用到了reduceByKey这个算子,所以也是使用到了shuffle

重分区一般也会shuffle,因为需要在整个集群中,对之前所有的分区的数据进行随机,
均匀的打乱,然后把数据放入下游新的指定数量的分区内

// 将 RDD 中的元素按照3的余数分组
val rdd = sc.parallelize(1 to 10)
val group = rdd.groupBy(_%3)
group.collect

// 将 RDD 中的元素每10个元素分组
val rdd = sc.parallelize(1 to 101)
rdd.glom.collect
rdd.glom.map(_.sliding(10, 10).toArray)
// sliding是Scala中的方法

这里方便测试,创建一个rdd,包含101个元素, 2个分区, 通过glom算子,将每个分区形成一个数组, 也就是一个分区50个元素, 此时我们如果要将RDD中每个10元素分为一组,使用sliding方法, sliding第一个参数的含义是几个元素组成一个Array,这里是10个, 然后第二个参数是步长, 剩余不够的元素组成最后一个Array. 所以如下图所示, 原本两个分区,各50个元素组成两个Array, 然后分组后,每10个元素分成了一组,但是总的分区依旧是2个.
在这里插入图片描述

// 对数据采样。fraction采样的百分比,近似数
// 有放回的采样,使用固定的种子
rdd.sample(true, 0.2, 2).collect

// 无放回的采样,使用固定的种子
rdd.sample(false, 0.2, 2).collect
// 有放回的采样,不设置种子
rdd.sample(false, 0.2).collect

// 数据去重
val random = scala.util.Random
val arr = (1 to 20).map(x => random.nextInt(10))
val rdd = sc.makeRDD(arr)
rdd.distinct.collect

// RDD重分区
val rdd1 = sc.range(1, 10000, numSlices=10)
val rdd2 = rdd1.filter(_%2==0)
rdd2.getNumPartitions

// 减少分区数;都生效了
val rdd3 = rdd2.repartition(5)
rdd3.getNumPartitions
val rdd4 = rdd2.coalesce(5)
rdd4.getNumPartitions

// 增加分区数
val rdd5 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值