Spark---持久化,共享变量和RDD之间的依赖关系详解

发布于:2024-05-02 ⋅ 阅读:(24) ⋅ 点赞:(0)

一、持久化

1、什么是持久化,为什么要持久化

Spark中最重要的功能之一是跨操作在内存中持久化(或缓存)数据集。当您持久化RDD时,每个节点将其计算的任何分区存储在内存中,并在该数据集(或从该数据集派生的数据集)上的其他操作中重用这些分区。这使得未来的行动更快(通常超过10倍)。缓存是迭代算法和快速交互使用的关键工具。

2、如何进行持久化

持久化的方法就是rdd.persist()或者rdd.cache()

3、持久化策略

可以通过persist(StoreageLevle的对象)来指定持久化策略

rdd.persist(StorageLevel.MEMORY_ONLY)
持久化策略 含义
MEMORY_ONLY(默认) rdd中的数据,以未经序列化的java对象格式,存储在内存中。如果内存不足,剩余的部分不持久化,使用的时候,没有持久化的那一部分数据重新加载。这种效率是最高,但是是对内存要求最高的。
MEMORY_ONLY_SER 就比MEMORY_ONLY多了一个SER序列化,保存在内存中的数据是经过序列化之后的字节数组,同时每一个partition此时就是一个比较大的字节数组。
MEMORY_AND_DISK 和MEMORY_ONLY相比就多了一个,内存存不下的数据存储在磁盘中
MEMEORY_AND_DISK_SER 比MEMORY_AND_DISK多了个序列化
DISK_ONLY 就是MEMORY_ONLY对应,都保存在磁盘,效率太差,一般不用。
xxx_2 就是上述多个策略后面加了一个_2,比如MEMORY_ONLY_2,MEMORY_AND_DISK_SER_2等等,就多了一个replicate而已,备份,所以性能会下降,但是容错或者高可用加强了。所以需要在二者直接做权衡。如果说要求数据具备高可用,同时容错的时间花费比从新计算花费时间少,此时便可以使用,否则一般不用。
HEAP_OFF(experimental) 使用非Spark的内存,也即堆外内存,比如Tachyon,HBase、Redis等等内存来补充spark数据的缓存。
4、如何选择一款合适的持久化策略

第一就选择默认MEMORY_ONLY,因为性能最高嘛,但是对空间要求最高;如果空间满足不了,退而求其次,选择MEMORY_ONLY_SER,此时性能还是蛮高的,相比较于MEMORY_ONLY的主要性能开销就是序列化和反序列化;如果内存满足不了,直接跨越MEMORY_AND_DISK,选择MEMEORY_AND_DISK_SER,因为到这一步,说明数据蛮大的,要想提高性能,关键就是基于内存的计算,所以应该尽可能的在内存中存储对象;DISK_ONLY不用,xx_2的使用如果说要求数据具备高可用,同时容错的时间花费比从新计算花费时间少,此时便可以使用,否则一般不用。

二、共享变量

为了能够更加高效的在driver和算子之间共享数据,spark提供了两种有限的共享变量,一广播变量,二累加器。

1、broadcast广播变量

如果我们要在分布式计算里面分发大对象,例如:字典,集合,黑白名单等,这个都会由Driver端进行分发,一般来讲,如果这个变量不是广播变量,那么每个task就会分发一份,这在task数目十分多的情况下Driver的带宽会成为系统的瓶颈,而且会大量消耗task服务器上的资源,如果将这个变量声明为广播变量,那么只是每个executor拥有一份,这个executor启动的task会共享这个变量,节省了通信的成本和服务器的资源。

val list = List("hello hadoop")
//定义广播变量
val broadCast = sc.broadcast(list)
//调用
val data=broadCast.value

注意:
1、能不能将一个RDD使用广播变量广播出去?
不能,因为RDD是不存储数据的。可以将RDD的结果广播出去。
2、 广播变量只能在Driver端定义,不能在Executor端定义。
3、 在Driver端可以修改广播变量的值,在Executor端无法修改广播变量的值。
4、如果executor端用到了Driver的变量,如果不使用广播变量在Executor有多少task就有多少Driver端的变量副本。
5、如果Executor端用到了Driver的变量,如果使用广播变量在每个Executor中只有一份Driver端的变量副本。

2、accumulator累加器

accumulator累加器的概念和mr中出现的counter计数器的概念有异曲同工之妙,对某些具备某些特征的数据进行累加。累加器的一个好处是,不需要修改程序的业务逻辑来完成数据累加,同时也不需要额外的触发一个action job来完成累加

//构建一个累加器
val accu = sc.longAccumuator()
//累加的操作
accu.add(参数)
//获取累加器的结果,累加器的获取,必须需要action的触发
val ret = accu.value

注意:
1、累加器的调用,也就是accumulator.value必须要在action之后被调用,也就是说累加器必须在action触发之后。
2、多次使用同一个累加器,应该尽量做到用完即重置。accumulator.reset
3、尽量给累加器指定name,方便我们在web-ui上面进行查看。

三、RDD数据分区

Spark目前支持Hash分区和Range分区,用户也可以自定义分区,Hash分区为当前的默认分区,Spark中分区器直接决定了RDD中分区的个数、RDD中每条数据经过Shuffle过程属于哪个分区和Reduce的个数。
分区的决定,就是在宽依赖的过程中才有,窄依赖因为是一对一或者一对常熟,分区确定的,所以不需要指定分区操作。

1、Partitioner

在Spark中涉及RDD的分区策略的抽象类为Partitioner,有两个核心的子类实现,一个HashPartitioner,一个RangePartitioner。Spark中数据分区的主要工具类(数据分区类),主要用于Spark底层RDD的数据重分布的情况中。

2、HashPartitioner

Spark中非常重要的一个分区器,也是默认分区器,默认用于90%以上的RDD相关API上。
功能:依据RDD中key值的hashCode的值将数据取模后得到该key值对应的下一个RDD的分区id值,支持key值为null的情况,当key为null的时候,返回0;该分区器基本上适合所有RDD数据类型的数据进行分区操作;但是需要注意的是,由于JAVA中数组的hashCode是基于数组对象本身的,不是基于数组内容的,所以如果RDD的key是数组类型,那么可能导致数据内容一致的数据key没法分配到同一个RDD分区中,这个时候最好自定义数据分区器,采用数组内容进行分区或者将数组的内容转换为集合。

3、RangePartitioner

SparkCore中除了HashPartitioner分区器外,另外一个比较重要的已经实现的分区器,主要用于RDD的数据排序相关API中,比如sortByKey底层使用的数据分区器就是RangePartitioner分区器;该分区器的实现方式主要是通过两个步骤来实现的,第一步:先从整个RDD中抽取出样本数据,将样本数据排序,计算出每个分区的最大key值,形成一个Array[KEY]类型的数组变量rangeBounds;第二步:判断key在rangeBounds中所处的范围,给出该key值在下一个RDD中的分区id下标;该分区器要求RDD中的KEY类型必须是可以排序的。

四、RDD依赖关系

1、依赖关系

RDD和它依赖的父RDD的关系有两种不同的类型,即窄依赖(narrow dependency)和宽依赖(wide dependency)。
1、窄依赖(narrow dependency):指的是子RDD一个分区中的数据,来自于上游RDD中一个分区或者常数个分区。
2、宽依赖(wide dependency):指的是子RDD一个分区中的数据,来自于上游RDD所有的分区。

2、血统Lineage

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