RDD持久化原理与共享变量

来源:互联网 发布:eric python 编辑:程序博客网 时间:2024/06/10 03:08

RDD 持久化工作原理

  • Spark非常重要的一个功能特性就是可以将RDD 持久化在内存中,当对RDD执行持久化操作时,每个节点都会将自己操作的RDD的partition持久化到内存中,并且在之后对该RDD的反复使用中,直接使用内存缓存的partition,这样的话,对于针对一个RDD反复执行多个操作的场景,就只要对RDD计算一次即可,后面直接使用该RDD ,而不需要计算多次该RDD
  • 巧妙使用RDD持久化,甚至在某些场景下,可以将spark应用程序的性能提升10倍。对于迭代式算法和快速交互式应用来说,RDD持久化,是非常重要的。
  • 要持久化一个RDD,只要调用其cache()或者persist()方法即可。在该RDD第一次被计算出来时,就会直接缓存在每个节点中。而且Spark的持久化机制还是自动容错的,如果持久化的RDD的任何partition丢失了,那么Spark会自动通过其源RDD,使用transformation操作重新计算该partition。
  • cache()和persist()的区别在于,cache()是persist()的一种简化方式,cache()的底层就是调用的persist()的无参版本,同时就是调用persist(MEMORY_ONLY),将数据持久化到内存中。如果需要从内存中去除缓存,那么可以使用unpersist()方法。

RDD持久化使用场景

  • 1、第一次加载大量的数据到RDD中
  • 2、频繁的动态更新RDD Cache数据,不适合使用Spark Cache、Spark lineage

RDD 持久化的策略

如何选择一种最合适的持久化策略?

  • 默认情况下,性能最高的当然是MEMORY_ONLY,但前提是你的内存必须足够足够大,可以绰绰有余地存放下整个RDD的所有数据。因为不进行序列化与反序列化操作,就避免了这部分的性能开销;对这个RDD的后续算子操作,都是基于纯内存中的数据的操作,不需要从磁盘文件中读取数据,性能也很高;而且不需要复制一份数据副本,并远程传送到其他节点上。但是这里必须要注意的是,在实际的生产环境中,恐怕能够直接用这种策略的场景还是有限的,如果RDD中数据比较多时(比如几十亿),直接用这种持久化级别,会导致JVM的OOM内存溢出异常。
  • 如果使用MEMORY_ONLY级别时发生了内存溢出,那么建议尝试使用MEMORY_ONLY_SER级别。该级别会将RDD数据序列化后再保存在内存中,此时每个partition仅仅是一个字节数组而已,大大减少了对象数量,并降低了内存占用。这种级别比MEMORY_ONLY多出来的性能开销,主要就是序列化与反序列化的开销。但是后续算子可以基于纯内存进行操作,因此性能总体还是比较高的。此外,可能发生的问题同上,如果RDD中的数据量过多的话,还是可能会导致OOM内存溢出的异常。
  • 如果纯内存的级别都无法使用,那么建议使用MEMORY_AND_DISK_SER策略,而不是MEMORY_AND_DISK策略。因为既然到了这一步,就说明RDD的数据量很大,内存无法完全放下。序列化后的数据比较少,可以节省内存和磁盘的空间开销。同时该策略会优先尽量尝试将数据缓存在内存中,内存缓存不下才会写入磁盘。
  • 通常不建议使用DISK_ONLY和后缀为_2的级别:因为完全基于磁盘文件进行数据的读写,会导致性能急剧降低,有时还不如重新计算一次所有RDD。后缀为_2的级别,必须将所有数据都复制一份副本,并发送到其他节点上,数据复制以及网络传输会导致较大的性能开销,除非是要求作业的高可用性,否则不建议使用。

RDD进行持久化和不进行持久化的区别

RDD持久化代码

