【读懂面经中的源码】SPARK源码解析——checkpoint过程

本次为各位同学准备的是Spark高频八股–Spark的checkpoint过程~

创作不易!多多支持!

面筋

checkpoint是什么

  1. Spark生产环境下经常会面临Transformations的RDD非常多或者RDD计算非常复杂,耗时事件特别长,这个时候就需要考虑任务执行过程中的数据持久化

  2. Spark擅长多步骤迭代,同时擅长基于Job的服用,如果能够对曾经计算的过程产生的数据进行服用,就可以极大提高效率

  3. 如果采用persist把数据放在内存中,虽然快速,但不可靠。如果放在磁盘上,不完全可靠,因为磁盘损坏等也会发生

  4. checkpoint 的产生就是为了相对而言更加可靠的持久化数据,在 Checkpoint 可以指定把数据放在本地并且是多副本的方式,但是在正常生产环境下放在 HDFS 上,这就天然的借助HDFS 高可靠的特征来完成最大化的可靠的持久化数据的方式。

  5. checkpoint 是为了最大程度保证绝对可靠的复用 RDD 计算数据的 Spark 的高级功能,通过 checkpoint 可以把数据持久化到 HDFS 上来保证数据的最大程度的安全性

  6. checkpoint 就是针对整个RDD 计算链条中特别需要数据持久化的环节(后面会反覆使用当前环节的RDD) 开始基于HDFS 等的数据持久化复用策略,通过对 RDD 启动 Checkpoint 机制来实现容错和高可用

在这里插入图片描述
图片来源:公众号旧时光大数据

checkpoint的过程主要如下:

  1. 通过SparkContext设置checkpoint数据保存的目录,RDD调用checkpoint方法,生产RDDCheckpointData,当RDD上运行一个Job后,就会立刻触发RDDCheckpointData中的checkpoint方法,在内部调用doCheckpoint;
  2. 然后调用ReliableRDDCheckpointDAata的doCheckpoint;ReliableCheckpointRDD的writeRDDToCheckpointDirectory的调用;
  3. 在writeRDDToCheckpointDirectory方法内部会触发runJob,来执行把当前的RDD中的数据写到已经设置的checkpoint的目录,同时产生ReliableCheckpointRDD实例
  4. checkpoint保存在HDFS中,具有多个副本;persist保存在内存或磁盘中。在Job作业调度的时候checkpoint沿着finalRDD的血统关系从后往前回溯向上查找,查找哪些RDD层标记要进行checkpoint,标记为checkpointlnProgress,一旦进行checkpoint,RDD所有父RDD就会被清空

checkpoint和persist的区别

  1. cache/persist都是为了提速,如果步骤过长,使用这两者,就不需要从头算起
  2. checkpoint:更多是为了高可用。其核心是HDFS的replication,利用HDFS的多副本错略,即使一个节点崩了也不会影响checkpoint的数据使用。这就是天然的借助了HDFS高容错性、高可靠性的特征来完成最大化的可靠的持久化数据的方式
  3. persist只是将数据保存在BlockManager中,但是RDD的lineage是不会变化的。checkpoint完毕之后,RDD已经没有之前的lineage血缘关系,而只有一个强行为其设置的checkpointRDD,也就是说checkpoint之后,lineage发生了改变
  4. rdd.persist(StorageLevel.DISK_ONLY)虽然可以将 RDD 的 partition 持久化到磁盘,但该 partition 由 blockManager 管理。一旦 driver program 执行结束,也就是 executor 所在进程 CoarseGrainedExecutorBackend stop,blockManager 也会 stop,被 cache 到磁盘上的 RDD 也会被清空(整个 blockManager 使用的 local 文件夹被删除)。而 checkpoint 将 RDD 持久化到 HDFS 或本地文件夹,如果不被手动 remove 掉,是一直存在的,也就是说可以被下一个 driver program 使用,而 cached RDD 不能被其他 dirver program 使用。e/p。.ersist 可以说一方面是为

checkpoint的调用

