大数据-Spark(二)

                                  大数据-Spark(二)

目录

RDD 基本概念

RDD 五大属性

RDD 创建方式

RDD 算子分类

RDD 常见的算子操作说明

RDD 常用的算子操作演示

RDD 依赖关系

RDD 缓存机制

RDD checkpoint机制

DAG 有向无环图生成

Spark 任务调度

Spark 运行架构


RDD 基本概念

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

  • Resilient: 表示弹性,rdd的数据是可以保存在内存或者是磁盘中。
  • Distributed:它内部的元素进行了分布式存储,方便于后期进行分布式计算。
  • Dataset: 就是一个集合,存储很多数据。

RDD 五大属性

  • A list of partitions 一个分区(Partition)列表,数据集的基本组成单位。

这里表示一个rdd有很多分区,每一个分区内部是包含了该rdd的部分数据,spark中任务是以task线程的方式运行, 一个分区就对应一个task线程。用户可以在创建RDD时指定RDD的分区个数,如果没有指定,那么就会采用默认值。
val rdd=sparkContext.textFile("/words.txt")
//通过sc读取hdfs上数据文件生成的RDD分区数:
RDD的分区数=max(文件的block个数,defaultMinPartitions)

  • A function for computing each split 一个计算每个分区的函数

Spark中RDD的计算是以分区为单位的,每个RDD都会实现compute计算函数以达到这个目的。

  • A list of dependencies on other RDDs 一个rdd会依赖于其他多个rdd

这里就涉及到rdd与rdd之间的依赖关系,spark任务的容错机制就是根据这个特性(血统)而来。

  • Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned) 一个Partitioner,即RDD的分区函数(可选项)

当前Spark中实现了两种类型的分区函数,一个是基于哈希的HashPartitioner,(key.hashcode % 分区数= 分区号)。它是默认值。

另外一个是基于范围的RangePartitioner,只有对于key-value的RDD(RDD[(String, Int)]),并且产生shuffle,才会有Partitioner,非key-value的RDD(RDD[String])的Parititioner的值是None。

Option类型:可以表示有值或者没有值,它有2个子类: 一个是Some表示封装了值, 另一个是None表示没有值。

  • Optionally, a list of preferred locations to compute each split on (e.g. block locations for an HDFS file) 一个列表,存储每个Partition的优先位置(可选项)

这里涉及到数据的本地性,数据块位置最优。spark任务在调度的时候会优先考虑存有数据的节点开启计算任务,减少数据的网络传输,提升计算效率。

RDD 创建方式

① 通过已经存在的scala集合去构建

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 算子分类

  • transformation(转换)

根据已经存在的rdd转换生成一个新的rdd, 它是延迟加载,它不会立即执行。例如map / flatMap / reduceByKey 等。

  • action (动作)

它会真正触发任务的运行,将rdd的计算的结果数据返回给Driver端,或者是保存结果数据到外部存储介质中。例如collect / saveAsTextFile 等。

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<V>,Iterable<W>))类型的RDD
coalesce(numPartitions)减少 RDD 的分区数到指定值。
repartition(numPartitions)重新给 RDD 分区
repartitionAndSortWithinPartitions(partitioner)重新给 RDD 分区,并且每个分区内以记录的 key 排序
  • 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 常用的算子操作演示

为了方便前期的测试和学习,可以使用spark-shell进行演示:

spark-shell --master local[2]
  • map
val rdd1 = sc.parallelize(List(5, 6, 4, 7, 3, 8, 2, 9, 1, 10))

//把rdd1中每一个元素乘以10
rdd1.map(_*10).collect
  • filter

val rdd1 = sc.parallelize(List(5, 6, 4, 7, 3, 8, 2, 9, 1, 10))

//把rdd1中大于5的元素进行过滤
rdd1.filter(x => x >5).collect
  • flatMap
val rdd1 = sc.parallelize(Array("a b c", "d e f", "h i j"))
//获取rdd1中元素的每一个字母
rdd1.flatMap(_.split(" ")).collect
  • intersection、union
val rdd1 = sc.parallelize(List(5, 6, 4, 3))
val rdd2 = sc.parallelize(List(1, 2, 3, 4))
//求交集
rdd1.intersection(rdd2).collect

//求并集
rdd1.union(rdd2).collect
  • distinct

val rdd1 = sc.parallelize(List(1,1,2,3,3,4,5,6,7))
//去重
rdd1.distinct
  • join、groupByKey

