Spark——(序列化,RDD依赖关系,RDD持久化/缓存,RDD容错机制Checkpoint)

序列化

  1. 自定义一些对RDD的操作时,初始化工作是在Driver端进行的,实际运行程序是在Executor端进行的,此时,涉及到了进程之间的通信,需要进行序列化。可以简单的认为SparkContext代表Driver。

  2. 普通的类不具备序列化能力,但也有相应的解决方案

  3. 如果在方法、函数的定义中引用了不可序列化的对象,也会导致任务不能序列化,延迟创建的解决方案较为简单,适用性广

    import org.apache.spark.{SparkConf, SparkContext}
    
    class MyClass1(x: Int){
      val num: Int = x
    }
    
    case class MyClass2(num: Int)
    
    class MyClass3(x: Int) extends Serializable {
      val num: Int = x
    }
    
    object SerializableDemo {
      def main(args: Array[String]): Unit = {
        // 初始化
        val conf = new SparkConf().setAppName(this.getClass.getCanonicalName.init).setMaster("local[*]")
        val sc = new SparkContext(conf)
        sc.setLogLevel("WARN")
    
        val o1 = new MyClass1(8)
    //    println(s"o1.num = ${o1.num}")
    
        val rdd1 = sc.makeRDD(1 to 20)
        // 方法
        def add1(x: Int) = x + 100
        // 函数
        val add2 = add1 _
    
        // 函数、方法都具备序列化和反序列化的能力
    //    rdd1.map(add1(_)).foreach(println)
    //    println("****************************************************")
    //    rdd1.map(add2(_)).foreach(println)
    
        val object1 = new MyClass1(20)
        val i = 20
        // rdd1.map(x => object1.num + x).foreach(println)
    
        // 解决方案一:使用case class
        val object2 = MyClass2(20)
        // rdd1.map(x => object2.num + x).foreach(println)
    
        // 解决方案二:MyClass1 实现 Serializable 接口
        val object3 = new MyClass3(20)
        rdd1.map(x => object3.num + x).foreach(println)
    	
    	// 解决方案三:延迟创建
    	println("解决方案三:延迟创建")
    	lazy val object4 = new MyClass1(20)
    	rdd1.map(x => object4.num + x).foreach(println)
        sc.stop()
      }
    }
    

RDD依赖关系

  1. RDD只支持粗粒度转换,即在大量记录上执行的单个操作。将创建RDD的一系列Lineage(血统)记录下来,以便恢复丢失的分区。

  2. RDD的Lineage会记录RDD的元数据信息和转换行为,当该RDD的部分分区数据丢失时,可根据这些信息来重新运算和恢复丢失的数据分区。
    在这里插入图片描述

  3. RDD和它依赖的父RDD(s)的关系有两种不同的类型,即窄依赖(narrowdependency)和宽依赖(wide dependency)。 窄依赖:1:1 或 n:1。宽依赖:n:m;意味着有 shuffle
    在这里插入图片描述

  4. 依赖有2个作用:其一用来解决数据容错;其二用来划分stage。

  5. DAG(Directed Acyclic Graph) 有向无环图。原始的RDD通过一系列的转换就就形成了DAG,根据RDD之间的依赖关系的不同将DAG划分成不同的Stage:

    1. 对于窄依赖,partition的转换处理在Stage中完成计算
    2. 对于宽依赖,由于有Shuffle的存在,只能在parent RDD处理完成后,才能开始接下来的计算
    3. 宽依赖是划分Stage的依据
      在这里插入图片描述
  6. RDD任务切分中间分为:Driver programe、Job、Stage(TaskSet)和Task

    1. Driver program:初始化一个SparkContext即生成一个Spark应用
    2. Job:一个Action算子就会生成一个Job
    3. Stage:根据RDD之间的依赖关系的不同将Job划分成不同的Stage,遇到一个宽依赖则划分一个Stage
    4. Task:Stage是一个TaskSet,将Stage划分的结果发送到不同的Executor执行即为一个Task
    5. Task是Spark中任务调度的最小单位;每个Stage包含许多Task,这些Task执行的计算逻辑相同的,计算的数据是不同的
    6. 注意:Driver programe->Job->Stage-> Task每一层都是1对n的关系。