val reducedPairs = pairs.reduceByKey(_+_).setName("reduceByKey操作")
reducedPairs.persist(StorageLevel.MEMORY_AND_DISK)
reducedPairs.checkpoint()

如上述reducePairs.checkpoint操作,表示调用了checkpoint,它的方法详细如下

/**
 * Mark this RDD for checkpointing. It will be saved to a file inside the checkpoint
 * directory set with `SparkContext#setCheckpointDir` and all references to its parent
 * RDDs will be removed. This function must be called before any job has been
 * executed on this RDD. It is strongly recommended that this RDD is persisted in
 * memory, otherwise saving it on a file will require recomputation.
 * 将此 RDD 标记为检查点。它将被保存到使用 SparkContext#setCheckpointDir 设置的检查点目录中的文件中,
 * 并且对其父 RDD 的所有引用都将被删除。在此 RDD 上执行任何作业之前,必须调用此函数。
 * 强烈建议将此 RDD 持久保存在内存中,否则将其保存在文件中将需要重新计算。
 */
def checkpoint(): Unit = RDDCheckpointData.synchronized {
  // NOTE: we use a global lock here due to complexities downstream with ensuring
  // children RDD partitions point to the correct parent partitions. In the future
  // we should revisit this consideration.
  // 注意:我们在这里使用全局锁,原因是下游的复杂性:子RDD分区指向正确的父分区,未来我们应该重新考虑这个问题
  if (context.checkpointDir.isEmpty) {
    throw new SparkException("Checkpoint directory has not been set in the SparkContext")
  } else if (checkpointData.isEmpty) {
    checkpointData = Some(new ReliableRDDCheckpointData(this))
  }
}

代码主要显示了如果设置了checkpoint保存路径,那么就创建ReliableRDDCheckpointData

回到SparkContext.runJob(),这时候是启动了任务程序嘛,这一部分可以查阅我的另外一篇文章【读懂面经中的源码】SPARK源码解析——Spark任务提交、调度、执行过程_番茄薯仔的博客-CSDN博客

// dagScheduler出现了,可以切分stage
dagScheduler.runJob(rdd, cleanedFunc, partitions, callSite, resultHandler, localProperties.get)
progressBar.foreach(_.finishAll())
rdd.doCheckpoint()

可以看到dagScheduler.runJob()表示启动任务,在启动完任务之后,就开始rdd.doCheckpoint()。表明通过保存此 RDD 来执行此 RDD 的检查点。它是在使用此 RDD 的作业完成后调用的(因此 RDD 已具体化并可能存储在内存中)。doCheckpoint() 在父 RDD 上递归调用。

private[spark] def doCheckpoint(): Unit = {
  RDDOperationScope.withScope(sc, "checkpoint", allowNesting = false, ignoreParent = true) {
    if (!doCheckpointCalled) {
      doCheckpointCalled = true
      if (checkpointData.isDefined) {
        if (checkpointAllMarkedAncestors) {
          // TODO We can collect all the RDDs that needs to be checkpointed, and then checkpoint
          // them in parallel.
          // Checkpoint parents first because our lineage will be truncated after we
          // checkpoint ourselves
          dependencies.foreach(_.rdd.doCheckpoint())
        }
        checkpointData.get.checkpoint()
      } else {
        dependencies.foreach(_.rdd.doCheckpoint())
      }
    }
  }
}

其中 dependencies.foreach(_.rdd.doCheckpoint())表示的就是递归的调用checkpoint,直至没有依赖了,因为当采用了checkpoint之后,lineage血缘就切断了,无法回溯之前的RDD算子了。

然后进入checkpointData.get.checkpoint()的方法内部

final def checkpoint(): Unit = {
  // Guard against multiple threads checkpointing the same RDD by
  // atomically flipping the state of this RDDCheckpointData
  RDDCheckpointData.synchronized {
    if (cpState == Initialized) {
      cpState = CheckpointingInProgress
    } else {
      return
    }
  }

  val newRDD = doCheckpoint()

  // Update our state and truncate the RDD lineage
  // 更新我们的状态和截断RDD的血统
  RDDCheckpointData.synchronized {
    cpRDD = Some(newRDD)
    cpState = Checkpointed
    rdd.markCheckpointed()
  }
}