val rdd1 = sc.parallelize(List(("tom", 1), ("jerry", 3), ("kitty", 2)))
val rdd2 = sc.parallelize(List(("jerry", 2), ("tom", 1), ("shuke", 2)))
//求join
val rdd3 = rdd1.join(rdd2)
rdd3.collect
//求并集
val rdd4 = rdd1 union rdd2
rdd4.groupByKey.collect
  • cogroup
val rdd1 = sc.parallelize(List(("tom", 1), ("tom", 2), ("jerry", 3), ("kitty", 2)))
val rdd2 = sc.parallelize(List(("jerry", 2), ("tom", 1), ("jim", 2)))
//分组
val rdd3 = rdd1.cogroup(rdd2)
rdd3.collect
  • reduce
val rdd1 = sc.parallelize(List(1, 2, 3, 4, 5))

//reduce聚合
val rdd2 = rdd1.reduce(_ + _)
rdd2.collect

val rdd3 = sc.parallelize(List("1","2","3","4","5"))
rdd3.reduce(_+_)

//这里可能会出现多个不同的结果,由于元素在不同的分区中,每一个分区都是一个独立的task线程去运行。
//这些task运行有先后关系
  • reduceByKey、sortByKey
val rdd1 = sc.parallelize(List(("tom", 1), ("jerry", 3), ("kitty", 2),  ("shuke", 1)))
val rdd2 = sc.parallelize(List(("jerry", 2), ("tom", 3), ("shuke", 2), ("kitty", 5)))
val rdd3 = rdd1.union(rdd2)

//按key进行聚合
val rdd4 = rdd3.reduceByKey(_ + _)
rdd4.collect

//按value的降序排序
val rdd5 = rdd4.map(t => (t._2, t._1)).sortByKey(false).map(t => (t._2, t._1))
rdd5.collect
  • repartition、coalesce

val rdd1 = sc.parallelize(1 to 10,3)
//打印rdd1的分区数
rdd1.partitions.size

//利用repartition改变rdd1分区数
//减少分区
rdd1.repartition(2).partitions.size

//增加分区
rdd1.repartition(4).partitions.size

//利用coalesce改变rdd1分区数
//减少分区
rdd1.coalesce(2).partitions.size


//repartition:  重新分区, 有shuffle
//coalesce:     合并分区 / 减少分区 	默认不shuffle   
//默认 coalesce 不能扩大分区数量。除非添加true的参数,或者使用repartition。

//适用场景:
    //1、如果要shuffle,都用 repartition
    //2、不需要shuffle,仅仅是做分区的合并,coalesce
    //3、repartition常用于扩大分区。
  • map、mapPartitions 、mapPartitionsWithIndex

val rdd1=sc.parallelize(1 to 10,5)
rdd1.map(x => x*10)).collect
rdd1.mapPartitions(iter => iter.map(x=>x*10)).collect

//index表示分区号  可以获取得到每一个元素属于哪一个分区
rdd1.mapPartitionsWithIndex((index,iter)=>iter.map(x=>(index,x)))

map:用于遍历RDD,将函数f应用于每一个元素,返回新的RDD(transformation算子)。
mapPartitions:用于遍历操作RDD中的每一个分区,返回生成一个新的RDD(transformation算子)。

总结:
如果在映射的过程中需要频繁创建额外的对象,使用mapPartitions要比map高效
比如,将RDD中的所有数据通过JDBC连接写入数据库,如果使用map函数,可能要为每一个元素都创建一个connection,这样开销很大,如果使用mapPartitions,那么只需要针对每一个分区建立一个connection。
  • foreach、foreachPartition
val rdd1 = sc.parallelize(List(5, 6, 4, 7, 3, 8, 2, 9, 1, 10))

//foreach实现对rdd1里的每一个元素乘10然后打印输出
rdd1.foreach(x=>println(x * 10))

//foreachPartition实现对rdd1里的每一个元素乘10然后打印输出
rdd1.foreachPartition(iter => iter.foreach(x=>println(x * 10)))

foreach:用于遍历RDD,将函数f应用于每一个元素,无返回值(action算子)。
foreachPartition: 用于遍历操作RDD中的每一个分区。无返回值(action算子)。


总结:
一般使用mapPartitions或者foreachPartition算子比map和foreach更加高效,推荐使用。

RDD 依赖关系

