spark将rdd转为string_RDD的检查点(Checkpoint)实现原理

本文详细探讨了Spark中的RDD Checkpoint机制,包括基本概念、分类、与cache/persist的区别,以及可靠和本地checkpointing的实现原理。RDD Checkpoint用于截断血缘,确保在故障时能恢复数据,而本地checkpointing则牺牲了容错性以提高性能。通过设置存储路径和调用checkpoint()函数,RDD的数据会被持久化到外部存储或Executor本地。
摘要由CSDN通过智能技术生成

本文介绍两类RDD的checkpoint的作用和原理,并对checkpointing的实现进行分析。

RDD Checkpoint的基本概念

RDD可能经过多个转换(transformations)操作,导致RDD的血缘不断增长。Spark提供了一种将RDD进行稳定持久化存储的方法,当集群发生故障时,Spark不需要从头开始计算RDD的分区。这个特征被称为checkpointing。

checkpointing是一个截断RDD血缘,并把RDD持久化到外部可靠的文件系统(例如:HDFS,S3)或本地文件系统的过程。

由于checkpointing会把RDD的数据写到Spark外部,因此当Spark应用结束时,checkpointing的数据会继续存在。另外,checkpointing将会占用更多的外部存储空间,它的写操作将会消耗系统资源,所以它会比persisting(持久化)操作要慢。但是,checkpointing不会使用任何Spark内存,而且若Spark worker失败,也不会进行重新计算。

Checkpointing的分类

有两类Checkpointing:

  • reliable checkpointing(可靠的checkpointing)

这类checkpointing将RDD数据写入到可靠的存储系统中,比如:HDFS,S3等。它允许Spark driver(驱动程序)在当前计算的状态下失败时重新启动。

  • local checkpointing(本地checkpointing)

local checkpointing在性能和容错性之间进行权衡,它会跳过把数据保存到可靠且容错性好的存储系统的步骤。把数据写入每个Executor的本地临时存储。这对于RDD构建需要经常截断的长血缘(long lineage)(例如GraphX或spark streaming等)的应用非常有用。

checkpoint和cache/persist的比较

  • checkpoint把数据保存到可靠的外部存储系统,而persist/cache使用spark集群的存储资源。
  • persist/cache可以指定存储级别,可以把数据保存到内存或磁盘,而checkpoint只能持久化保存。
  • persist/cache的性能更快,而checkpoint性能较低。
  • persist/cache的数据可能丢失,而checkpoint的数据保存在可靠的外部存储系统,一般不会丢失。
  • persist/cache会保存RDD的血缘,而checkpoint会截断RDD的血缘。
  • 当Spark应用终止时persist/cache会删除缓存的RDD数据,而checkpoint的RDD数据却不会被删除,这样checkpoint的数据可以在不同Spark应用之间使用。

checkpointing的使用

为了更好的理解checkpoint的实现,我们先看一下使用checkpoint的过程:

  1. 设置checkpoint存储路径,并通过RDD的checkpoint操作来保存RDD数据
 sc.setCheckpointDir("hdfs://hadoop3:7078/data/checkpoint")
 val rdd1 = sc.parallelize(Array((1,2),(3,4),(5,6),(7,8)),3)
 rdd1.checkpoint()
 ​
 //注意:要action才能触发checkpoint
 rdd1.take(1)

2. 使用checkpoint保存的数据
需要使用checkpoint的保存的数据时,可以通过SparkContext#checkpointFile函数来读取。

  • 把下面的程序打成jar包,比如叫:dospark.jar
 package org.apache.spark
 ​
 import org.apache.spark.rdd.RDD
 ​
 object RDDUtilsInSpark {
   def getCheckpointRDD[T](sc:SparkContext, path:String) = {
     //path要到part-000000的父目录
     val result : RDD[Any] = sc.checkpointFile(path)
     result.asInstanceOf[T]
   }
 }
  • 使用工具类读取checkpoint的RDD
 import org.apache.spark.rdd.RDD
 import org.apache.spark.RDDUtilsInSpark
 ​
 val checkpointPath="hdfs://hadoop3:7078/data/checkpoint/7d020f7b-9122-4247-ba2d-80b8277a1022/rdd-0"
 ​
 val rdd : RDD[(Int, Int)]= RDDUtilsInSpark.getCheckpointRDD(sc, checkpointPath)
 println(rdd.count())
 rdd.collect().foreach(println)

