Spark:RDD编程总结(概述、算子、分区、共享变量)

本文详细介绍了Spark的RDD(弹性分布式数据集)概念,包括其不可变、只读、分区和依赖特性。重点讲解了RDD的创建、转换与行动操作,以及如何通过缓存和检查点实现数据持久化。此外,还探讨了RDD的分区策略,如HashPartitioner和RangePartitioner,以及如何自定义分区。最后,文章讨论了在Spark中使用广播变量和累加器进行高效的共享变量操作。
摘要由CSDN通过智能技术生成

目录

1、RDD概述

1.1、RDD是什么 

1.2、RDD的弹性

1.3、RDD的特点

1.3.1、分区

1.3.2、只读

1.3.3、依赖

1.3.4、缓存

1.3.5、检查点

2、RDD编程

2.1、RDD创建

2.1.1、并行化集合

2.1.2、读取外部数据集

2.2、RDD的操作

2.2.1、转换

2.2.2、行动

2.2.3、控制

      1)缓存

      2)检查点

3、RDD算子练习案例

3.1、求百年来降水量TOP10

3.2、二次排序

3.3、祖父-孙子关系

3.4、PageRank

4、PairRDD数据分区

4.1、获取/设置分区方式

4.2、HashPartitioner

4.3、RangePartitioner

4.4、自定义分区器

5、共享变量

5.1、广播变量

5.2、累加器


1、RDD概述

1.1、RDD是什么 

RDD(Resilient Distributed Dataset),弹性分布式数据集,实现了Spark数据处理的核心抽象。

MapReduce也是一种基于数据集的工作模式,MapReduce一般是从存储上加载数据集,然后操作数据集,中间计算过程也有可能会写入存储,最后写入物理存储设备。而数据更多面临的是一次性处理。MR在迭代式算法以及交互式数据挖掘上就很不擅长。

1)RDD代表一个不可变、可分区(分片)、只读的集合

2)在 Spark 中,对数据的所有操作不外乎创建 RDD、转化已有 RDD 以及调用 RDD 操作进行求值

3)每个 RDD 都被分为多个分区,这些分区运行在集群中的不同节点上

4)RDD 具有数据流模型的特点:自动容错、位置感知性调度和可伸缩性

5)RDD 允许执行多个查询时显式地将工作集缓存在内存中,后续的查询能够重用工作集,极大地提升了查询速度

6)RDD 支持两种操作:转化操作和行动操作。RDD 的转化操作是返回一个新的 RDD 的操作,比如 map() 和 filter(),而行动操作则是向驱动器程序返回结果或把结果写入外部系统的操作。比如 count() 和 first()

7)Spark 采用 惰性计算模式,RDD 只有第一次在一个行动操作中用到时,才会真正计算。Spark 可以优化整个计算过程。默认情况下,Spark 的 RDD 会在你每次对它们进行行动操作时重新计算。如果想在多个行动操作中重用同一个 RDD,可以使用 RDD.persist() 让 Spark 把这个 RDD 缓存下来。

1.2、RDD的弹性

1)内存与磁盘存储的弹性
Spark优先将数据放在内存中,若内存放不下才会被放到磁盘中,弹性切换存储模式
RDD可通过persist持久化将RDD缓存到内存或磁盘中,当再次使用到该RDD时直接从内存中读取即可;也可将RDD进行保存检查点,checkpoint会将RDD存储在hdfs上,该RDD的所有父RDD依赖都会被移出

2)基于血统的容错弹性
RDD在进行转换和行动时,会形成RDD的Lineage依赖链。当一个RDD失效时,可以通过重新计算上游的RDD来重新生成丢失的RDD数据

3)数据调度计算的弹性
Spark将任务执行模型抽象为通用的有向无环图DAG,可以将多stage的任务并行执行,调度引擎自动处理stage及task的失败。RDD的计算任务task,或者task中的某个stage计算失败时,会自动进行重新计算,默认的次数为4次

4)数据分区的高弹性
每个 RDD 的分区运行在集群中的不同节点上,RDD允许动态地调整数据分区的个数

1.3、RDD的特点

关于一个 RDD 至少可以知道以下几点信息:
1、分区数以及分区方式;
2、由父 RDDs 衍生而来的相关依赖信息;
3、计算每个分区的数据,计算步骤为:
   1)如果被缓存,则从缓存中取的分区的数据;
   2)如果被 checkpoint,则从 checkpoint 处恢复数据;
   3)根据血缘关系计算分区的数据。

1.3.1、分区

RDD逻辑上是分区的,底层采用的是MapReduce V1中的文件逻辑分片,每个分区的数据位于集群中不同的节点中。计算的时候回通过compute函数来得到每个分区的数据。如果RDD通过已有的文件系统构建,则comput函数读取指定文件系统中的数据;若RDD是通过其他RDD转化而来的,则compute函数执行转罗逻辑将其他RDD的数据进行转换

