前面我们了解了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是如何构造的:
-
调用createLocalDirs方法创建本地文件目录,然后创建二维数组subDirs,用来缓存一级目录localDirs及二级目录。DiskBlockManager创建二级目录结构是因为二级目录用于对文件进行散列存储,散列存储可以使所有文件都随机存放,写入或删除文件更方便,存取速度快,节省空间。
- 一级目录是通过本地目录的数组,默认获取
spark.local.dir
属性或者系统属性java.io.tmpdir
指定的目录,目录可能有多个; - 二级目录的是数量配置通过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))
- 一级目录是通过本地目录的数组,默认获取
-
添加运行时环境结束时的钩子,用于在进程关闭时创建线程,通过调用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 } }
获取存储文件
获取磁盘存储文件要经过以下步骤:
- 根据文件名计算非负哈希值;
- 根据哈希值与本地文件一级目录的总数求余,记为dirId,获取一级目录;
- 根据哈希值与本地文件一级目录的总数求商,此商再与二级目录的数目求余,记为subDirId,获取二级目录;
- 如果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)
}
}
}
}
}