本次为各位同学准备的是Spark高频八股–Spark的checkpoint过程~
创作不易!多多支持!
面筋
checkpoint是什么
-
Spark生产环境下经常会面临Transformations的RDD非常多或者RDD计算非常复杂,耗时事件特别长,这个时候就需要考虑任务执行过程中的数据持久化
-
Spark擅长多步骤迭代,同时擅长基于Job的服用,如果能够对曾经计算的过程产生的数据进行服用,就可以极大提高效率
-
如果采用persist把数据放在内存中,虽然快速,但不可靠。如果放在磁盘上,不完全可靠,因为磁盘损坏等也会发生
-
checkpoint 的产生就是为了相对而言更加可靠的持久化数据,在 Checkpoint 可以指定把数据放在本地并且是多副本的方式,但是在正常生产环境下放在 HDFS 上,这就天然的借助HDFS 高可靠的特征来完成最大化的可靠的持久化数据的方式。
-
checkpoint 是为了最大程度保证绝对可靠的复用 RDD 计算数据的 Spark 的高级功能,通过 checkpoint 可以把数据持久化到 HDFS 上来保证数据的最大程度的安全性
-
checkpoint 就是针对整个RDD 计算链条中特别需要数据持久化的环节(后面会反覆使用当前环节的RDD) 开始基于HDFS 等的数据持久化复用策略,通过对 RDD 启动 Checkpoint 机制来实现容错和高可用
图片来源:公众号旧时光大数据
checkpoint的过程主要如下:
- 通过SparkContext设置checkpoint数据保存的目录,RDD调用checkpoint方法,生产RDDCheckpointData,当RDD上运行一个Job后,就会立刻触发RDDCheckpointData中的checkpoint方法,在内部调用doCheckpoint;
- 然后调用ReliableRDDCheckpointDAata的doCheckpoint;ReliableCheckpointRDD的writeRDDToCheckpointDirectory的调用;
- 在writeRDDToCheckpointDirectory方法内部会触发runJob,来执行把当前的RDD中的数据写到已经设置的checkpoint的目录,同时产生ReliableCheckpointRDD实例
- checkpoint保存在HDFS中,具有多个副本;persist保存在内存或磁盘中。在Job作业调度的时候checkpoint沿着finalRDD的血统关系从后往前回溯向上查找,查找哪些RDD层标记要进行checkpoint,标记为checkpointlnProgress,一旦进行checkpoint,RDD所有父RDD就会被清空
checkpoint和persist的区别
- cache/persist都是为了提速,如果步骤过长,使用这两者,就不需要从头算起
- checkpoint:更多是为了高可用。其核心是HDFS的replication,利用HDFS的多副本错略,即使一个节点崩了也不会影响checkpoint的数据使用。这就是天然的借助了HDFS高容错性、高可靠性的特征来完成最大化的可靠的持久化数据的方式
- persist只是将数据保存在BlockManager中,但是RDD的lineage是不会变化的。checkpoint完毕之后,RDD已经没有之前的lineage血缘关系,而只有一个强行为其设置的checkpointRDD,也就是说checkpoint之后,lineage发生了改变
- 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)
查看,执行了如下步骤
-
首先为checkpoint创建输出文件目录
-
然后是广播sc.broadcast,这里将路径信息广播给所有的Executor
-
执行
sc.runJob
来执行把当前的RDD中的数据写到checkpoint的目录中,同时产生ReliableCheckpointRDD实例sc.runJob(originalRDD, writePartitionToCheckpointFile[T](checkpointDirPath.toString, broadcastedConf) _)
-
把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),收获不少!!!
希望本篇文章能够帮助到各位同学,谢谢阅读到这里的同学!!!