1.3.2、只读

RDD是只读的,要想改变RDD中的数据,只能在现有的RDD基础上创建新的RDD。
spark中提供了80多种算子实现了RDD的转换。RDD的操作算子包括两类:一是转换算子,它将RDD进行转换,构建RDD的血缘关系;二是行动算子,它用来触发RDD的计算,可以得到RDD的计算结果或将RDD保存到文件系统中

1.3.3、依赖

RDD通过算子进行转换,得到的新RDD包含了从其他RDD衍生所必需的的信息,RDD之间维护着血缘关系,也称为依赖。由于算子操作对象为RDD中的每个分区,所以RDD之间的依赖也可理解为各个分区之间的依赖
依赖包括两种:窄依赖和宽依赖。窄依赖是一对一的关系,一个父分区对应一个子分区;宽依赖则为一对多,每个父分区对应多个子分区

1)窄依赖允许在一个集群节点上以流水线的方式(pipeline)计算所有父分区。例如,逐个元素地执行map、然后filter操作;窄依赖能够更有效地进行失效节点的恢复,即只需重新计算丢失RDD分区的父分区,而且不同节点之间可以并行计算
2)宽依赖则需要首先计算好所有父分区数据,然后在节点之间进行Shuffle,这与MapReduce类似。对于一个宽依赖关系的Lineage图,单个节点失效可能导致这个RDD的所有祖先丢失部分分区,因而需要整体重新计算。

通过RDD之间的依赖关系,一个任务可以描述为DAG。窄依赖的转换在一个stage中流式进行,宽依赖需要进行混洗

1.3.4、缓存

如果应用程序中多次使用同一个RDD,可以将该RDD缓存起来,则该RDD只有在第一次计算的时候回根据依赖得到分区的数据,在后续用到该RDD时,会直接从缓存中取得数据而不用重新按照血缘关系计算一遍

1.3.5、检查点

对于长时间迭代型的应用来说,随着多次迭代RDD之间的依赖关系会越来越长,那么在后续过程中出错,则需要通过非常长的血缘关系去重建,与hdfs的检查点机制一个道理(防止edit编辑日志文件过长)。RDD支持checkpoint将数据保存到持久化地存储中,这样会切断之前的血缘关系

2、RDD编程

2.1、RDD创建

RDD的创建大致可以分为2类:从集合中创建RDD、从外部存储创建RDD

2.1.1、并行化集合

可以通过SparkContext对象的parallelize或makeRDD方法并行化集合,并且可以传入分区数量参数

  def parallelize[T: ClassTag](
      seq: Seq[T],
      numSlices: Int = defaultParallelism): RDD[T] = withScope {
    assertNotStopped()
    new ParallelCollectionRDD[T](this, seq, numSlices, Map[Int, Seq[String]]())
  }

  //调用了parallelize
  def makeRDD[T: ClassTag](
      seq: Seq[T],
      numSlices: Int = defaultParallelism): RDD[T] = withScope {
    parallelize(seq, numSlices)
  }

  //接收的参数类型是Seq[(T, Seq[String])],也就是每个对象具有一个或多个位置首选项(Spark 节点的主机名)
  //并且分区数为Seq[(T, Seq[String])]的size
  def makeRDD[T: ClassTag](seq: Seq[(T, Seq[String])]): RDD[T] = withScope {
    assertNotStopped()
    val indexToPrefs = seq.zipWithIndex.map(t => (t._2, t._1._2)).toMap
    new ParallelCollectionRDD[T](this, seq.map(_._1), math.max(seq.size, 1), indexToPrefs)
  }

关于分区数详情可见https://jodjod.blog.csdn.net/article/details/97494565

//standalone模式下,有3个Excutor,每个Excutor默认1核,所以默认分区数为3。实际调用的是parallelize
scala> val rdd = sc.makeRDD(List(1,2,3,4,5))
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[1] at makeRDD at <console>:24

//获取RDD分区数
scala> rdd.partitions.size
res1: Int = 3

//指定为4个分区
scala> val rdd = sc.parallelize(List(1,2,3,4,5),4)
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[2] at parallelize at <console>:24

scala> rdd.partitions.size
res2: Int = 3

//glom函数将同一个分区内的元素合并到一个数组中,collect函数将RDD类型的数据转化为数组,同时会从远程集群是拉取数据到driver端
scala> rdd.glom.collect
res4: Array[Array[Int]] = Array(Array(1), Array(2), Array(3), Array(4, 5))

//传入的是List[(Int,List[String])],此时列表中有几个元素就有几个分区
scala> val rdd = sc.makeRDD(List((1,List("master","slave1")),(2,List("slave2"))))
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[5] at makeRDD at <console>:24

//preferredLocations函数查看"首选位置",其实就是访问字符串列表;partitions(N)中N指定了分区号
scala> rdd.preferredLocations(rdd.partitions(0))
res7: Seq[String] = List(master, slave1)

