Spark Block Manager管理
spark中的RDD-Cache, Shuffle-output, 以及broadcast的实现都是基于BlockManager来实现, BlockManager提供了数据存储(内存/文件存储)接口.
这里的Block和HDFS中谈到的Block块是有本质区别:HDFS中是对大文件进行分Block进行存储,Block大小固定为512M等;而Spark中的Block是用户的操作单位, 一个Block对应一块有组织的内存,一个完整的文件或文件的区间端,并没有固定每个Block大小的做法;
##BlockID和ManagerBuffer 上面谈到,Block是用户的操作单位,而这个操作对应的key就是这里BlockID,该Key所对应的真实数据内容为ManagerBuffer;
先提前看一下BlockDataManager这个数据接口,它在NetWork包中,对外提供了Block的操作.
trait BlockDataManager {
def getBlockData(blockId: String): Option[ManagedBuffer]
def putBlockData(blockId: String, data: ManagedBuffer, level: StorageLevel): Unit
}
我们看到BlockDataManager中有getBlockData和putBlockData两个接口,分别通过blockId获取一个ManagedBuffer,以及将一个blockId与ManagedBuffer对添加到 BlockManager中管理; 在Spark中BlockDataManager的唯一实现也就是我们这里谈到的BlockManager"服务".
BlockID本质上是一个字符串,但是在Spark中将它保证为"一组"case类,这些类的不同本质是BlockID这个命名字符串的不同,从而可以通过BlockID这个字符串来区别BlockID.
首先我们来看在Spark中Block类型,其实也就是开头谈到的RDD-Cache, Shuffle-output, 以及broadcast等;
- RDDBlock:“rdd_” + rddId + “_” + splitIndex; 即每个RDD
block表示一个特定rdd的一个分片 - ShuffleBlock:多说一句关于shuffle,在Spark的1.1版本中发布一个sort版本的shuffle,原先的版本为hash,因此两种类型的shuffle也对应了两种数据结构
- Hash版本,ShuffleBlock:“shuffle_” + shuffleId + “” + mapId + “” + reduceId
- Sort版本,对于每一个bucket(shuffleId + “” + mapId + “” + reduceId组合)由ShuffleDataBlock和ShuffleIndexBlock两种block组成
- “shuffle_” + shuffleId + “” + mapId + “” + reduceId + “.data”
- “shuffle_” + shuffleId + “” + mapId + “” + reduceId + “.index”
- BroadcastBlock:“broadcast_” + broadcastId + “_” + field)具体这里不多说,不感兴趣
- TaskResultBlock:“taskresult_” +
taskId;Spark中task运行的结果也是通过BlockManager进行管理 - StreamBlock: “input-” + streamId + “-” +
uniqueId应该是用于streaming中,不是特别感兴趣 - TempBlock: “temp_” + id
通过上面的命名规则,我们可以快速确定每个Block的类型,以及相应的业务信息. 其中RDDBlock, ShuffleBlock, TaskResultBlock是个人比较感兴趣的三种Block.
再来看看ManagedBuffer, 本质上ManagedBuffer是一个对外Buffer的封装,这个类型在BlockManager内部使用并不多,外部通过BlockDataManager的接口来获取和 保存相应的Buffer到BlockManager中,这里我们首先简单的分析一下
ManagedBuffer.
sealed abstract class ManagedBuffer {
def size: Long
def nioByteBuffer(): ByteBuffer
def inputStream(): InputStream
}
每个ManagedBuffer都有一个Size方法获取Buffer的大小,然后通过nioByteBuffer和inputStream两个接口对外提供了对Buffer的访问接口.至于这个Buffer具体存储方式由 子类来实现.比如ManagedBuffer的FileSegmentManagedBuffer子类实现了,将文件部分段转化为一个ManagedBuffer
final class FileSegmentManagedBuffer(val file: File, val offset: Long, val length: Long)
extends ManagedBuffer {
override def nioByteBuffer(): ByteBuffer = {
var channel: FileChannel = null
channel = new RandomAccessFile(file, "r").getChannel
channel.map(MapMode.READ_ONLY, offset, length)
}
}
FileSegmentManagedBuffer通过NIO接口将文件Map到内存中,并返回ByteBuffer;注意这个nioByteBuffer函数是每次调用将会返回一个新的ByteBuffer,对它的操作不影响 真实的Buffer的offset和long
除了FileSegmentManagedBuffer实现以外, 还有NioByteBufferManagedBuffer(将一块已有的ByteBuffer内存封装为ManagedBuffer)和NettyByteBufManagedBuffer( 将netty中的ByteBuf内存封装为ManagedBuffer)
上面说到了ManagedBuffer只是BlockManager对外提供的Buffer表示,现在问题来了,这里谈到的BlockID对于的"Block块"在BlockManager服务中是怎么维护它的状态的呢?
BlockInfo和StorageLevel
上面谈到的Block在BlocManager中是怎么样的维护它的状态的呢?注意我们这里不去分析Block具体是怎么存储,后面会去分析;这里分析Block的属性信息;BlockManager 为了每个Block的属性信息来跟踪每个Block的状态.
首先来看StorageLevel, 在Spark中,对应RDD的cache有很多level选择,这里谈到的StorageLevel就是这块内容;首先我们来看存储的级别:
- DISK,即文件存储
- Memory,内存存储,这里的内存指的是Jvm中的堆内存,即onHeap
- OffHeap, 非JVM中Heap的内存存储
对于DISK和Memory两种级别是可以同时出现的,而OffHeap与其他两个是互斥的.
关于OffHeap这里多说两句:JVM中如果直接进行内存分配都是受JVM来管理,使用的是JVM中内存堆,但是现在有很多技术可以在JVM代码中访问不受JVM管理的内存,即OffHeap内存; OffHeap最大的好处就是将内存的管理工作从JVM的GC管理器剥离出来由自己进行管理,特别是大对象,自定义生命周期的对象来说OffHeap很实用,可以减少GC的代销.
Spark中实现的OffHeap是基于Tachyon:分布式内存文件系统来实现的,在我们这篇分析文档中不会具体分析Tachyon的实现,有时间再去研究一下.
继续回到StorageLevel的分析; 除了三种存储级别以外,StorageLevel还