这里的doCheckpoint()方法有两个实现,分别是LocalRDDCheckpointData以及ReliableRDDCheckpointData。

  • LocalRDDCheckpointData是在 Spark 缓存层之上实现的检查点实现。local checkpointing没有将RDD数据保存到可靠且容错的存储的昂贵步骤来牺牲容错性能。相反,数据将写入每个执行程序中的本地临时块存储。这对于RDD建立需要经常截断的长谱系的用例很有用(例如GraphX)。
  • ReliableRDDCheckpointData就是RDD写到可信存储的实现,比如HDFS等

ReliableRDDCheckpointData在doCheckpoint()方法中执行如下操作

protected override def doCheckpoint(): CheckpointRDD[T] = {
  // doCheckpoint,在生产过程中会导致ReliableCheckpointRDD的writeRDDToCheckpointDirectory的调用,
  // 而在writeRDDToCheckpointDirectory方法内部,会触发runJob来执行把当前的RDD中的数据写到checkpoint的目录中,
  // 同时会产生ReliableCheckpointRDD实例
  val newRDD = ReliableCheckpointRDD.writeRDDToCheckpointDirectory(rdd, cpDir)

  // Optionally clean our checkpoint files if the reference is out of scope
  // 如果引用超出范围,则可选地清理检查点文件
  if (rdd.conf.get(CLEANER_REFERENCE_TRACKING_CLEAN_CHECKPOINTS)) {
    rdd.context.cleaner.foreach { cleaner =>
      cleaner.registerRDDCheckpointDataForCleanup(newRDD, rdd.id)
    }
  }

  logInfo(s"Done checkpointing RDD ${rdd.id} to $cpDir, new parent is RDD ${newRDD.id}")
  newRDD
}

其中,调用了如下 val newRDD = ReliableCheckpointRDD.writeRDDToCheckpointDirectory(rdd, cpDir)方法,这个方法内部会触发runJob来执行把当前的RDD中的数据写到checkpoint目录中,同时产生ReliableCheckpointRDD实例,这个newRDD实例将作为新的parentRDD,与切断lineage血缘操作有关系。

点进去ReliableCheckpointRDD.writeRDDToCheckpointDirectory(rdd, cpDir)查看,执行了如下步骤

  1. 首先为checkpoint创建输出文件目录

  2. 然后是广播sc.broadcast,这里将路径信息广播给所有的Executor

  3. 执行sc.runJob来执行把当前的RDD中的数据写到checkpoint的目录中,同时产生ReliableCheckpointRDD实例

    sc.runJob(originalRDD,
      writePartitionToCheckpointFile[T](checkpointDirPath.toString, broadcastedConf) _)
    
  4. 把partitioner对象写入HDFS(writePartitionerToCheckpointDir)

返回RDD.checkpoint()方法,中间执行了rdd.markCheckpointed(),表示将此 RDD 的依赖项从其原始父级更改为从检查点文件创建的新 RDD(newRDD),并忘记其旧的依赖项和分区(xxx = null哈哈哈)

checkpoint的读操作

看到RDD代码computeOrReadCheckpoint中的compute方法,其中调用了ReliableCheckpointRDD.readCheckpointFile(file, broadcastedConf, context)来读取checkpoint的数据

  override def compute(split: Partition, context: TaskContext): Iterator[T] = {
    val file = new Path(checkpointPath, ReliableCheckpointRDD.checkpointFileName(split.index))
    ReliableCheckpointRDD.readCheckpointFile(file, broadcastedConf, context)
  }

至此,checkpoint的数据就已经被读出来了啦!!!

总结

本篇文章主要参考一文搞懂Spark CheckPoint (qq.com),收获不少!!!

希望本篇文章能够帮助到各位同学,谢谢阅读到这里的同学!!!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值