RDD 的依赖和持久化

RDD 的依赖和持久化

本文记录的知识点为RDD 的依赖和持久化的实现



提示:以下是本篇文章正文内容,下面案例可供参考

RDD 的血缘和依赖关系

血缘关系

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

val sparConf = new SparkConf().setMaster("local").setAppName("WordCount")
val sc = new SparkContext(sparConf)

val lines: RDD[String] = sc.textFile("datas/words.txt")
println(lines.toDebugString)
println("*************************")
val words: RDD[String] = lines.flatMap(_.split(" "))
println(words.toDebugString)
println("*************************")
val wordToOne = words.map(word=>(word,1))
println(wordToOne.toDebugString)
println("*************************")
val wordToSum: RDD[(String, Int)] = wordToOne.reduceByKey(_+_)
println(wordToSum.toDebugString)
println("*************************")
val array: Array[(String, Int)] = wordToSum.collect()
array.foreach(println)

sc.stop()

结果:

(1) datas/words.txt MapPartitionsRDD[1] at textFile at RDD_Dep.scala:13 []
 |  datas/words.txt HadoopRDD[0] at textFile at RDD_Dep.scala:13 []
*************************
(1) MapPartitionsRDD[2] at flatMap at RDD_Dep.scala:16 []
 |  datas/words.txt MapPartitionsRDD[1] at textFile at RDD_Dep.scala:13 []
 |  datas/words.txt HadoopRDD[0] at textFile at RDD_Dep.scala:13 []
*************************
(1) MapPartitionsRDD[3] at map at Dep.scala:19 []
 |  MapPartitionsRDD[2] at flatMap at RDD_Dep.scala:16 []
 |  datas/words.txt MapPartitionsRDD[1] at textFile at Dep.scala:13 []
 |  datas/words.txt HadoopRDD[0] at textFile at RDD_Dep.scala:13 []
*************************
(1) ShuffledRDD[4] at reduceByKey at RDD_Dep.scala:22 []
 +-(1) MapPartitionsRDD[3] at map at RDD_Dep.scala:19 []
    |  MapPartitionsRDD[2] at flatMap at RDD_Dep.scala:16 []
    |  datas/words.txt MapPartitionsRDD[1] at textFile at RDD_Dep.scala:13 []
    |  datas/words.txt HadoopRDD[0] at textFile at RDD_Dep.scala:13 []
*************************
(scala,1)
(spark,1)
(hello,2)
(hbase,1)
(world,1)

值得注意的是:RDD不会保存数据的,但为了提供容错性,会将RDD间的关系保存下来,也就是说每个RDD会保存血缘关系,这样如果其中哪个RDD出现错误,那么就会根据血缘关系,重新计算

依赖关系

这里所谓的依赖关系,其实就是两个相邻 RDD 之间的关系,当然除了rdd之间的依赖,还有数据的依赖

  1. (OneToOneDependency) 窄依赖表示每一个父(上游)RDD 的 Partition 最多被子(下游)RDD 的一个 Partition 使用,窄依赖我们形象的比喻为独生子女,一对一或多对一
    class OneToOneDependency[T](rdd: RDD[T]) extends NarrowDependency[T](rdd)
    
  2. (ShuffleDependency)宽依赖表示同一个父(上游)RDD 的 Partition 被多个子(下游)RDD 的 Partition 依赖,会引起 Shuffle,宽依赖我们形象的比喻为多生,一对多或多对多
    class ShuffleDependency[K: ClassTag, V: ClassTag, C: ClassTag](...) extends Dependency[Product2[K, V]]
    

代码如下(示例):

val sparConf = new SparkConf().setMaster("local").setAppName("Dep")
val sc = new SparkContext(sparConf)

val lines: RDD[String] = sc.textFile("datas/words.txt")
println(lines.dependencies)
println("*************************")
val words: RDD[String] = lines.flatMap(_.split(" "))
println(words.dependencies)
println("*************************")
val wordToOne = words.map(word=>(word,1))
println(wordToOne.dependencies)
println("*************************")
val wordToSum: RDD[(String, Int)] = wordToOne.reduceByKey(_+_)
println(wordToSum.dependencies)
println("*************************")
val array: Array[(String, Int)] = wordToSum.collect()
array.foreach(println)

sc.stop()

结果:
ShuffleDependency对应的是reduceByKey算子,reduceByKey跟groupByKey一样,存在洗牌操作,因为它需要判断当前数据是否处于同一个分区内,如果不是则会new ShuffledRDD(),重分区,这里就进行了洗牌操作

List(org.apache.spark.OneToOneDependency@285f38f6)
*************************
List(org.apache.spark.OneToOneDependency@61d84e08)
*************************
List(org.apache.spark.OneToOneDependency@1abfe081)
*************************
List(org.apache.spark.ShuffleDependency@4052c8c2)
*************************
(scala,1)
(spark,1)
(hello,2)
(hbase,1)
(world,1)

RDD 持久化与容错(checkpoint)

RDD是不存储数据,所以每次行动算子触发任务都会从头开始计算,这样性能会降低,所以有了持久化机制

