SparkCore — CacheManager源码分析

CacheManager源码分析

  CacheManager主要发生在利用RDD的数据执行算子的时候,也就是在Task端ShuffleWriter的write方法写数据的时候,它传入了RDD的iterator方法作为参数。RDD的iterator()会读取或计算RDD的数据。我们分析一下iterator()方法:

RDD.iterator()方法
  final def iterator(split: Partition, context: TaskContext): Iterator[T] = {
    // 如果有本地化级别
    // 如果StorageLevel不为None,那么说明之前持久化过RDD,那么就不用直接从从父RDD执行算子,计算RDD的partition了,
    // 优先尝试使用CacheManager,去获取持久化的数据
    if (storageLevel != StorageLevel.NONE) {
      // CacheManager是从持久化的RDD中读取当前计算RDD需要的数据
      SparkEnv.get.cacheManager.getOrCompute(this, split, context, storageLevel)
    } else {
      // 进行RDD的partition的计算
      computeOrReadCheckpoint(split, context)
    }
  }

  从上面的代码中就可以看出,在读取数据的时候,首先会判断RDD的本地化级别是否为None,如果存在本地化级别,那么就使用CacheManager去获取持久化的数据;否则就看RDD是否被checkpoint了,假如也没有checkpoint,那么就重新计算。下面我们看一下CacheManager是怎么读取数据的(Checkpoint在下面的博客会继续分析)。

CacheManager的getOrCompute()方法
def getOrCompute[T](
      rdd: RDD[T],
      partition: Partition,
      context: TaskContext,
      storageLevel: StorageLevel): Iterator[T] = {
    // 获取RDD的BlockId
    val key = RDDBlockId(rdd.id, partition.index)
    logDebug(s"Looking for partition $key")
    // 直接使用BlockManager的get方法直接获取数据,如果获取到数据,那么就直接返回
    blockManager.get(key) match {
      case Some(blockResult) =>
        // Partition is already materialized, so just return its values
        val existingMetrics = context.taskMetrics
          .getInputMetricsForReadMethod(blockResult.readMethod)
        existingMetrics.incBytesRead(blockResult.bytes)

        val iter = blockResult.data.asInstanceOf[Iterator[T]]
        new InterruptibleIterator[T](context, iter) {
          override def next(): T = {
            existingMetrics.incRecordsRead(1)
            delegate.next()
          }
        }
      // 如果从BlockManager没有获取到数据,虽然RDD持久化,但是因为未知原因
      // 数据既不在本地内存或磁盘,也不在远程BlockManager上,那么就进行后续处理
      case None =>
        // Acquire a lock for loading this partition
        // If another thread already holds the lock, wait for it to finish return its results
        // 再次调用一次BlockManager的get()方法获取,如果获取到了,那么就直接返回
        // 假设还没有获取到,那么接着执行
        val storedValues = acquireLockForPartition[T](key)
        if (storedValues.isDefined) {
          return new InterruptibleIterator[T](context, storedValues.get)
        }

        // Otherwise, we have to load the partition ourselves
        try {
          logInfo(s"Partition $key not found, computing it")
          // 调用computeOrReadCheckpoint方法
          // 如果RDD之前Checkpoint过,那么尝试读取它的checkpoint
          // 如果没有RDD没有checkpoint,那么只能重新使用父RDD的数据,执行算子,计算一份
          val computedValues = rdd.computeOrReadCheckpoint(partition, context)

          // If the task is running locally, do not persist the result
          // 如果是local模式,那么不需要持久化RDD
          if (context.isRunningLocally) {
            return computedValues
          }

          // Otherwise, cache the values and keep track of any updates in block statuses
          // 由于走CacheManager,那么意味着RDD肯定设置持久化级别的,只是因为某些原因,持久化的数据没有找到(内存不够可能被删除)
          // 所以读取了checkpoint数据,或者是重新计算数据之后,要用putInBlockManager,将数据在BlockManager持久化一份
          val updatedBlocks = new ArrayBuffer[(BlockId, BlockStatus)]
          // 将数据重新持久化一份
          val cachedValues = putInBlockManager(key, computedValues, storageLevel, updatedBlocks)
          val metrics = context.taskMetrics
          val lastUpdatedBlocks = metrics.updatedBlocks.getOrElse(Seq[(BlockId, BlockStatus)]())
          metrics.updatedBlocks = Some(lastUpdatedBlocks ++ updatedBlocks.toSeq)
          new InterruptibleIterator(context, cachedValues)

        } finally {
          loading.synchronized {
            loading.remove(key)
            loading.notifyAll()
          }
        }
    }
  }

  我们分析一下这个方法,首先获取RDD的BlockId,然后直接使用BlockManager.get()方法去获取数据,它的get()方法很简单,里面先调用getLocal()方法尝试从本地获取数据(之前BlockManager分析过doGetLocal),假如没有获取到,那么就去远程获取(getRemote,里面调用了doGetRemote);假设获取到了,那么将获取到的数据封装一下直接返回,否则的话接着执行;
  这里就比较有意思,假如,第一次BlockManager没有获取到,那么这里还会去尝试获取一次,如果第二次获取到了,那么就直接返回。假设还没有获取到,就会调用RDD的computeOrReadCheckpoint()方法,尝试去读取之前被checkpoint的数据,这个方法里面会进行判断,假设数据被checkpoint了,那么读取checkpoint,否则就重新计算一份。
  下面接着执行,如果是local模式,那么就不需要持久化了。否则的话,需要对之前读取不到的RDD重新进行持久化。代码执行到这里的时候,就已经说明RDD是被设置了持久化级别了的,这里调用putInBlockManager,将数据在BlockManager上持久化一份。在持久化的时候,我们要注意Memory的持久化,下面我们分析一下putInBlockManager的方法。