public class PersistApp {public static void main(String[] args) {    SparkConf conf = new SparkConf().setAppName(PersistApp.class.getSimpleName()).setMaster("local");    JavaSparkContext sc = new JavaSparkContext(conf);    JavaRDD<String> linesRDD = sc.textFile("E:\\test\\scala\\access_2016-05-30.log");    linesRDD.cache();    long start = System.currentTimeMillis();    List<String> list = linesRDD.take(10);    long end = System.currentTimeMillis();    System.out.println("first times cost" + (end - start) + "ms");    System.out.println("-----------------------------------");    start = System.currentTimeMillis();    long count = linesRDD.count();    end = System.currentTimeMillis();    System.out.println("second times cost" + (end - start) + "ms");    sc.close(); }}

共享变量

  • 通常情况下,当向Spark操作(如map,reduce)传递一个函数时,它会在一个远程集群节点上执行,它会使用函数中所有变量的副本。这些变量被复制到所有的机器上,远程机器上并没有被更新的变量会向驱动程序回传。在任务之间使用通用的,支持读写的共享变量是低效的。 尽管如此,Spark提供了两种有限类型的共享变量,广播变量和累加器。

广播变量

  • Spark的另一种共享变量是广播变量。通常情况下,当一个RDD的很多操作都需要使用driver中定义的变量时,每次操作,driver都要把变量发送给worker节点一次,如果这个变量中的数据很大的话,会产生很高的传输负载,导致执行效率降低。使用广播变量可以使程序高效地将一个很大的只读数据发送给多个worker节点,而且对每个worker节点只需要传输一次,每次操作时executor可以直接获取本地保存的数据副本,不需要多次传输。

object BroadCastApp {def main(args: Array[String]): Unit = {    val conf = new SparkConf().setMaster("local[2]").setAppName("BroadCastApp")    val sc = new SparkContext(conf)    val list = List(1, 2, 4, 6, 0, 9)    val set = mutable.HashSet[Int]()    val num = 7    val bset = sc.broadcast(set)    val bNum = sc.broadcast(7)    val listRDD = sc.parallelize(list)    listRDD.map(x => {        bset.value.+=(x)        x * bNum.value    }).foreach(x => print(x + " "))    println("----------------------")    for (s <- set) {        println(s)    }    sc.stop()    }}

创建并使用广播变量的过程如下:

在一个类型T的对象obj上使用SparkContext.brodcast(obj)方法,创建一个Broadcast[T]类型的广播变量,obj必须满足Serializable。
通过广播变量的.value()方法访问其值。
另外,广播过程可能由于变量的序列化时间过程或者序列化变量的传输过程过程而成为瓶颈,而Spark Scala中使用的默认的Java序列化方法通常是低效的,因此可以通过spark.serializer属性为不同的数据类型实现特定的序列化方法(如Kryo)来优化这一过程。

累加器

Spark提供的Accumulator,主要用于多个节点对一个变量进行共享性的操作。Accumulator只提供了累加的功能。但是确给我们提供了多个task对一个变量并行操作的功能。但是task只能对Accumulator进行累加操作,不能读取它的值。只有Driver程序可以读取Accumulator的值。

object AccumulatorApp {def main(args: Array[String]): Unit = {    val conf = new SparkConf().setAppName("AccumulatorApp").setMaster("local[2]")    val sc = new SparkContext(conf)    val list = List(1, 2, 4, 6, 0, 9)    val listRDD = sc.parallelize(list)    val acc = sc.accumulator(0)    list.map(x => {        /**          * 在这里只能对累加器进行写的操作,不能进行读的操作          * count-->action          * 主要是可以替代直接使用count来统计某一个transformation运行的数据量,          * 因为count是一个action,一旦执行了action操作,前面rdd partition中数据会被释放掉          * 这样要想在进行其他的操作,就需要重新加载计算数据,会是spark程序性能降低          */        acc.add(1)        (x, 1)    })    println("累加结果: " + acc.value)    sc.stop() }}
0 0