从上面的例子中可以看到,spark中的checkpoint的使用涉及到以下几个函数,下面通过分析这几个函数的实现过程来介绍spark的checkpoint的实现原理。

checkpointing的实现

设置checkpoint的存储路径

在使用checkpoint时首先要设置一个存储的路径。路径的设置是通过SparkContext#setCheckpointDir函数来实现的。该函数的实现要点:

  • 若spark是以集群模式运行,给定的checkpoint目录必须是HDFS目录
  • RDD的数据并非保存在参数指定的目录下,而是会在参数指定的目录后添加一个UUID的子目录
  • 若指定的目录不存在,则会创建该目录

该函数的实现代码如下:

 def setCheckpointDir(directory: String) {
     // 若不是本地模式,而指定了本地路径,则会记录一个警告日志
     if (!isLocal && Utils.nonLocalPaths(directory).isEmpty) {
       ... 
     }
     // 若是集群模式,目录必须是hdfs目录
     checkpointDir = Option(directory).map { dir =>
       // 会在给定目录后面添加一个UUID的字符串的子目录
       val path = new Path(dir, UUID.randomUUID().toString)
       val fs = path.getFileSystem(hadoopConfiguration)
       // 若目录不存在,则创建该目录
       fs.mkdirs(path)
       fs.getFileStatus(path).getPath.toString
     }
   }

checkpoint()函数的实现

在RDD的抽象类中定义了checkpoint()函数,该函数的实现要点有:

  • 检查是否已经设置了checkpoint目录,若没有设置则抛出异常
  • 若该RDD已经被checkpoint过了,则什么都不做
  • 返回一个ReliableRDDCheckpointData对象,并赋给RDD的checkpointData变量,该对象保存了需要进行checkpoint的所有信息。下一次再执行checkpoint()时,会根据ReliableRDDCheckpointData对象的值来判断该RDD是否已经被checkpoint了。

该函数的实现代码如下:

   def checkpoint(): Unit = RDDCheckpointData.synchronized {
     if (context.checkpointDir.isEmpty) { //若没有设置checkpoint目录,不能进行checkpoint
       throw new SparkException("Checkpoint directory has not been set in the SparkContext")
     } else if (checkpointData.isEmpty) { //若该RDD已经checkpoint了,就什么也不做
       // 返回ReliableRDDCheckpointData对象
       checkpointData = Some(new ReliableRDDCheckpointData(this))
     }
   }

也就是说,checkpoint()函数只是返回了一个ReliableRDDCheckpointData对象,并设置了变量checkpointData。该函数并没有进行实际的RDD存储的动作,那么什么时候执行checkpoint动作,如何执行呢?下面继续分析。

RDD数据的存储:doCheckpoint()

从上面的例子中可以看出,执行rdd.checkpoint()这一句,不能触发RDD的存储的动作,而必须要加上一个action操作。那么,这是为什么呢?

我们来看一下action的执行过程,每个action操作都会调用runJob来触发任务的执行。注意到,在runJob函数中调用了rdd.doCheckpoint(),部分代码如下:

   def runJob[T, U: ClassTag](
       rdd: RDD[T],
       func: (TaskContext, Iterator[T]) => U,
       partitions: Seq[Int],
       resultHandler: (Int, U) => Unit): Unit = {
       ...
       rdd.doCheckpoint()
    }

可以看到,当调用RDD的action操作时,会触发doCheckpoint动作。我们来看一下doCheckpoint的具体执行过程:

1)doCheckpoint会检查是否已经设置了checkpoint的目录,若没有设置则什么都不做。若设置了checkpoint目录,则会根据checpointing的类型(ReliableCheckpointRDD或LocalCheckpointRDD)来调用完成数据的保存。

2)若是ReliableCheckpointRDD,则会把数据写到外部存储系统中。若是LocalCheckpointRDD,则会检查是否有缺失的分区,若有,则会提交Job来计算该分区的数据,此时数据被保存在Executor端的本地存储系统中。

3)接下来清除血缘,这个清除的操作其实就是删除掉依赖的RDD列表,从实现层面来说,其实就是把RDD的dependencies_变量置成null值。