RDD持久化/缓存

  1. 缓存是将计算结果写入不同的介质,用户定义可定义存储级别(存储级别定义了缓存存储的介质,目前支持内存、堆外内存、磁盘);

  2. 通过缓存,Spark避免了RDD上的重复计算,RDD持久化或缓存是Spark构建迭代式算法和快速交互式查询的关键因素;

  3. Spark速度非常快的原因之一,就是在内存中持久化(或缓存)一个数据集。当持久化一个RDD后,每一个节点都将把计算的分片结果保存在内存中,并在对此数据集(或者衍生出的数据集)进行的其他动作(Action)中重用。这使得后续的动作变得更加迅速;

  4. 使用persist()方法对一个RDD标记为持久化。之所以说“标记为持久化”,是因为出现persist()语句的地方,并不会马上计算生成RDD并把它持久化,而是要等到遇到第一个行动操作触发真正计算以后,才会把计算结果进行持久化;
    在这里插入图片描述

  5. 通过persist()或cache()方法可以标记一个要被持久化的RDD,持久化被触发,RDD将会被保留在计算节点的内存中并重用;

  6. 一般情况下,如果多个动作需要用到某个 RDD,而它的计算代价又很高,那么就应该把这个 RDD 缓存起来;缓存有可能丢失,或者存储于内存的数据由于内存不足而被删除。RDD的缓存的容错机制保证了即使缓存丢失也能保证计算的正确执行。通过基于RDD的一系列的转换,丢失的数据会被重算。RDD的各个Partition是相对独立的,因此只需要计算丢失的部分即可,并不需要重算全部Partition。

  7. persist()的参数可以指定持久化级别参数;使用cache()方法时,会调用persist(MEMORY_ONLY),即:

    cache() == persist(StorageLevel.Memeory_ONLY)
    
  8. 使用unpersist()方法手动地把持久化的RDD从缓存中移除;cache RDD 以 分区为单位;程序执行完毕后,系统会清理cache数据;

存储级别描述
MEMORY_ONLY将RDD 作为反序列化的对象存储JVM 中。如果RDD不能被内存装下,一些分区将不会被缓存并且在需要的时候被重新计算。 默认的缓存级别
MEMORY_AND_DISK将RDD 作为反序列化的的对象存储在JVM 中。如果RDD不能被与内存装下,超出的分区将被保存在硬盘上,并且在需要时被读取
MEMORY_ONLY_SER将RDD 作为序列化的的对象进行存储(每一分区一个字节数组)。 通常来说,这比将对象反序列化的空间利用率更高,读取时会比较占用CPU
MEMORY_AND_DISK_SER与MEMORY_ONLY_SER 相似,但是把超出内存的分区将存储在硬盘上而不是在每次需要的时候重新计算
DISK_ONLY只将RDD 分区存储在硬盘上
DISK_ONLY_2等带2的与上述的存储级别一样,但是将每一个分区都复制到集群的两个结点上

RDD容错机制Checkpoint

  1. Spark中对于数据的保存除了持久化操作之外,还提供了检查点的机制;检查点本质是通过将RDD写入高可靠的磁盘,主要目的是为了容错。检查点通过将数据写入到HDFS文件系统实现了RDD的检查点功能。
  2. Lineage过长会造成容错成本过高,这样就不如在中间阶段做检查点容错,如果之后有节点出现问题而丢失分区,从做检查点的RDD开始重做Lineage,就会减少开销。
  3. cache 和 checkpoint 是有显著区别的,缓存把 RDD 计算出来然后放在内存中,但是 RDD 的依赖链不能丢掉, 当某个点某个 executor 宕了,上面 cache 的RDD就会丢掉, 需要通过依赖链重放计算。不同的是,checkpoint 是把 RDD 保存在 HDFS中,是多副本可靠存储,此时依赖链可以丢掉,所以斩断了依赖链。
  4. 当DAG中的Lineage过长,如果重算,则开销太大时,以及在宽依赖上做 Checkpoint 获得的收益更大时,很适合使用检查点机制,与cache类似 checkpoint 也是 lazy 的。
  5. checkpoint的文件作业执行完毕后不会被删除
    val rdd1 = sc.parallelize(1 to 100000)
    
    // 设置检查点目录
    sc.setCheckpointDir("/tmp/checkpoint")
    val rdd2 = rdd1.map(_*2)
    rdd2.checkpoint
    
    // checkpoint是lazy操作
    rdd2.isCheckpointed
    
    // checkpoint之前的rdd依赖关系
    rdd2.dependencies(0).rdd
    rdd2.dependencies(0).rdd.collect
    
    // 执行一次action,触发checkpoint的执行
    rdd2.count
    rdd2.isCheckpointed
    
    // 再次查看RDD的依赖关系。可以看到checkpoint后,RDD的lineage被截断,变成从checkpointRDD开始
    rdd2.dependencies(0).rdd
    rdd2.dependencies(0).rdd.collect
    
    //查看RDD所依赖的checkpoint文件
    rdd2.getCheckpointFile
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值