putInBlockManager持久化

private def putInBlockManager[T](
key: BlockId,
values: Iterator[T],
level: StorageLevel,
updatedBlocks: ArrayBuffer[(BlockId, BlockStatus)],
effectiveStorageLevel: Option[StorageLevel] = None): Iterator[T] = {

// 获取持久化级别
val putLevel = effectiveStorageLevel.getOrElse(level)
// 如果持久化级别没有指定内存级别,那么就是磁盘的级别
if (!putLevel.useMemory) {
  /*
   * This RDD is not to be cached in memory, so we can just pass the computed values as an
   * iterator directly to the BlockManager rather than first fully unrolling it in memory.
   */
  // 直接调用blockManager的putIterator,将数据写入磁盘即可,使用doPut()来存储数据
  updatedBlocks ++=
    blockManager.putIterator(key, values, level, tellMaster = true, effectiveStorageLevel)
  blockManager.get(key) match {
    case Some(v) => v.data.asInstanceOf[Iterator[T]]
    case None =>
      logInfo(s"Failure to store $key")
      throw new BlockException(key, s"Block manager failed to return cached value for $key!")
  }
} else {
  // 指定了内存级别
  // 这里调用memoryStore的unrollSafely方法,尝试将数据写入内存
  // 如果unrollSafely判断数据可以写入内存,那么就将数据写入内存,
  // 假如内存不够,那么就将数据写入磁盘
  blockManager.memoryStore.unrollSafely(key, values, updatedBlocks) match {
    case Left(arr) =>
      // We have successfully unrolled the entire partition, so cache it in memory
      updatedBlocks ++=
        blockManager.putArray(key, arr, level, tellMaster = true, effectiveStorageLevel)
      arr.iterator.asInstanceOf[Iterator[T]]
    case Right(it) =>
      // There is not enough space to cache this partition in memory
      val returnValues = it.asInstanceOf[Iterator[T]]
      // 如果数据无法写入内存,那么判断数据是否有磁盘级别,如果有的话,那么就将数据写入磁盘文件中
      if (putLevel.useDisk) {
        logWarning(s"Persisting partition $key to disk instead.")
        val diskOnlyLevel = StorageLevel(useDisk = true, useMemory = false,
          useOffHeap = false, deserialized = false, putLevel.replication)
        putInBlockManager[T](key, returnValues, level, updatedBlocks, Some(diskOnlyLevel))
      } else {
        // 否则不进行持久化,返回
        returnValues
      }
  }
}
}

  这里我们主要分析一下当持久化级别是内存的时候,是怎么做的。这里做法就很小心,它会一点一点的判断当前能申请到的内存是否足够进行存储,就是为了防止一次性申请过大的内存导致OOM。它主要还是调用了MemoryStore的unrollSafely方法。下面我们分析一下这个方法

MemoryStore的unrollSafely()方法