checkpointing的实现小结

从以上分析可以得出以下几点结论:

  • 在进行可靠checkpoint操作时,首先要设置checkpoint的目录。
  • 通过RDD调用checkpoint()函数时,实际上并没有开始进行RDD的存储操作,而只是对路径进行了检查,并创建一个ReliableRDDCheckpointData对象,并赋值给:checkpointData变量。
  • 实际上执行RDD的checkpoint作是在执行完成了第一个action操作时进行的。
  • 若spark是非本地模式运行,则设置的checkpoint路径必须是hdfs的路径。

两类checkpointing的实现

前面已经介绍了,有两类checkpointing。从实现层面来讲,spark中定义了一个checkpointing的抽象类:RDDCheckpointData。并且每对象会和一种RDD相关联。有两个类继承了该抽象类,他们分别是:ReliableRDDCheckpointData和LocalRDDCheckpointData。这两个类分别对应checnpointing的两种类型,他们包含RDD checkpointing的所有信息,并定义了具体执行checkpointing动作的函数:doCheckpoint。

                                   RDDCheckpointData(抽象类)
                                         / /
                                         |  |
        ReliableRDDCheckpointData________|  |_______LocalRDDCheckpointData
              (可靠checkpoint)                          (本地checkpoint)

这两种类型的checkpoint分别对应该类型的RDD,checkpoint类型的RDD由一个抽象类CheckpointRDD来描述。有两个类实现了该抽象类:LocalCheckpointRDD和ReliableCheckpointRDD。

                                   CheckpointRDD(抽象类)
                                         / /
                                         |  |
            ReliableCheckpointRDD________|  |_______LocalCheckpointRDD
              (可靠checkpointRDD)                      (本地checkpointRDD)

下面介绍这两类checkpointing的具体实现原理。

可靠的checkpointing

前面的实战例子分析的就是”可靠checkpointing“。我们再回顾一下它的实现要点:

1)首先设置保存checkpoint数据的目录,若是集群模式,该目录是一个hdfs目录,若不存在则会创建该目录。

2)调用RDD.checkpoint()来标记一个需要checkpoint的RDD,但需要注意,必须在对该RDD执行任何任务之前调用此函数。

3)当触发该RDD的Job任务执行时(调用action函数),才会执行checkpoint动作,此时会切断RDD的血缘,然后保存该RDD的数据到外部存储中。

本地checkpointing

本地checkpointing是通过RDD#localCheckpoint函数来实现。该函数通过persist函数来标记该RDD为本地checkpointing(local checkpointing)。

localCheckpoint适用于希望截断RDD 血缘图(RDD linage graph),同时跳过访问外部可靠存储系统的系统消耗的场景。这对于具有需要定期截断的长血统(long linage)的RDD很有用,例如,GraphX。

需要注意的是:Local Checkpointing为了性能牺牲了容错性,特别是:本地检查点数据(checkpointed data)被写入执行器(Executor)中的临时本地存储系统而不是可靠的容错存储系统。其结果是,如果执行程序在计算过程中失败,则可能无法再访问检查点数据(checkpointed data),从而导致无法恢复失败的作业。

另外,Local Checkpointing不需要通过SparkContext#setCheckpointDir来设置checkpointing目录。

localCheckpoint函数的实现

RDD#localCheckpoint函数的实现主要是以下两句:

if (storageLevel == StorageLevel.NONE) {
  persist(LocalRDDCheckpointData.DEFAULT_STORAGE_LEVEL)
} else {
  persist(LocalRDDCheckpointData.transformStorageLevel(storageLevel), allowOverride = true)
}

从以上源码中可以看出其实现逻辑为:

1)检查RDD的storageLevel是否设置,若没有设置,则调用persist函数,使用默认的存储级别:StorageLevel.MEMORY_AND_DISK。

2)若RDD的storageLevel已经设置,则允许通过persist函数来修改storageLevel。会在现有的storageLevel上添加useDisk选项,也就是说若需要修改storageLevel,则必须要使用磁盘来保存数据。

可见,localCheckpoint函数不需要设置checkpoint路径,它是基于RDD#persist缓存函数来实现的。

执行本地checkpointing动作

