RDD的数据持久化
RDD本身并不存放数据,在没有特殊设置的情况下,每一个动作算子都会导致重新计算一遍数据。即使我们在代码里面看起来好像已经重用了某些算子对象。例如:
object RDDStoreExample {
def main(args: Array[String]): Unit = {
val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Spark_RDDStore")
val sparkContext = new SparkContext(sparkConf)
val list = List("hello scala", "hello spark")
val rdd = sparkContext.makeRDD(list)
val flatRDD = rdd.flatMap(_.split(" "))
val mapRDD = flatRDD.map(item => {
println("@@@@@@@@")
(item, 1)
})
val reduceRDD = mapRDD.reduceByKey(_ + _)
reduceRDD.collect().foreach(println)
println()
val groupRDD = mapRDD.groupByKey()
groupRDD.collect().foreach(println)
sparkContext.stop()
}
}
运行结果:
@@@@@@@@
@@@@@@@@
@@@@@@@@
@@@@@@@@
(spark,1)
(scala,1)
(hello,2)
@@@@@@@@
@@@@@@@@
@@@@@@@@
@@@@@@@@
(spark,CompactBuffer(1))
(scala,CompactBuffer(1))
(hello,CompactBuffer(1, 1))
通过运行结果发现,即使已经复用了mapRDD对象,但是数据流还是在进行从头开始的计算。为了避免这种情况,我们可以设置RDD缓存。
1:代码设置
mapRDD.cache()
// or
mapRDD.persist(StorageLevel.DISK_ONLY)
cache的底层采用的依然是persist。但是默认存储的级别是MEMORY_ONLY
.并且持久化之后的数据在程序运行结束之后会被删除。如果想要长久保存,应该设置检查点。检查点设置可参考下文。
2:cache()方法解析
cache()方法会调用persist方法,persist方法首先会判断该RDD是否被设置为了检查点。如果被设置了检查点,则执行转换检查点存储级别的操作,但是这一步是一定会抛出异常的,具体原因见persist的源码。
def persist(newLevel: StorageLevel): this.type = {
if (isLocallyCheckpointed) {
// This means the user previously called localCheckpoint(), which should have already
// marked this RDD for persisting. Here we should override the old storage level with
// one that is explicitly requested by the user (after adapting it to use disk).
persist(LocalRDDCheckpointData.transformStorageLevel(newLevel), allowOverride = true)
} else {
persist(newLevel, allowOverride = false)
}
}
isLocallyCheckpointed方法:
/**
* Return whether this RDD is marked for local checkpointing.
* 返回此RDD是否标记为本地检查点。
* Exposed for testing.
* 暴露出来进行测试。
*/
private def isLocallyCheckpointed: Boolean = {
checkpointData match {
case Some(_: LocalRDDCheckpointData[T]) => true
case _ => false
}
}
checkpointData类型:
private[spark] var checkpointData: Option[RDDCheckpointData[T]] = None
persist(newLevel: StorageLevel, allowOverride: Boolean)实现
/**
* Mark this RDD for persisting using the specified level.
* 将此RDD标记为使用指定级别进行持久化
* @param newLevel 新的存储级别
* @param allowOverride 是否用新存储级别替代已经存在的存储级别
*/
private var storageLevel: StorageLevel = StorageLevel.NONE
private def persist(newLevel: StorageLevel, allowOverride: Boolean): this.type = {
// 如果默认的存储级别不是None,或者新的存储级别不是None,或者允许覆盖之前的存储级别
// 以上三种情况都会报错
// 此处可以解释如果rdd已经设置了检查点此处的allowOverride变成false,此处一定会报错。
if (storageLevel != StorageLevel.NONE && newLevel != storageLevel && !allowOverride) {
throw new UnsupportedOperationException(
"Cannot change storage level of an RDD after it was already assigned a level")
}
// 如果这是第一次将此RDD标记为持久化,请注册它使用SparkContext进行清理和核算。只做一次。
if (storageLevel == StorageLevel.NONE) {
sc.cleaner.foreach(_.registerRDDForCleanup(this))
// 持久化数据
sc.persistRDD(this)
}
storageLevel = newLevel
this
}
缓存rdd
/**
* Register an RDD to be persisted in memory and/or disk storage
*/
private[spark] def persistRDD(rdd: RDD[_]) {
persistentRdds(rdd.id) = rdd
}
追踪rdd
// Keeps track of all persisted RDDs
// 跟踪所有持久化的rdd
private[spark] val persistentRdds = {
val map: ConcurrentMap[Int, RDD[_]] = new MapMaker().weakValues().makeMap[Int, RDD[_]]()
map.asScala
}
3:persist
/**
* Set this RDD's storage level to persist its values across operations after the first time
* it is computed. This can only be used to assign a new storage level if the RDD does not
* have a storage level set yet. Local checkpointing is an exception.
*/
def persist(newLevel: StorageLevel): this.type = {
if (isLocallyCheckpointed) {
// This means the user previously called localCheckpoint(), which should have already
// marked this RDD for persisting. Here we should override the old storage level with
// one that is explicitly requested by the user (after adapting it to use disk).
persist(LocalRDDCheckpointData.transformStorageLevel(newLevel), allowOverride = true)
} else {
persist(newLevel, allowOverride = false)
}
}
接下来就和之前cache一样。
4:checkPoint()
首先要设置缓存路径SparkContext.setCheckpointDir
/**
* Mark this RDD for checkpointing. It will be saved to a file inside the checkpoint
* 将此RDD标记为检查点进行落盘,数据将会保存在前面通过SparkContext.setCheckpointDir设置的目录下,
* directory set with `SparkContext#setCheckpointDir` and all references to its parent
* 关于此RDD的所有血缘关系将会被移除,checkpoint的操作会在当前rdd上的所有算子被运算完成之后才被调用,
* RDDs will be removed. This function must be called before any job has been
* 强烈建议在RDD调用checkpoint之前首先调用persist将数据缓存进内存,否则此RDD将会被重复计算。
* executed on this RDD. It is strongly recommended that this RDD is persisted in
* memory, otherwise saving it on a file will require recomputation.
*/
def checkpoint(): Unit = RDDCheckpointData.synchronized {
// NOTE: we use a global lock here due to complexities downstream with ensuring
// 注意:由于下游的复杂性,我们在这里使用全局锁,以确保子RDD分区指向正确的父分区。
// children RDD partitions point to the correct parent partitions. In the future
// 今后我们应该重新考虑这个问题。
// we should revisit this consideration.
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))
}
}
持久化和检查点的区别:
在Spark的持久化使用中,我们会将一些经常使用到的数据进行持久化,比如使用cache()或者persist()方法进行持久化操作,但是当某个节点或者executor挂掉之后,持久化的数据会丢失,因为我们的数据是保存在内存当中的,这时就会重新计算RDD,如果某个之前的RDD需要大量的计算时间,这时将会浪费很多时间,因此,我们有时候需要使用checkpoint操作来将一些数据持久化可容错文件系统中,比如HDFS文件系统中,虽然这种方式可能对性能带来了一定的影响(磁盘IO),但是为了避免大量的重复计算操作,有时也可以使用性能代价来换取时间效率上的提升。
当我们对某个RDD进行了缓存操作之后,首先会去CacheManager中去找,然后紧接着去BlockManager中去获取内存或者磁盘中缓存的数据,如果没有进行缓存或者缓存丢失,那么就会去checkpoint的容错文件系统中查找数据,如果最终没有找到,那就会按照RDD血缘重新计算。cache 和 checkpoint 之间有一个重大的区别,cache 将 RDD以及RDD的血统(记录了这个RDD如何产生)缓存到内存中,当缓存的RDD失效的时候(如内存损坏),它们可以通过血统重新计算来进行恢复。但是 checkpoint 将 RDD 缓存到了 HDFS 中,同时忽略了它的血统(也就是RDD之前的那些依赖)。为什么要丢掉依赖?因为可以利用 HDFS 多副本特性保证容错!
在进行 checkpoint 前,要先对 RDD 进行 cache。原因是:checkpoint 会等到 job 结束后另外启动专门的 job 去完成 checkpoint,也就是说需要 checkpoint 的 RDD 会被计算两次。
persist和 checkpoint之间的区别
persist()可以将 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 使用。