RDD和它依赖的父RDD的关系有两种不同的类型,窄依赖(narrow dependency)和宽依赖(wide dependency)。

  • 窄依赖 :窄依赖指的是每一个父RDD的Partition最多被子RDD的一个Partition使用。map/flatMap/filter/union等这些算子操作是窄依赖,所有的窄依赖不会产生shuffle。
  • 宽依赖 :宽依赖指的是多个子RDD的Partition会依赖同一个父RDD的Partition。reduceByKey/sortByKey/groupBy/groupByKey/join等这些算子操作是宽依赖,所有的宽依赖会产生shuffle。
  •  补充说明:由上图可知,join分为宽依赖和窄依赖,如果RDD有相同的partitioner,那么将不会引起shuffle,这种join是窄依赖,反之就是宽依赖。

lineage(血统)

  • RDD只支持粗粒度转换,即只记录单个块上执行的单个操作。
  • 将创建RDD的一系列Lineage(即血统)记录下来,以便恢复丢失的分区。
  • RDD的Lineage会记录RDD的元数据信息和转换行为,lineage保存了RDD的依赖关系,当该RDD的部分分区数据丢失时,它可以根据这些信息来重新运算和恢复丢失的数据分区。

RDD 缓存机制

什么是rdd的缓存

可以把一个rdd的数据缓存起来,后续有其他的job需要用到该rdd的结果数据,可以直接从缓存中获取得到,避免了重复计算。缓存是加快后续对该数据的访问操作。

如何对rdd设置缓存

rdd通过persist方法或cache方法可以将前面的计算结果缓存。但是并不是这两个方法被调用时立即缓存,而是==触发后面的action==时,该RDD将会被缓存在计算节点的内存中,并供后面重用。

通过查看源码发现cache最终也是调用了persist方法,默认的存储级别都是仅在内存存储一份,Spark的存储级别还有好多种,存储级别在object StorageLevel中定义的。 

cache和persist区别

  • cache:   默认是把数据缓存在内存中,其本质就是调用persist方法;
  • persist:可以把数据缓存在内存或者是磁盘,有丰富的缓存级别,这些缓存级别都被定义在StorageLevel这个object中。

什么时候设置缓存

  • 某个rdd的数据后期被使用了多次

如上图所示的计算逻辑: 

(1)当第一次使用rdd2做相应的算子操作得到rdd3的时候,就会从rdd1开始计算,先读取HDFS上的文件,然后对rdd1 做对应的算子操作得到rdd2,再由rdd2计算之后得到rdd3。同样为了计算得到rdd4,前面的逻辑会被重新计算。

(2)默认情况下多次对一个rdd执行算子操作, rdd都会对这个rdd及之前的父rdd全部重新计算一次。 这种情况在实际开发代码的时候会经常遇到,但是我们一定要避免一个rdd重复计算多次,否则会导致性能急剧降低。   

总结:可以把多次使用到的rdd,也就是公共rdd进行持久化,避免后续需要,再次重新计算,提升效率。

  • 为了获取得到一个rdd的结果数据,经过了大量的算子操作或者是计算逻辑比较复杂

清除缓存数据

  • 自动清除:一个application应用程序结束之后,对应的缓存数据也就自动清除。
  • 手动清除:调用rdd的unpersist方法。

RDD checkpoint机制

checkpoint概念

checkpoint 检查点 ,它是提供了一种相对而言更加可靠的数据持久化方式。它是把数据保存在分布式文件系统,比如HDFS上。这里就是利用了HDFS高可用性,高容错性(多副本)来最大程度保证数据的安全性。

如何设置checkpoint

1.在hdfs上设置一个checkpoint目录

sc.setCheckpointDir("hdfs://node01:8020/checkpoint")

2.对需要做checkpoint操作的rdd调用checkpoint方法  

val rdd1=sc.textFile("/words.txt")
rdd1.checkpoint
val rdd2=rdd1.flatMap(_.split(" ")) 

3.最后需要有一个action操作去触发任务的运行

rdd2.collect

cache、persist、checkpoint三者区别

  • cache和persist

    • cache默认数据缓存在内存中
    • persist可以把数据保存在内存或者磁盘中
    • 后续要触发 cache 和 persist 持久化操作,需要有一个action操作
    • 它不会开启其他新的任务,一个action操作就对应一个job
    • 它不会改变rdd的依赖关系,程序运行完成后对应的缓存数据就自动消失
  • checkpoint

    • 可以把数据持久化写入到hdfs上
    • 后续要触发checkpoint持久化操作,需要有一个action操作,后续会开启新的job执行checkpoint操作
    • 它会改变rdd的依赖关系,后续数据丢失了不能够在通过血统进行数据的恢复。
    • 程序运行完成后对应的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目录中。