当调用RDD#localCheckpoint会调用RDD#persist来设置存储级别,因为是本地checkpointing所以默认会加上useDisk的存储选项。

当触发一个action动作并提交job后,会调用RDD的doCheckpoint函数,此时会调用LocalRDDCheckpointData#doCheckpoint来进行实际的本地checkpointing操作。该操作实际上是检查RDD的分区是否都已经存在,若不存在,则运行一个job来计算该分区。代码实现如下:

  private[spark] class LocalRDDCheckpointData[T: ClassTag](@transient private val rdd: RDD[T])   extends RDDCheckpointData[T](rdd) with Logging {
	// 确保该RDD被全部缓存了,这样分区才可以在后来恢复。
  protected override def doCheckpoint(): CheckpointRDD[T] = {
    val level = rdd.getStorageLevel
    ...
    // 查找丢失的分区id
    val missingPartitionIndices = rdd.partitions.map(_.index).filter { i =>
      !SparkEnv.get.blockManager.master.contains(RDDBlockId(rdd.id, i))
    }
    // 发起job计算丢失的分区数据块
    if (missingPartitionIndices.nonEmpty) {
      rdd.sparkContext.runJob(rdd, action, missingPartitionIndices)
    }
		// 返回本地checkpointRDD对象
    new LocalCheckpointRDD[T](rdd)
  }
  ...
}

计算完成后数据块就保存到Executor执行的本地存储系统中。

注意,虽然localCheckpoint是基于RDD的persist函数实现的,但还是会截断RDD的血缘。

另外,在使用本地checkpointing时,启用动态分配并不安全,因为动态分配会删除Executor及其缓存的块。如果需要同时使用动态分配和本地checkpointing这两个功能,建议将spark.dynamicAllocation.cachedExecutorIdleTimeout的值设置得大一些。

本地checkpointing测试

根据"RDD的血缘(Lineage)"一节的例子,我们可以看一下localCheckpoint的具体表现:

scala> rdd20.toDebugString
res0: String =
(10) UnionRDD[8] at union at <console>:36 []
 |   UnionRDD[7] at union at <console>:36 []
 |   UnionRDD[6] at union at <console>:36 []
 |   CartesianRDD[2] at cartesian at <console>:28 []
 |   ParallelCollectionRDD[0] at parallelize at <console>:24 []
 |   ParallelCollectionRDD[1] at parallelize at <console>:24 []
 |   MapPartitionsRDD[3] at map at <console>:26 []
 |   ParallelCollectionRDD[0] at parallelize at <console>:24 []
 |   ZippedPartitionsRDD2[4] at zip at <console>:28 []
 |   ParallelCollectionRDD[0] at parallelize at <console>:24 []
 |   ParallelCollectionRDD[1] at parallelize at <console>:24 []
 |   MapPartitionsRDD[5] at keyBy at <console>:26 []
 |   ParallelCollectionRDD[1] at parallelize at <console>:24 []
 
// 标记localcheckpointing,注意:不需要设置目录,数据保存在executor端的本地
scala> rdd20.localCheckpoint
res1: rdd20.type = UnionRDD[8] at union at <console>:36
// 触发localcheckpoit的执行
scala> rdd20.count()
res4: Long = 130  
// 查看血缘
scala> rdd20.toDebugString
res5: String =
(10) UnionRDD[8] at union at <console>:36 [Disk Memory Deserialized 1x Replicated]
 |        CachedPartitions: 10; MemorySize: 5.0 KB; ExternalBlockStoreSize: 0.0 B; DiskSize: 0.0 B
 |   LocalCheckpointRDD[9] at count at <console>:39 [Disk Memory Deserialized 1x Replicated]

可以看到rdd20的分区保存到了Executor端的本地,而且数据同时保存到了磁盘和内存中。这是localCheckpoint的默认存储级别。

总结

本文分析了两类RDD checkpointing的基本概念和实现原理。

可靠checkpoint会把数据块保存到外部可靠存储系统中,这样能保证数据块不会丢失。

而本地checkpoint则把数据保存到Executor的本地,但Executor本地的数据可能会丢失,所以这种checkpoint方式不太稳定,可能导致任务失败。但在资源充足的情况下,可以使用这种方式来提升性能。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值