简单的讲,Spark存储体系是各个Driver和Executor实例中的BlockManager所组成的。但是从一个整体出发,把各个节点的BlockManager看成存储体系的一部分,那么存储体系还有更多衍生的内容,比如块存储服务、map任务输出跟踪器,Shuffle管理器。
6.1.1 存储体系架构
1)BlockManagerMaster:代理BlockManager与Driver上的BlockManagerMasterEndpoint通信。通信的内容很多,例如:注册BlockManager、更新Block信息、获取Block的位置,删除Executor等。
2)BlockManagerMasterEndpoint:由Driver上的SparkEnv负责创建和注册到Driver的RpcEnv中。主要对各个节点上的BlockManager、BlockManager与Executor的映射关系及Block的位置信息等进行管理
3)BlockManagerSlaveEndpoint,每个Executor或Driver的SparkEnv中都有属于自己的BlockManagerSlaveEndpoint,分别由各自的SparkEnv负责创建和注册到各自的RpcEnv中。BlockManagerSlaveEndpoint将接受BlockManagerMasterEndpoint下发的命令。下发命令很多,例如:删除Block、获取Block状态、获取匹配的BlockId等。
4)SerializerManager:序列化管理器
5)MemoryManager:内存管理器,负责对单个节点上内存的分配和回收
6)MapOutputTracker:map任务输出管理器
7)ShuffleManager:Shuffle管理器
8)BlockTransferService:块传输管理器,主要用于不同阶段的任务之间的Block数据的传输和读写
9)shuffleClient:Shuffle客户端,与BlockTransferService配合使用
10)SecurityManager:安全管理器
11)DiskBlockManager:磁盘块管理器,对磁盘上的文件以及目录的读写操作进行管理
12)BlockInfoManager:块信息管理器,负责对Block的元数据以及锁资源进行管理
13)MemoryStore:内存存储
14)DiskStore:磁盘存储
6.1.2 基本概念
1、BlockManager的唯一标识BlockManagerId
BlockManagerId的提供的方法:
isDriver:当前BlockManager所在的实例是否是Driver,此方法根据executorId的值是否是Driver来判断
writeExternal:将BlockManagerId的所有信息序列化后写到外部二进制中
readExternal:从外部二进制流中读取BlockManagerId的所有信息
2、块的唯一标识
文件系统的文件存储到磁盘上,都是以块为单位写入的,操作系统的块都是以固定大小读写的
在Spark存储体系中,数据的读写也是以块为单位的,只不过这个块并非操作系统的块,而是设计用于Spark存储体系的块。每个Block都有唯一的标识,Spark把这个表示抽象为BlockId.
Spark与Hadoop最重要的区别之一就在于对内存的使用,Hadoop只将内存作为计算资源,而Spark除了将内存作为计算资源之外,还将内存的一部分纳入到存储体系中。Spark使用MemoryManager对存储体系和内存体系计算所使用的内存进行管理。
Spark存储体系的每个存储节点上也不止一个内存池。内存池实质上是对物理内存的逻辑规划,协助Spark任务在运行时合理地使用内存资源。Spark将内存从逻辑上区分为堆内存和堆外内存,称为内存模式。
这里的堆内存并不能与JVM中的Java堆直接画等号,它只是JVM堆内存的一部分,堆外内存则是Spark使用sun.misc.Unsafe的API直接在工作节点的系统内存中开辟的空间,无论哪种内存,都需要一个内存池对内存进行资源管理,抽象类MemoryPool
Spark中一共有两种MemoryPool的具体实现,其中StorageMemoryPool是存储体系中用到的内存池,ExecutionMemoryPool则是计算引擎用到的内存池。
StorageMemoryPool是对用于存储的物理内存的逻辑抽象,通过对存储内存的逻辑管理,提高Spark存储体系对内存的使用效率。
1)acquireMemory 用于给BlockId对应的Block获取numBytes指定大小的内存
此方法首先计算要申请的内存大小numBytes与空闲空间memoryFree的差值,如果numByteToFree大于0.则说明需要腾出部分Block所占用的内存空,然后调用acquireMemory方法申请获得内存
2)releaseMemory 用于释放内存
3)freeSpaceToShrinkPool 用于释放指定大小的空间,缩小内存池的大小
如果空闲内存大于等于spaceToFree,那么返回sparkToFree即可,否则需要腾出占用内存的部分Block,以补齐spaceToFree的大小,并返回腾出空间与memoryFree的大小之和
1)maxOnHeapStorageMemory:返回用于存储的最大堆内存,此方法需要子类实现
2)maxOffHeapStorageMemory:返回用于存储的最大堆外内存,此方法需要子类实现
3)setMemorySize:给onHeapStorageMemoryPool和offHeapStorageMemoryPool设置MemoryStore
4)acquireStorageMemory:为存储BlockId对应的Block,从堆内存或堆外内存获取所需大小(即numBytes)
5)acquireUnrollMemory:为展开BlockId对应的Block,从堆内存或堆外内存获取所需大小(即numBytes)
6)releaseStorageMemory:从堆内存或堆外内存释放指定大小
7)releaseAllStorageMemory:从堆内存及堆外内存释放所有内存
8)releaseUnrollMemory:释放指定大小的展开内存
9)storageMemoryUsed:onHeapStorageMemoryPool与offHeapStorageMemoryPool中一起占用的存储内存
UnifiedMemoryManager提供了统一的存储管理机制,即Spark应用程序在运行期的存储内存和执行内存将共享统一的内存空间,可以动态调节两块内存的空间大小。
UnifiedMemoryManager在MemoryManager的内存模型之上,将计算内存和存储内存之间的边界修改为“软”边界,即任何一方可以向另一方借用空闲的内存。
属性:
1)conf
2)maxHeapMemory
3)onHeapStorageRegionSize
4)numCores
除了继承自MemoryManager的方法外,其特有的方法有:
1)maxOnHeapStorageMemory:返回用于存储的最大堆内存
2)maxOffHeapStorageMemory:返回用于存储的最大堆外内存
3)acquireStorageMemory:为存储BlockId对应的Block,从堆内存或堆外内存获取所需大小(numBytes)的内存
4)acquireUnrollMemory:为展开BlockId对应的Block,从堆内存或堆外内存获取所需大小(即numBytes)的内存,此方法实际代理调用了acquireStorageMemory
MemoryStore负责将Block存储到内存,Spark通过广播数据、RDD、Shuffle数据存储到内存,减少了对磁盘I/O的依赖,提高了读写效率
整个MemoryStore的存储分为三块:
1、一块是MemoryStore的entries属性持有的很多MemoryEntry所占据的内存blocksMemoryUsed;
2、一块是onHeapUnrollMemoryMap或offHeapUnrollMemoryMap中使用展开方式占用的内存currentUnrollMemory
3、blocksMemoryUsed和currentUnrollMemory的空间之和是已经使用的空间,用memoryUsed表示。
memoryStore提供了很多方法,便于对Block数据的存储和读取
1)getSize 此方法用于获取BlockId对应MemoryEntry(即Block的内存形式)所占用的大小
2)putBytes 此方法将BlockId对应的Block(已经封装为ChunkedByteBuffer)写入内存
3)reserveUnrollMemoryForThisTask 此方法用于为展开尝试执行任务给定的Block,保留指定内存模式上指定大小的内存
4)releaseUnrollMemoryForThisTask 此方法用于释放任务尝试线程占用的内存
5)putIteratorAsValues 此方法将BlockId对应的Block(已经转换为Iterator)写入内存。有时候放入内存的Block很大,所以一次性将此对象写入内存可能引发OOM,为了避免这种情况发生,首先需要将Block转换为Iterator,然后渐进式地展开此Iterator,并且周期性的检查是否有足够的展开内存。
6)putIteratorAsBytes 此方法以序列化的字节数组方式,将BlockId对应的Block写入内存
7)getBytes 此方法从内存中读取BlockId对应的Block(已经封装为ChunkedByteBuffer),只能获取序列化的Block
8)getValues 此方法用于从内存中读取BlockId对应的Block(已经封装为Iterator) 只能读取没有序列化的Block
9)remove 此方法用于从内存中移除BlockId对应的Block
10)clear 此方法用于清理MemoryStore
11)evictBlocksToFreeSpace 此方法用于驱逐Block,以便释放一些空间来存储新的Block\
不应该对有线程正在读取或写入的Block进行驱逐,只能驱逐那些没有被其他线程读或写的Block
12)contains 此方法用于判断本地MemoryStore中是否包含给定BlockId所对应的Block文件
BlockManager运行在每个节点上(包括Driver和Executor),提供对本地或远端节点上的内存、磁盘及堆外内存中Block的管理。存储体系狭义上来说值得就是BlockManager,但从广义上来说,则包括整个Spark集群中的各个BlockManager、BlockInfoManager、DiskBlockManager、DiskStore、MemoryManager、MemoryStore、对集群中的所有BlockManager进行管理的BlockManagerMaster及各个节点上对外提供Block上传和下载服务的BlockTransferService.
每个Driver或Executor在创建自身的SparkEnv时都会创建BlockManager。BlockManager只有在其initialize方法被调用后才能发挥作用。
1、register 此方法用于向BlockManagerMaster重新注册BlockManager,并向BlockManagerMaster报告所有的Block信息
2、getLocalBytes 此方法用于从存储体系中获取BlockId所对应的BlockId的数据,并封装为ChunkedByteBuffer
3、getBlockData 此方法用于获取本地的Block的数据
4、putBytes 用于写入Block数据,需要首先介绍doPut.doPut用于执行Block的写入
5、putBlockData 此方法用于将Block数据写入本地
6、getStatus 此方法用于获取Block的状态
7、getMatchingBlockIds 此方法用于获取匹配过滤器条件的BlockId的序列
8、getLocalValues 此方法用于从本地的BlockManager中获取Block的数据
9、getRemoteBytes 该方法的作用为从远端的BlockManager以序列化的字节形式获取Block数据
10、get 此方法用于优先从本地获取Block数据,当本地获取不到所需的Block数据时,再从远端获取Block数据
11、downgradeLock 此方法将当前线程持有的Block的写锁降级为读锁
12、releaseLock 此方法用于当前线程对持有的Block的锁进行释放
13、registerTask 此方法用于将任务尝试线程注册到BlockInfoManager
14、releaseAllLockForTask 此方法用于任务尝试线程对持有的所有Block的锁进行释放
15、getOrElseUpdate 此方法用于获取Block
16、putIterator 此方法用于将Block数据写入存储体系
17、getDiskWriter 此方法用于创建并获取DiskBlockObjectWriter 通过DiskBlockObjectWriter可以跳过对DiskStore的使用,直接将数据写入磁盘
18、putSingle 此方法的作用为获取由单个对象组成的Block
19、putSingle 此方法用于将由单个对象组成的Block写入存储体系。
20、dropFromMemory 此方法用于从内存中删除Block 当Block的存储级别允许写入磁盘时,Block将被写入磁盘
21、removeBlock 此方法的作用用于删除指定的Block
22、removeRdd 此方法的作用为移除属于指定的RDD的所有Block
BlockManagerMaster的作用是对存在于Executor或Driver上的BlockManager进行统一管理。Executor与Driver关于BlockManager的交互都依赖于BlockManagerMaster,比如Executor需要向Driver发送注册BlockManager、更新Executor上Block的最新信息、询问所需要Block目前所在的位置及当Executor运行结束需要将此Executor移除等。
我们知道Driver上的BlockManagerMaster会实例化并且注册BlockManagerMasterEndpoint。无论是Driver还是Executor,它们的BlockManagerMaster的driverEndpoint属性都持有BlockManagerMasterEndpoint的RpcEndpointRef。无论是Driver还是Executor,每个BlockManager都拥有自己的BlockManagerSlaveEndpoint,且BlockManager的slaveEndpoint属性保存着各自的RpcEndpointRef。BlockManagerMasterEndpoint负责发送消息,BlockManagerSlaveEndpoint负责消息的接收与处理,BlockManagerSlaveEndpoint则接收BlockManagerMasterEndpoint下发的命令。
BlockManagerMaster负责发送各种与存储体系相关的信息
以RegisterBlockManager为例:注册BlockManager的实质是向BlockManagerMasterEndpoint发送RegisterBlockManager消息
BlockManagerMasterEndpoint接收Driver或Executor上BlockManagerMaster发送的消息,对所有的BlockManager进行统一管理。
BlockManagerMasterEndpoint中定义了一些管理BlockManager的属性:
1、blockManagerInfo:BlockManagerId与BlockManagerInfo之间的映射关系
2、blockManagerIdByExecutor:Executor ID与BlockManagerId之间的映射关系
3、blockLocation:BlockId与存储了此BlockId对应Block的BlockManager的BlockManagerId之间的一对多关系映射
4、topologyMapper:对集群所有节点的拓扑结构的映射
以RegisterBlockManager为例:BlockManagerMasterEndpoint在接收到RegisterBlockManager消息后,将调用register方法。
BlockManagerSlaveEndpoint用于接收BlockManagerMasterEndpoint的命令并执行相应的操作。
BlockManagerSlaveEndpoint也重写了receiverAndReply方法
receiveAndReply方法支持接收以下方法:
1、RemoveBlock
2、RemoveRdd
3、RemoveShuffle
4、RemoveBroadcast
5、GetBlockStatus
6、getMatchingBlockIds
7、TriggerThreadDump
BlockTansferService是BlockManager的子组件之一。抽象类BlockTransferService有两个实现:用于测试的MockBlockTransferService以NettyBlockTransferService。我们知道BlockManager实际采用了NettyBlockTransferService提供的Block传输服务。
6.9.1 初始化NettyBlockTransferService
6.9.2 NettyBlockRpcServer详解