DAG 有向无环图生成

  • DAG 是什么

DAG(Directed Acyclic Graph) 叫做有向无环图(有方向,无闭环,代表着数据的流向),原始的RDD通过一系列的转换就形成了DAG。

下图是基于单词统计逻辑得到的DAG有向无环图:

  • DAG 划分 stage

stage是什么

一个Job会被拆分为多组Task,每组任务被称为一个stage。stage表示不同的调度阶段,一个spark job会对应产生很多个stage。stage类型一共有2种:

  • ShuffleMapStage 最后一个shuffle之前的所有变换的Stage叫ShuffleMapStage,它对应的task是shuffleMapTask。
  • ResultStage 最后一个shuffle之后操作的Stage叫ResultStage,它是最后一个Stage。它对应的task是ResultTask。

为什么要划分stage

根据RDD之间依赖关系的不同将DAG划分成不同的Stage(调度阶段)。对于窄依赖,partition的转换处理在一个Stage中完成计算。对于宽依赖,由于有Shuffle的存在,只能在parent RDD处理完成后,才能开始接下来的计算,由于划分完stage之后,在同一个stage中只有窄依赖,没有宽依赖,可以实现流水线计算,stage中的每一个分区对应一个task,在同一个stage中就有很多可以并行运行的task。

如何划分stage

划分stage的依据就是宽依赖

(1) 首先根据rdd的算子操作顺序生成DAG有向无环图,接下里从最后一个rdd往前推,创建一个新的stage,把该rdd加入到该stage中,它是最后一个stage。

(2) 在往前推的过程中运行遇到了窄依赖就把该rdd加入到本stage中,如果遇到了宽依赖,就从宽依赖切开,那么最后一个stage也就结束了。

(3) 重新创建一个新的stage,按照第二个步骤继续往前推,一直到最开始的rdd,整个划分stage也就结束了。

stage与stage之间的关系

划分完stage之后,每一个stage中有很多可以并行运行的task,后期把每一个stage中的task封装在一个taskSet集合中,最后把一个一个的taskSet集合提交到worker节点上的executor进程中运行。

rdd与rdd之间存在依赖关系,stage与stage之前也存在依赖关系,前面stage中的task先运行,运行完成了再运行后面stage中的task,也就是说后面stage中的task输入数据是前面stage中task的输出结果数据。

Spark 任务调度

① Driver端运行客户端的main方法,构建SparkContext对象,在SparkContext对象内部依次构建DAGScheduler和TaskScheduler

② 按照rdd的一系列操作顺序,来生成DAG有向无环图。

③ DAGScheduler拿到DAG有向无环图之后,按照宽依赖进行stage的划分。每一个stage内部有很多可以并行运行的task,最后封装在一个一个的taskSet集合中,然后把taskSet发送给TaskScheduler

④ TaskScheduler得到taskSet集合之后,依次遍历取出每一个task提交到worker节点上的executor进程中运行。

⑤ 所有task运行完成,整个任务也就结束了。

Spark 运行架构

① Driver端向资源管理器Master发送注册和申请计算资源的请求

② Master通知对应的worker节点启动executor进程(计算资源)

③ executor进程向Driver端发送注册并且申请task请求

④ Driver端运行客户端的main方法,构建SparkContext对象,在SparkContext对象内部依次构建DAGScheduler和TaskScheduler

⑤ 按照客户端代码洪rdd的一系列操作顺序,生成DAG有向无环图

⑥ DAGScheduler拿到DAG有向无环图之后,按照宽依赖进行stage的划分。每一个stage内部有很多可以并行运行的task,最后封装在一个一个的taskSet集合中,然后把taskSet发送给TaskScheduler

⑦ TaskScheduler得到taskSet集合之后,依次遍历取出每一个task提交到worker节点上的executor进程中运行

⑧ 所有task运行完成,Driver端向Master发送注销请求,Master通知Worker关闭executor进程,Worker上的计算资源得到释放,最后整个任务也就结束了。

基于wordcount程序剖析spark任务的提交、划分、调度流程


此博文仅供学习参考,如有错误欢迎指正。

上一篇《大数据-Spark(一)

下一篇《大数据-Spark(六)

希望对大数据相关技术感兴趣的友友们关注一下,大家可以一起交流学习哦~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值