scala> rdd.preferredLocations(rdd.partitions(1))
res8: Seq[String] = List(slave2)

2.1.2、读取外部数据集

可以从本地文件系统或者HDFS上读取数据集

//默认为HDFS路径,若从本地文件系统读取,在standalone模式下确保每个Worker节点也存在相同文件,因为RDD不同分区的数据会存储在不同节点中
scala> val tf = sc.textFile("/f1.txt")
tf: org.apache.spark.rdd.RDD[String] = /f1.txt MapPartitionsRDD[9] at textFile at <console>:24

RDD也支持从其他文件格式(sequencefile、objectfile等)中读取文件,在后面会讲到

2.2、RDD的操作

RDD 中的所有转换都是延迟加载的,也就是说它们并不会直接计算结果。它们只是记住这些应用到基础数据集(例如一个文件)上的转换动作。只有当发生一个要求返回结果给 Driver 的动作时,这些转换才会真正运行。这种设计让 Spark 更加有效率地运行。

操作类型大致可以分为:转换操作、控制操作、行动操作

2.2.1、转换

转换操作会返回一个新的RDD

1)map(func)

//返回一个新的 RDD,该 RDD 由每一个输入元素经过 func 函数转换后组成
def map[U: ClassTag](f: T => U): RDD[U]
scala> val rdd = sc.parallelize(List(1,2,3))
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[10] at parallelize at <console>:24

//对rdd中每个元素+1
scala> rdd.map(_+1)
res9: org.apache.spark.rdd.RDD[Int] = MapPartitionsRDD[11] at map at <console>:27

scala> rdd.collect
res10: Array[Int] = Array(1, 2, 3)

2)filter(func)

//返回一个新的 RDD,该 RDD 由经过 func 函数计算后返回值为 true 的输入元素组成
def filter(f: T => Boolean): RDD[T] 
scala> val rdd = sc.parallelize(List(1,2,3))
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[12] at parallelize at <console>:24

//过滤出奇数,注意会过滤掉func返回false的
scala> rdd.filter(_%2 == 1).collect
res13: Array[Int] = Array(1, 3)

3)flatMap(func)

//类似于 map,但将map结果扁平化到一个序列中
def flatMap[U: ClassTag](f: T => TraversableOnce[U]): RDD[U]

scala> val rdd = sc.parallelize(List(List(1,2,3),List(4,5,6)))
rdd: org.apache.spark.rdd.RDD[List[Int]] = ParallelCollectionRDD[15] at parallelize at <console>:24

scala> rdd.map(x=>x).collect
res16: Array[List[Int]] = Array(List(1, 2, 3), List(4, 5, 6))

scala> rdd.flatMap(x=>x).collect
res18: Array[Int] = Array(1, 2, 3, 4, 5, 6)

//注意:字符串也是序列!
scala> val rdd = sc.parallelize(List("abc","def"))
rdd: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[20] at parallelize at <console>:24

scala> rdd.flatMap(x=>x).collect
res20: Array[Char] = Array(a, b, c, d, e, f)

4)mapPartitions(func)

    //类似于 map,但独立地在 RDD 的每一个分片上运行,因此在类型为 T 的 RDD 上运行时,func 的函数类型必须是 Iterator[T] => Iterator[U]
    def mapPartitions[U: ClassTag](
        f: Iterator[T] => Iterator[U],
        preservesPartitioning: Boolean = false): RDD[U]

假设有 N 个元素,有 M 个分区,那么 map 的函数的将被调用 N 次,而 mapPartitions 被调用 M 次,一个函数一次处理所有分区。mapPartitions 的执行效率要比 map 高。例如将RDD中的所有数据通过JDBC连接写入数据库中,如果使用map函数可能需要为每一个元素都创建一个连接,若使用mapPartition函数可以针对每一个分区创建一个连接

  //相当于RDD中每个元素调用func
  def map[U: ClassTag](f: T => U): RDD[U] = withScope {
    val cleanF = sc.clean(f)
    new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.map(cleanF))
  }

  def map[B](f: A => B): Iterator[B] = new AbstractIterator[B] {
    def hasNext = self.hasNext
    def next() = f(self.next())
  }

  //相当于将分区(迭代器)作为参数传递给func
  def mapPartitions[U: ClassTag](
      f: Iterator[T] => Iterator[U],
      preservesPartitioning: Boolean = false): RDD[U] = withScope {
    val cleanedF = sc.clean(f)
    new MapPartitionsRDD(
      this,
      (context: TaskContext, index: Int, iter: Iterator[T]) => cleanedF(iter),
      preservesPartitioning)
  }
scala> val rdd = sc.parallelize(1 to 10, 5)
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[23] at parallelize at <console>:24

//第一个_表示分区(迭代器),第二个_表示分区中的元素
scala> rdd.mapPartitions(_.map(_+"a")).collect
res21: Array[String] = Array(1a, 2a, 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值