Spark rdd之cache & persist

1.cache & persist 算子

cache 和 persist 这两个算子的执行原理一样,cache 的底层实现仍然是 persist,persist
提供了不同的存储级别。这里特别要注意的是:

//三种使用方式等价
cache() = persist() = persist(MEMORY_ONLY)

从下面源码来看,cache() 函数的源码,其实调的就是 persist(), 而 persist() 调的则是
persist(StorageLevel.MEMORY_ONLY)。

  /**
 * 1.当用户调用 cache 时,其实调的就是 persist(),
   */
  def cache(): this.type = persist()

  /**


 * 2.然而,persist() 函数其实调用的是 persist(StorageLevel.MEMORY_ONLY)
 * 使用的缓存级别为 MEMORY_ONLY ,也就是内存缓存
   */
  def persist(): this.type = persist(StorageLevel.MEMORY_ONLY)
  

2. Spark 缓存级别

关于缓存级别, Spark 官方基于以下 3 个方面进行衡量:

  • 存储位置。在 Spark 的存储级别中,既可以存储在内存,也可以存储在磁盘。对于 MEMORY_ONLY 默认级别,当内存不够时,剩下的 partition 便不会进行缓存,使用的时候需要重新计算。

  • 是否序列化缓存数据。对缓存数据进行序列化,可以减少存储空间的开销,但是在反序列化的时会带来一定的延时。

  • 缓存数据是否进行备份。把缓存数据复制多份存储到其它节点上,解决了单节点缓存数据失效问题,但会消耗更多的存储空间。

根据上述 3 个方面组合,Spark 一共提供了以下 12 种存储级别,你可以根据我的注释去理解。

  val NONE = new StorageLevel(false, false, false, false)          // 不存储
  val DISK_ONLY = new StorageLevel(true, false, false, false)      //只存储在磁盘,不序列化,副本为 1
  val DISK_ONLY_2 = new StorageLevel(true, false, false, false, 2) //只存储在磁盘,不序列化,副本为 2
  val MEMORY_ONLY = new StorageLevel(false, true, false, true)     //只存储在内存,不序列化,副本为 1
  val MEMORY_ONLY_2 = new StorageLevel(false, true, false, true, 2) //只存储在内存,不序列化,副本为 2
  val MEMORY_ONLY_SER = new StorageLevel(false, true, false, false) //只存储在内存,序列化,副本为 1
  val MEMORY_ONLY_SER_2 = new StorageLevel(false, true, false, false, 2) //只存储在内存,序列化,副本为 2
  val MEMORY_AND_DISK = new StorageLevel(true, true, false, true)       //内存 + 磁盘,不序列化,副本为 1
  val MEMORY_AND_DISK_2 = new StorageLevel(true, true, false, true, 2)  //内存 + 磁盘,不序列化,副本为 2
  val MEMORY_AND_DISK_SER = new StorageLevel(true, true, false, false)  //内存 + 磁盘,序列化,副本为 1
  val MEMORY_AND_DISK_SER_2 = new StorageLevel(true, true, false, false, 2) //内存 + 磁盘,序列化,副本为 2
  val OFF_HEAP = new StorageLevel(false, false, true, false)                //存储在堆外内存

在实际生产环境中,缓存级别该如何选择?

不同的缓存级别所对应的需求也不同,我们在选择时主要考虑以下两个问题:

  • 是否有足够内存、磁盘空间进行缓存?没有足够的内存、磁盘空间但又需要进行数据缓存,可以选择 MEMORY_AND_DISK 或者
    MEMORY_AND_DISK_SER 级别缓存数据。

  • 如果数据缓存到磁盘上,那么读取缓存数据的时间是否大于重新计算出该数据的时间。如果是,可以不缓存或者分配更大的内存来进行缓存。


3.用户调用 cache 后,系统是怎么对 RDD 进行 cache 的?

关于它的实现原理,我们只针对 persist(StorageLevel.MEMORY_ONLY) 这一种情况进行分析。假设用户在业务程序中调用了 cache() ,它底层实际会调用下面这个函数:

private def persist(newLevel: StorageLevel, allowOverride: Boolean): this.type = {
    // 处理存储级别变化的情况
    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))
      //把要缓存的 RDD 存到一个 Map(id,RDD) 的数据结构中
      sc.persistRDD(this)
    }
    //为 RDD 指定缓存级别
    storageLevel = newLevel
    this
  }

从上述源码来看,实际上用户在使用 cache() 算子进行缓存时,此时只是把分区数据,打上了一个存储级别标记 (每一个 RDD 都有一个 storageLevel 变量,初始默认为 NONE)

而并没有真正立马执行 RDD 缓存,这个算子是一个懒加载执行,只有当 RDD 真正被计算时,RDD 才会被缓存。一旦存储级别被指定了之后,在相同的 SparkContext 下就不能修改。


4.那会在什么时候真正执行缓存这个动作呢?

当用户程序调用 Action 算子触发计算,task 便会在计算 Partition 时,判断该 Partition 是否是需要 cache,如果需要被缓存,则先把 Partition 结果计算出来,计算完后立马缓存到内存。

在 RDD 的抽象类中,提供了一个迭代器函数 iterator(),通过这个迭代器函数便可以访问到 RDD 中的分区数据,也就是从这里开始进行计算数据。

final def iterator(split: Partition, context: TaskContext): Iterator[T] = {
    //遍历数据时,先判断 RDD 的存储级别是否为 NONE,如果用户在某一个 RDD 执行了 cache 或者
    //persist,此时 RDD 中 storagelevel 已经被修改,所以会从缓存中获取,获取不到则重新计算
    if (storageLevel != StorageLevel.NONE) {
      getOrCompute(split, context)
    } else {
      //如果还是默认的存储级别 NONE,迭代要么从 checkpoint 的目录中读取,要么重新计算
      computeOrReadCheckpoint(split, context)
    }
  }

在 iterator() 源码中,先判断当前分区数据的存储级别,如果用户之前调用了 cache() 算子,此时分区数据的存储级别应该不为 NONE 这个级别。因此,便会调用 getOrCompute() 函数。

getOrCompute 函数

private[spark] def getOrCompute(partition: Partition, context: TaskContext): Iterator[T] = {
    //先获取 RDD 的 block id
    val blockId = RDDBlockId(id, partition.index)
    var readCachedBlock = true
    //先根据 blockId 从 blockManager 查看是否已经被缓存了
    SparkEnv.get.blockManager.getOrElseUpdate(blockId, storageLevel, elementClassTag, () => {
      readCachedBlock = false
      computeOrReadCheckpoint(partition, context)
    }) match {
        .....
    }
  }

当 RDD.iterator() 被调用时, 也就是要计算该 RDD 中某个 partition 的时候。首先,会生成一个 blockId, 表明是要存哪个 RDD 的哪个 partition。

注意:这个 blockId 类型是 RDDBlockId,它是由 rddid + partitionId 组成 。

之后,会把计算出来的 partition 数据放到 BlockManager 中的 MemoryStore 中,MemoryStorye 维护了一个 LinkedHashMap[blockId,memoryEntry]

key 是 blockId,value 是当前缓存的数据。因此,缓存的分区数据最后会存放在 LinkedHashMap 数据结构中,LinkedHashMap 是基于双向链表实现的。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值