spark存储管理源码分析系列之DiskBlockManager

前面我们了解了Spark存储的底层块数据Block的相关数据结构以及块管理器BlockInfoManager,本节我们来看下DiskBlockManager,它主要负责维护块数据在磁盘上存储位置的关系,负责数据目录规划,然后通过DiskStore进行数据写入,读取,删除操作。

DiskBlockManager

DiskBlockManager负责维护块数据与其在磁盘上存储位置的关系,创建了二级目录来维护逻辑block和落地后的block文件的映射关系的,二级目录用于对文件进行散列存储,散列存储可以使所有文件都随机存放,写入或删除文件更方便,存取速度快,节省空间,另外它还负责创建用于shuffle或本地的临时文件。BlockManager在构造时会创建DiskBlockManager,如下所示:

val diskBlockManager = {
  // Only perform cleanup if an external service is not serving our shuffle files.
  val deleteFilesOnStop = !externalShuffleServiceEnabled || executorId == SparkContext.DRIVER_IDENTIFIER
  new DiskBlockManager(conf, deleteFilesOnStop)
}

可以看出来,DiskBlockManager有两个参数,conf和是否结束时候清理目录,只有当不指定外部的ShuffleClient[即spark.shuffle.service.enabled属性为false]或者当前实例是Driver时,才会进行清理操作。

接下来我们看下DiskBlockManager是如何构造的:

  1. 调用createLocalDirs方法创建本地文件目录,然后创建二维数组subDirs,用来缓存一级目录localDirs及二级目录。DiskBlockManager创建二级目录结构是因为二级目录用于对文件进行散列存储,散列存储可以使所有文件都随机存放,写入或删除文件更方便,存取速度快,节省空间

    1. 一级目录是通过本地目录的数组,默认获取spark.local.dir属性或者系统属性java.io.tmpdir指定的目录,目录可能有多个;
    2. 二级目录的是数量配置通过spark.diskStore.subDirectories属性设置,默认为64。
    // 一级目录:大小为spark.local.dir属性或者系统属性java.io.tmpdir指定的目录的个数
    private[spark] val localDirs: Array[File] = createLocalDirs(conf)
    
    private def createLocalDirs(conf: SparkConf): Array[File] = {
      // 获取一级目录的路径,并进行flatMap
      Utils.getConfiguredLocalDirs(conf).flatMap { rootDir =>
        try {
          // 在每个一级目录下都创建名为"blockmgr-UUID字符串"的子目录
          val localDir = Utils.createDirectory(rootDir, "blockmgr")
          logInfo(s"Created local directory at $localDir")
          Some(localDir)
        } catch {
          case e: IOException =>
          logError(s"Failed to create local dir in $rootDir. Ignoring this directory.", e)
          None
        }
      }
    }
    
    // 二级目录:大小由spark.diskStore.subDirectories决定,默认为64。
    private val subDirs = Array.fill(localDirs.length)(new Array[File](subDirsPerLocalDir))
    
  2. 添加运行时环境结束时的钩子,用于在进程关闭时创建线程,通过调用DiskBlockManager的stop方法,清除一些临时目录。

    private val shutdownHook = addShutdownHook()
    
    private def addShutdownHook(): AnyRef = {
      logDebug("Adding shutdown hook") // force eager creation of logger
      // 虚拟机关闭钩子
      ShutdownHookManager.addShutdownHook(ShutdownHookManager.TEMP_DIR_SHUTDOWN_PRIORITY + 1) { () =>
        logInfo("Shutdown hook called")
        DiskBlockManager.this.doStop() // 在虚拟机关闭时也关闭DiskBlockManager
      }
    }
    

获取存储文件

获取磁盘存储文件要经过以下步骤:

  1. 根据文件名计算非负哈希值;
  2. 根据哈希值与本地文件一级目录的总数求余,记为dirId,获取一级目录;
  3. 根据哈希值与本地文件一级目录的总数求商,此商再与二级目录的数目求余,记为subDirId,获取二级目录;
  4. 如果dirId/subDirId存在,则获取dirId/subDirId目录下的文件,否则新建dirId/subDirId目录,最后返回文件。
// 根据指定的文件名获取文件。
  def getFile(filename: String): File = { 
    // 获取文件名的非负哈希值
    val hash = Utils.nonNegativeHash(filename)
    val dirId = hash % localDirs.length     // 按照Hash取余获取一级目录
    val subDirId = (hash / localDirs.length) % subDirsPerLocalDir     // 按照Hash取余获取二级目录
 
    // 尝试获取对应的二级目录
    val subDir = subDirs(dirId).synchronized {
      val old = subDirs(dirId)(subDirId)
      if (old != null) {  // 目录不为空
        old
      } else { // 目录为空,需要创建新的目录
        val newDir = new File(localDirs(dirId), "%02x".format(subDirId))
        if (!newDir.exists()) {
          Files.createDirectory(newDir.toPath)
        }
        // 记录到subDirs数组中
        subDirs(dirId)(subDirId) = newDir
        newDir
      }
    }

    new File(subDir, filename)
  }

另外提供了通过BlockId获取文件的方式,BlockId的name字段即为文件的名字:

// 此方法根据BlockId获取文件,blockId的name是文件命名
def getFile(blockId: BlockId): File = getFile(blockId.name)

临时块创建

DiskBlockManager还负责创建用于shuffle或本地的临时文件,供Shuffle Write阶段输出的存储文件和Spark计算过程中的中间结果使用,逻辑比较简单,利用随机数和临时文件的命名规则,调用getFile文件生成即可。

/** 用于为中间结果创建唯一的BlockId和文件,此文件将用于保存本地Block的数据。*/
def createTempLocalBlock(): (TempLocalBlockId, File) = {
  var blockId = new TempLocalBlockId(UUID.randomUUID())
  while (getFile(blockId).exists()) {
    blockId = new TempLocalBlockId(UUID.randomUUID())
  }
  (blockId, getFile(blockId))
}

/** 创建唯一的BlockId和文件,用来存储Shuffle中间结果(即Map任务的输出)*/
def createTempShuffleBlock(): (TempShuffleBlockId, File) = {
  var blockId = new TempShuffleBlockId(UUID.randomUUID())
  while (getFile(blockId).exists()) {
    blockId = new TempShuffleBlockId(UUID.randomUUID())
  }
  (blockId, getFile(blockId))
}

关闭清理目录

在BlockManager关闭时候,如果deleteFilesOnStop标记为真,则在DiskBlockManager关闭之前,需要遍历一二级目录,递归删除目录中的文件。

private def doStop(): Unit = {
  if (deleteFilesOnStop) {
    localDirs.foreach { localDir =>  // 遍历一级目录
      if (localDir.isDirectory() && localDir.exists()) {
        try {
          if (!ShutdownHookManager.hasRootAsShutdownDeleteDir(localDir)) {
            Utils.deleteRecursively(localDir) // 递归删除一级目录及其中的内容
          }
        } catch {
          case e: Exception =>
          logError(s"Exception while deleting local spark dir: $localDir", e)
        }
      }
    }
  }
}

参考

  1. https://cloud.tencent.com/developer/article/1491370
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值