def unrollSafely(
blockId: BlockId,
values: Iterator[Any],
droppedBlocks: ArrayBuffer[(BlockId, BlockStatus)])
: Either[Array[Any], Iterator[Any]] = {

// 记录当前unroll的次数
var elementsUnrolled = 0
// 假设一开始当前内存足够存放数据
var keepUnrolling = true
// 默认unroll内存限制是1M
val initialMemoryThreshold = unrollMemoryThreshold
// 将数据按照16为单位切分,每当取RDD的16个数据时,就申请内存,如果能申请到
// 就接着加上下一个16个数据申请,以此类推。
val memoryCheckPeriod = 16
// 内存阈值
var memoryThreshold = initialMemoryThreshold
// 内存获取的最大为当前数据大小的1.5倍
val memoryGrowthFactor = 1.5
// 存储当前获取了多少内存
var pendingMemoryReserved = 0L
// 存储当前RDD的数据
var vector = new SizeTrackingVector[Any]

// 获取足够的内存,默认先获取1M试试,假设能获取的到,说明可以尝试去获取
// 如果连1M也获取不到,那么就只会退出
keepUnrolling = reserveUnrollMemoryForThisTask(blockId, initialMemoryThreshold, droppedBlocks)

if (!keepUnrolling) {
  logWarning(s"Failed to reserve initial memory threshold of " +
    s"${Utils.bytesToString(initialMemoryThreshold)} for computing block $blockId in memory.")
} else {
  pendingMemoryReserved += initialMemoryThreshold
}

// Unroll this block safely, checking whether we have exceeded our threshold periodically
try {
  while (values.hasNext && keepUnrolling) {
    // 获取数据,每获取到16份数据的时候,就尝试申请内存
    vector += values.next()
    if (elementsUnrolled % memoryCheckPeriod == 0) {
      // If our vector's size has exceeded the threshold, request more memory
      // 获取当前数据大小
      val currentSize = vector.estimateSize()
      // 如果数据大小超过阈值
      if (currentSize >= memoryThreshold) {
        // 计算需要获取的内存大小,当前内存1.5倍 减去门限值
        val amountToRequest = (currentSize * memoryGrowthFactor - memoryThreshold).toLong
        // 尝试申请获取内存,其实里面调用的是evictBlocksToFreeSpace,会将旧的数据删除,
        // 假如旧数据的持久化级别没有Disk,那么就被彻底删除。
        keepUnrolling = reserveUnrollMemoryForThisTask(
          blockId, amountToRequest, droppedBlocks)
        if (keepUnrolling) {
          pendingMemoryReserved += amountToRequest
        }
        // 更新门限
        memoryThreshold += amountToRequest
      }
    }
    // 计数器加1
    elementsUnrolled += 1
  }

  // 返回,假如能够申请到足够的内存,那么就返回Array形式,否则就是返回数组迭代器
  if (keepUnrolling) {
    Left(vector.toArray)
  } else {
    logUnrollFailureMessage(blockId, vector.estimateSize())
    Right(vector.iterator ++ values)
  }

} finally {
  // 如果内存足够,则将数据持久化到内存中。
  if (keepUnrolling) {
    val taskAttemptId = currentTaskAttemptId()
    memoryManager.synchronized {
      // 将unrollMemoryMap中对应需要的内存释放掉
      unrollMemoryMap(taskAttemptId) -= pendingMemoryReserved
      // 先将需要的内存大小存入pendingUnrollMemoryMap,等tryPut真正的cache之后,就会释放掉。
      pendingUnrollMemoryMap(taskAttemptId) =
        pendingUnrollMemoryMap.getOrElse(taskAttemptId, 0L) + pendingMemoryReserved
    }
  } else {
    // Otherwise, if we return an iterator, we can only release the unroll memory when
    // the task finishes since we don't know when the iterator will be consumed.
  }
}
}

  这里有两个比较有意思的地方,第一:会先尝试获取一次内存,看能否获取的到,这里仅仅只获取1M的内存,如果连1M都获取不到,那么就不用去尝试不断获取了,直接返回,否则才开始下面的尝试获取;第二:不断的尝试获取内存,这里怎么获取,它为了防止一次性获取过多的内存,可能导致OOM,因此它分批次获取,每次取16个RDD元数据,依据它的大小去获取数据,而且获取数据是按照当前数据的1.5的大小来获取的,然后以此类推,直到获取到足够的内存,或内存不够为止。(这里插一句,获取内存的时候也是之前BlockManager源码中讲过,它会将当前最旧的数据尝试去删除,如果这个RDD的持久化包含Disk,那么会写入磁盘,否则就删除,空出内存)。
  在申请到了足够的内存之后,也不是立刻就进行cache,它会记录需要的内存大小,在BlockManager存数据的时候进行cache。
  总结一下,CacheManager它的作用就是当进行RDD的计算,读取RDD数据的时候,假设RDD进行了持久化,就用getOrCompute()尝试读取持久化的数据,假如读取到了,那么就很好立即返回;假设数据丢了,那么再尝试从checkpoint中读取,如果还没读取到,就重新计算一份。将重新计算的数据按照之前持久化级别进行持久化,这里注意内存持久化,它的设计比较有意思,先尝试获取一下内存,看能不能获取的到,这里默认1M;如果获取到了,那么就对RDD的数据分批次获取内存,每次以16个RDD元素为单位进行获取,这样就是为了防止一次性获取过多的内存,导致OOM。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值