RDD 通过 cache 或者 persist 方法将前面的计算结果缓存,默认情况下会把数据以缓存在 JVM 的堆内存中。但是并不是这两个方法被调用时立即缓存,而是触发后面的 action 算 子时,该 RDD 将会被缓存在计算节点的内存中,并供后面重用。

RDD cache() & persist() 方法缓存

cache缓存的底层源码就是调用persist() 方法,只不过此时的存储级别是默认的仅内存存储MEMORY_ONLY

/**
   * Persist this RDD with the default storage level (`MEMORY_ONLY`).
   */
  def cache(): this.type = persist()

/**
   * Persist this RDD with the default storage level (`MEMORY_ONLY`).
   */
  def persist(): this.type = persist(StorageLevel.MEMORY_ONLY)

代码如下(示例):

val sparConf = new SparkConf().setMaster("local").setAppName("Persist")
val sc = new SparkContext(sparConf)

val list = List("Hello Scala")

val rdd = sc.makeRDD(list)

val flatRDD = rdd.flatMap(_.split(" "))

val mapRDD = flatRDD.map(word=>{
    println("@@@@@@@@@@@@")
    (word,1)
})
// cache默认持久化的操作,只能将数据保存到内存中,如果想要保存到磁盘文件,需要更改存储级别
//mapRDD.cache()

// 持久化操作必须在行动算子执行时完成的。
//        mapRDD.persist(StorageLevel.DISK_ONLY)

val reduceRDD: RDD[(String, Int)] = mapRDD.reduceByKey(_+_)
reduceRDD.collect().foreach(println)
println("**************************************")
val groupRDD = mapRDD.groupByKey()
groupRDD.collect().foreach(println)

sc.stop()

结果:

@@@@@@@@@@@@
@@@@@@@@@@@@
(Hello,1)
(Scala,1)
**************************************
@@@@@@@@@@@@
@@@@@@@@@@@@
(Hello,CompactBuffer(1))
(Scala,CompactBuffer(1))

如果未调用cache()或persist()方法,则在第一次reduceByKey并触发任务后,mapRDD不会保存数据,而在第二次groupByKey并触发任务时,需要重新计算rdd,所以mapRDD会重新进行它的逻辑计算,所以“@@@@@@@@@@@@”会重新打印
如果调用cache()或persist()方法,那么第二次就无需重新计算,此时控制台打印的结果为:

@@@@@@@@@@@@
@@@@@@@@@@@@
(Hello,1)
(Scala,1)
**************************************
(Hello,CompactBuffer(1))
(Scala,CompactBuffer(1))

此外可以调用persist()更改存储级别,如下:

object StorageLevel {
val NONE = new StorageLevel(false, false, false, false)
val DISK_ONLY = new StorageLevel(true, false, false, false)
val DISK_ONLY_2 = new StorageLevel(true, false, false, false, 2)
val MEMORY_ONLY = new StorageLevel(false, true, false, true)
val MEMORY_ONLY_2 = new StorageLevel(false, true, false, true, 2)
val MEMORY_ONLY_SER = new StorageLevel(false, true, false, false)
val MEMORY_ONLY_SER_2 = new StorageLevel(false, true, false, false, 2)
val MEMORY_AND_DISK = new StorageLevel(true, true, false, true)
val MEMORY_AND_DISK_2 = new StorageLevel(true, true, false, true, 2)
val MEMORY_AND_DISK_SER = new StorageLevel(true, true, false, false)
val MEMORY_AND_DISK_SER_2 = new StorageLevel(true, true, false, false, 2)
val OFF_HEAP = new StorageLevel(true, true, true, false, 1)

在这里插入图片描述

RDD CheckPoint 检查点

  1. 所谓的检查点其实就是通过将 RDD 中间结果写入磁盘,即需要落盘,需要指定检查点保存路径,并且会独立执行作业
  2. 检查点路径保存的文件,当作业执行完毕后,不会被删除
  3. 由于血缘依赖过长会造成容错成本过高,这样就不如在中间阶段做检查点容错,如果检查点之后有节点出现问题,可以从检查点开始重做血缘,减少了开销

代码如下(示例):

val sparConf = new SparkConf().setMaster("local").setAppName("Persist")
val sc = new SparkContext(sparConf)
sc.setCheckpointDir("cp") // checkpoint保存的目录

val list = List("Hello Scala")

val rdd = sc.makeRDD(list)

val flatRDD = rdd.flatMap(_.split(" "))

val mapRDD = flatRDD.map(word=>{
    println("@@@@@@@@@@@@")
    (word,1)
})

// 为了保证数据安全,所以一般情况下,会独立执行作业
// 即除了collect触发任务runJob外,checkpoint的底层也会触发runJob,而此时触发任务是为了将数据写入磁盘
mapRDD.checkpoint()

val reduceRDD: RDD[(String, Int)] = mapRDD.reduceByKey(_+_)
reduceRDD.collect().foreach(println)
println("**************************************")
val groupRDD = mapRDD.groupByKey()
groupRDD.collect().foreach(println)

sc.stop()

结果:
可以发现 @@@@@@@@@@@@ 被执行了4次,collect两次,checkpoint2次,因为checkpoint会独立执行作业

@@@@@@@@@@@@
@@@@@@@@@@@@
@@@@@@@@@@@@
@@@@@@@@@@@@
(Hello,1)
(Scala,1)
**************************************
(Hello,CompactBuffer(1))
(Scala,CompactBuffer(1))

为了能够提高效率,一般情况下,是需要和cache联合使用,即在设置checkpoint前,添加cache缓存,那么此时checkpoint就不会重头再来一遍,会直接从内存中读取数据,此时@@@@@@@@@@@@只会被打印2次

mapRDD.cache()
mapRDD.checkpoint()

cache& persist & checkpoint 三者的区别与小结

cache :

