概述
本文主要介绍Spark存储中的Storage模块的解析。主要是阐述package org.apache.spark.storage
这个包中所阐述的关于存储方面的原理,由于不是源码分析,因此只会出现少量的代码。
RDD的存放和管理都是由Spark的存储管理模块Storage实现和管理的,涉及到多个类,主要是MemoryStore和BlockManager。本文先从架构和功能两个角度对Spark的存储管理模块进行介绍,然后接着介绍RDD持久化的机制和过程。
1. Storage模块解析
在storage这个包中,大部分都是以Block开头的类,而在子包memory中,有一个MemoryStore类,用于和MemoryManager来交互,例如持久化到内存中等等一类的操作(持久化到磁盘是DiskStore.scala)。
1.1 架构角度
从架构角度,存储管理模块主要分为以下两层:
存储层
:BlockManager,负责把数据存储到硬盘或者内存中,必要时还需要复制到远端,这些操作由存储层来实现和提供相应接口。(共用BlockManager类)通信层
:BlockManagerMaster,采用的是主从结构来实现通信层,主节点和从节点之间传输控制信息、状态信息。(主从均基于BlockManagerMaster,但是通信端点分别为:主BlockManagerMasterEndpoint、从BlockManagerSlaveEndpoint)
1.1.1 通信层架构
- 在存储管理模块的通信层,每个Executor上的BlockManager只负责管理其自身Executor所拥有的数据块原信息,而不会管理其他Executor上的数据块元信息;
- BlockManager类通过BlockManagerMaster进行通信;
- 主节点的BlockManager会包含所有从节点的BlockManager信息;
- 主从节点之间通过各自的BlockManager[Master/Slave]Endpoint来进行相互通信;
Driver端
:BlockManagerMaster的BlockManagerMasterEndpoint使用一个HashMap
维护所有已注册的BlockManager信息和数据块元信息
Executor端
:BlockManagerMaster的BlockManagerSlaveEndpoint通过向Driver发送信息来获得所需要的非本地数据的。(接收消息并处理的是BlockManagerMasterEndpoint)
1.1.2 存储层架构
RDD是由不同的分区组成的,我们所进行的转换和执行操作都是在每一块独立的分区上各自进行的。而在Storage模块内部,RDD又被视为由不同的数据块组成,对于RDD的存取是以数据块为单位的,本质上分区(partition)和数据块(block)是等价的,只是看待的角度不同。同时,在Spark存储管理模块中存取数据的最小单位是数据块,所有的操作都是以数据块为单位的。
在具体实现时Driver端和Executor端的Storage模块构成了主从式
的架构,即Driver端的BlockManager为Master,Executor端的BlockManager为Slave。Storage模块在逻辑上以Block为基本存储单位,RDD的每个Partition经过处理后唯一对应一个Block(BlockId的格式为rdd_RDD-ID_PARTITION-ID)。Master负责整个Spark应用程序的Block的元数据信息的管理和维护,而Slave需要将Block的更新等状态上报到Master,同时接收Master的命令,例如新增或删除一个RDD。
1.1.3 数据块
前面章节已经提到:存储管理模块以数据块为单位进行数据管理,数据块是存储管理模块中最小的操作单位。在存储管理模块中管理着各种不同的数据块,这些数据块为Spark框架提供了不同的功能,Spark存储管理模块中所管理的几种主要数据块为:
|
|
---|---|
RDD数据块 | 用来存储所缓存的RDD数据 |
Shuffle数据块 | 用来存储持久化的Shuffle数据 |
广播变量数据块 | 用来存储所存储的广播变量数据 |
任务返回结果数据块 | 用来存储在存储管理模块内部的任务返回结果。通常情况下任务返回结果随任务一起通过Akka返回到Driver端。但是当任务返回结果很大时,会引起Akka帧溢出,这时的另一种方案是将返回结果以块的形式放入存储管理模块,然后在Driver端获取该数据块即可,因为存储管理模块内部数据块的传输是通过Socket连接的,因此就不会出现Akka帧溢出了 |
流式数据块 | 只用在Spark Streaming中,用来存储所接收到的流式数据块 |
1.2 小结
1.2.1 BlockManager和BlockManagerMaster的创建
该怎么总结一下呢;先从BlockManager和BlockManagerMaster的创建来说吧:
先说Driver :
- 在
SparkContext
的初始化过程中是会去调用SparkEnv.createDriverEnv
创建Driver的相关运行组件,内部其实SparkEnv.create()
函数来先创建MemoryManager
(依据参数确定静态或者动态),然后创建BlockManagerMaster
和BlockManager
,BlockManager创建需要传入MemoryManager作为参数。如下两图 BlockManagerMaster
会创建自己的BlockManagerMasterEndpoint
,并到SparkEnv的RPCEnv队列中注册。
再说Executor:- 在
CoarseGrainedExecutorBackend
中,其实在run()
方法中会调用SparkEnv.createExecutorEnv
来创建Executor的相关运行组件,内部也是通过SparkEnv.create()
函数来创建的,因此对于Executor来说,它也有taskMemoryManager
、BlockManagerMaster
和BlockManager
。 - 区别在于创建BlockManagerMaster:
- 如果是Driver,那么是创建一个
BlockManagerMasterEndpoint
,然后将其在SparkEnv中注册;BlockManagerMasterEndpoint有一个HashMap用以维护注册的BlockManager的信息。 - 如果是Executor,那么创建的是
BlockManagerSlaveEndpoint
,同时会拿到BlockManagerMasterEndpointRef,去将自身的BlockManager注册到Driver的BlockManagerMasterEndpoint上
- 如果是Driver,那么是创建一个
注意的是create()方法的说明:
A helper method to create a SparkEnv for a driver or an executor.
说明,这个方法即可创建Driver,也可创建Executor,依据:
executorId == SparkContext.DRIVER_IDENTIFIER
1.2.2 MemoryStore和DiskStore的创建
在storage这个包
中,大部分都是以Block开头的类,而在子包memory
中,有一个MemoryStore类,用于和MemoryManager来交互,例如持久化到内存中等等一类的操作(持久化到磁盘是DiskStore.scala)。
在BlockManager这个类中,会创建MemoryStore和DiskStore分别用于与内存和磁盘交互。
- 而在创建MemoryStore,需要用到前面创建BlockManager是传入的UnifiedMemoryManager,这样,MemoryStore就能进行RDD持久化到内存这类的操作了。
- 就以RDD持久化为例,实际上是BlockManager调用
memoryStore.putIteratorAsValues(blockId, values, classTag)
这样的函数将RDD以序列化/非序列化的形式持久化到内存中 - DiskStore同理。
1.3 BlockManager运行实例
这部分转自[Spark内核] 第38课:BlockManager架构原理、运行流程图和源码解密。侵删。
1.3.1 从 Application 启动的角度来观察BlockManager
- 在
Application 启动
的时候会在spark-env.sh
中注册BlockMangerMaster
以及MapOutputTracker
,其中:- BlockManagerMaster:对整集群的 Block 数据进行管理;
- MapOutputTracker:跟踪所有的 Mapper 的输出;
BlockManagerMasterEndpoint
本身是一个消息体,会负责通过RPC通信的方式去管理所有节点的 BlockManager;- 每启动一个 ExecutorBackend 都会
实例化 BlockManager
并通过远程通信的方式注册给 BlockMangerMaster
;实际上是 Executor 中的 BlockManager 注册给 Driver 上的 BlockMangerMasterEndpoiont;(BlockManger 是 Driver 中的一个普通的对象而己,所以无法直接对一个对象做HA) MemoryStore
是 BlockManager 中专门负责内存数据存储和读写的类,MemoryStore 是以 一个又一个 Block 为单位的DiskStore
是 BlockManager 中专门负责磁盘数据存储和读写的类;DiskBlockManager
:管理 LogicalBlock 与 Disk 上的 PhysicalBlock 之间的映射关联并负责磁盘的文件的创建,读写等;
[下图是 Spark-Shell 启动时的日志信息-1]
[下图是 Spark-Shell 启动时的日志信息-2]
1.3.2 从 Job 运行的角度来观察BlockManager
- 首先通过 MemoryStore 来存储广播变量
- 在 Driver 中是通过
BlockManagerInfo
来管理集群中每个 ExecutorBackend 中的 BlockManager 中的元数据信息的。当改变了具体的 ExecutorBackend 上的 Block 的信息后就必需发消息给 Driver 中的 BlockManagerMaster 来更新相应的 BlockManagerInfo 的信息 - 当执行第二个 Stage 之后,第二个 Stage 会向 Driver 中的 MapOutputTrackerMasterEndpoint 发消息请求上一個 Stage 中相应的输出,此時 MapOutputTrackerMaster 会把上一個 Stage 的输出数据的元数据信息发送给当前请求的 Stage
[下图是 Spark-Shell 作业运行时的日志信息-1]
[下图是 Spark-Shell 作业运行时的日志信息-2]
1.4 功能角度
从功能角度,存储管理模块可以分为以下两个主要部分:
RDD持久化
:整个存储管理模块主要的工作是作为RDD的缓存,包括基于内存和磁盘的缓存。Shuffle数据的持久化
:Shuffle中间结果的数据也是交由存储管理模块进行管理的。Shuffle性能的好坏直接影响了Spark应用程序整体的性能,因此存储管理模块中对于Shuffle数据的处理有别于传统的RDD缓存。
2. RDD持久化
限于篇幅,参见Spark内存管理之Storage内存管理,里面以RDD持久化为例,详尽的阐述了RDD从产生到持久化的过程
3. Shuffle数据的持久化
这里讲的主要是Shuffle的结果数据的持久化,而对于shuffle临时数据的缓存,请参考Spark内存管理之执行内存管理
3.1 默认的shuffle数据处理方式
下图为Spark中Shuffle操作的流程示意图
- 首先,每一个Map任务会根据Reduce任务的数据量创建出相应的桶,桶的数量是M*R,其中M是Map任务的个数,R是Reduce任务的个数。
- 其次,Map任务产生的结果会根据所设置的分区算法填充到每个桶中。这里的分区算法是可自定义的,当然默认的算法是根据键哈希到不同的桶中。
- 当Reduce任务启动时,它会根据自己任务的ID和所依赖的Map任务的ID从远端或本地的存储管理模块中取得相应的桶作为任务的输入进行处理。
3.2 shuffle数据与RDD持久化数据的区别
Shuffle数据与RDD持久化的不同之处在于:
- Shuffle数据块必须是在磁盘上进行缓存的,而不能选择在内存中缓存;(缓存的是shuffle临时数据)
- 在RDD基于磁盘的持久化中,每一个数据块对应着一个文件,而在Shuffle数据块持久化中,Shuffle数据块的存储有两种方式:
一种
是将Shuffle数据块映射成文件,这是默认的方式;一种
是将Shuffle数据块映射成文件中的一段,这种方式需要将spark.shuffle.consolidateFiles
设置为true。
默认的方式会产生大量的文件,如1000个Map任务和1000个Reduce任务,会产生1000000个Shuffle文件,这会对磁盘和文件系统的性能造成极大的影响,因此有了第二种是实现方式,将分时运行的Map任务所产生的Shuffle数据块合并到同一个文件中,以减少Shuffle文件的总数。对于第二种存储方式,示意图如下:
前面介绍了Shuffle数据块的存取,下面我们来介绍Shuffle数据块的读取和传输。Shuffle是将一组任务的输出结果重新组合作为下一组任务的输入这样的一个过程,由于任务分布在不同的节点上,因此为了将重组结果作为输入,必然涉及Shuffle数据的读取和传输。
3.3 shuffle数据的读取和传输
在Spark存储管理模块中,Shuffle数据的读取和传输有两种方式:
- 一种是基于NIO以socket连接去获取数据;
- 另一种是基于OIO通过Netty服务端获取数据。
前者是默认的获取方式,通过配置spark.shuffle.use.netty
为true,可以启用第二种获取方式。之所以有两种Shuffle数据的获取方式,是因为默认的方式在一些情况下无法充分利用网络带宽,用户可以通过比较两种方式在性能上的差异来自行决定选用哪种Shuffle数据获取方式。
总的来说,Spark存储管理模块为Shuffle数据的持久化做了许多有别于RDD持久化的工作,包括存取Shuffle数据块的方式,以及读取和传输Shuffle数据块的方式,所有这些实现都是为了使Shuffle获得更好的性能和容错。
总结
主要介绍了Storage模块的原理方面的一些内容。主要有:
- Storage模块架构,通信层/存储层均采用主从架构
- 然后介绍了BlockManager、BlockManagerMaster、MemoryStore、DiskStore的创建和相互关系。
- 介绍了RDD的持久化和Shuffle数据的持久化
疑问
以上的内容,主要是基于各种博客的内容而编写的,夹杂了一些对源码的理解,但是对于后面的Shuffle数据持久化,这一块,个人是抱着疑问态度的,需要后续不对源码不断的跟进学习。