  1. 将数据临时存储在内存中进行数据重用,会在血缘关系中添加新的依赖,但因为保存在内存中,所以如果内存不够会造成数据丢失,可靠性低
  2. 一旦出现问题,可以重头读取数据
  3. cache缓存的底层源码就是调用persist() 方法

persist :

  1. 将数据临时存储在磁盘文件中进行数据重用
  2. 涉及到磁盘IO,性能较低,但是数据安全
  3. 如果作业执行完毕,临时保存的数据文件就会被删除丢失

checkpoint :

  1. 将数据长久地保存在磁盘文件中进行数据重用,例如储在 HDFS 等容错、高可用的文件系统,可靠性高
  2. 涉及到磁盘IO,性能较低,但是数据安全
  3. 为了保证数据安全,所以一般情况下,会独立执行作业,但这样会影响性能
  4. 所以为了能够提高效率,一般情况下,是需要和cache联合使用,因为这样checkpoint 的 job 只需从 Cache 缓存中读取数据即可
  5. 执行过程中,会切断血缘关系,重新建立新的血缘关系
  6. checkpoint等同于改变数据源

对于cache中会在血缘关系中添加新的依赖和checkpoint中会切断血缘关系,重新建立新的血缘关系,可以通过以下示例来理解:

代码如下(示例):

val sparConf = new SparkConf().setMaster("local").setAppName("Persist")
val sc = new SparkContext(sparConf)

sc.setCheckpointDir("cp") // checkpoint保存的目录

val list = List("Hello Scala", "Hello Spark")

val rdd = sc.makeRDD(list)

val flatRDD = rdd.flatMap(_.split(" "))

val mapRDD = flatRDD.map(word=>{
    (word,1)
})
println("触发任务前rdd的血缘关系")
mapRDD.cache()
//        mapRDD.checkpoint()
println(mapRDD.toDebugString)
val reduceRDD: RDD[(String, Int)] = mapRDD.reduceByKey(_+_)
reduceRDD.collect().foreach(println)
println("**************************************")
println("触发任务后rdd的血缘关系")
println(mapRDD.toDebugString)

sc.stop()

cache() => 结果:
可以发现前后多了一个依赖关系,即在血缘关系中添加新的依赖
CachedPartitions: 1; MemorySize: 368.0 B; ExternalBlockStoreSize: 0.0 B; DiskSize: 0.0 B

触发任务前rdd的血缘关系
(1) MapPartitionsRDD[2] at map at RDD_Persist.scala:32 [Memory Deserialized 1x Replicated]
 |  MapPartitionsRDD[1] at flatMap at RDD_Persist.scala:30 [Memory Deserialized 1x Replicated]
 |  ParallelCollectionRDD[0] at makeRDD at RDD_Persist.scala:28 [Memory Deserialized 1x Replicated]
(Spark,1)
(Hello,2)
(Scala,1)
**************************************
触发任务后rdd的血缘关系
(1) MapPartitionsRDD[2] at map at RDD_Persist.scala:32 [Memory Deserialized 1x Replicated]
 |       CachedPartitions: 1; MemorySize: 368.0 B; ExternalBlockStoreSize: 0.0 B; DiskSize: 0.0 B
 |  MapPartitionsRDD[1] at flatMap at RDD_Persist.scala:30 [Memory Deserialized 1x Replicated]
 |  ParallelCollectionRDD[0] at makeRDD at RDD_Persist.scala:28 [Memory Deserialized 1x Replicated]

checkpoint() => 结果:
可以发现前后依赖关系有较大改变,即在会切断血缘关系,重新建立新的血缘关系,其实也可以理解为checkpoint更改了数据源

触发任务前rdd的血缘关系
(1) MapPartitionsRDD[2] at map at Spark06_RDD_Persist.scala:32 []
 |  MapPartitionsRDD[1] at flatMap at Spark06_RDD_Persist.scala:30 []
 |  ParallelCollectionRDD[0] at makeRDD at Spark06_RDD_Persist.scala:28 []
(Spark,1)
(Hello,2)
(Scala,1)
**************************************
触发任务后rdd的血缘关系
(1) MapPartitionsRDD[2] at map at Spark06_RDD_Persist.scala:32 []
 |  ReliableCheckpointRDD[4] at collect at Spark06_RDD_Persist.scala:40 []

总结

本文仅作知识点的记录,欢迎大家指错,一起探讨~~~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值