Spark Storage模块详解

Storage模块负责管理Spark计算过程中产生的数据,包括基于Disk的和基于Memory的。用户在实际编程中,面对的是RDD,可以将RDD的数据通过调用org.apache.spark.rdd.RDD#cache将数据持久化;持久化的动作都是由Storage模块完成的,包括Shuffle过程中的数据,也都是由Storage模块管理的。可以说,RDD实现用户的逻辑,而Storage管理用户的数据。在Driver端和Executor端,都会有Storage模块。

一、Storage模块整体架构

1,整体架构

Storage模块采用的是Master/Slave的架构。Master负责整个Application的Block的元数据信息的管理和维护;而Slave需要将Block的更新等状态上报到Master,同时接收Master的命令,比如删除一个RDD、Shuffle相关的数据或者是广播变量。而Master与Slave之间通过AKKA消息传递机制进行通信。
在SparkContext创建时,它会创建Driver端的SparkEnv,而SparkEnv会创建BlockManager,BlockManager创建的时候会持有一个BlockManagerMaster。BlockManagerMaster会把请求转发给BlockManagerMasterActor来完成元数据的管理和维护。
而在Executor端,也存在一个BlockManager,它也会持有一个BlockManager-Master,只不过BlockManagerMaster会持有一个Driver端BlockManagerMasterActor的Reference,因此Executor端的BlockManager就能通过这个Actor的Reference将Block的信息上报给Master。
BlockManager本身还持有一个BlockManagerSlaveActor,而这个Slave的Actor还会被上报到Master。Master会持有这个Slave Actor的Reference,并通过这个Reference向Salve发送一些命令,比如删除Slave上的RDD、Shuffle相关的数据或者是广播变量。
在这里插入图片描述
Master和Slave之间并没有专门的心跳,而是通过Driver和Executor之间的心跳来间接完成的。
Master持有整个Application的Block的元数据信息,包括Block所在的位置,Block所占的存储空间大小(含三种类别:内存、Disk和Tachyon)。这些信息都保存在org.apache.spark.storage.BlockManagerMasterActor的三个数据结构中:
(1)private val blockManagerInfo=new mutable.HashMap[BlockManagerId,BlockManagerInfo]:保存了BlockManagerId到BlockManagerInfo的映射。BlockManagerInfo保存了Slave节点的内存使用情况、Slave上的Block的状态、Slave上的BlockManagerSlaveActor的Reference(Master通过这个Reference可以向Slave发送命令和查询请求)。
(2)private val blockManagerIdByExecutor=new mutable.HashMap[String,BlockManagerId]:保存了Executor ID到BlockManagerId的映射。这样Master就可以通过Executor ID快速查找到BlockManagerId。
(3)private val blockLocations=new JHashMap[BlockId,mutable.HashSet[BlockManagerId]]:保存Block是在哪些BlockManager上的Hash Map;由于Block可能在多个Slave上都有备份,因此注意Value是一个mutable.HashSet。通过查询blockLocations就可以找到某个Block所在的物理位置了。

2,源码组织结构

org.apache.spark.storage.BlockManager是Storage模块与其他模块交互最主要的类,它提供了读和写Block的接口。这里的Block,实际上就对应了RDD中提到的Partition,每一个Partition都会对应一个Block。每个Block由唯一的Block ID(org.apache.spark.storage.RDDBlockId)标识,格式是"rdd_“+rddId+”_"+partitionId。
在这里插入图片描述
BlockManager会运行在Driver和每个Executor上。而运行在Driver上的BlockManger负责整个Application的Block的管理工作;运行在Executor上的BlockManger负责管理该Executor上的Block,并且向Driver的BlockManager汇报Block的信息和接收来自它的命令。
在这里插入图片描述
Storage模块,各个主要类的功能说明如下:
(1)org.apache.spark.storage.BlockManager:提供了Storage模块与其他模块的交互接口,管理Storage模块。
(2)org.apache.spark.storage.BlockManagerMaster:Block管理的接口类,主要通过调用org.apache.spark.storage.BlockManagerMasterActor来完成。
(3)org.apache.spark.storage.BlockManagerMasterActor:在Driver节点上的Actor,负责track所有Slave节点的Block的信息。
(4)org.apache.spark.storage.BlockManagerSlaveActor:运行在所有的节点上,接收来自org.apache.spark.storage.BlockManagerMasterActor的命令,比如删除某个RDD的数据、删除某个Block、删除某个Shuffle数据、返回某些Block的状态等。
(5)org.apache.spark.storage.BlockObjectWriter:一个抽象类,可以将任何的JVM Object写入外部存储系统。注意,它不支持并发的写操作。
(6)org.apache.spark.storage.DiskBlockObjectWriter:支持直接写入一个文件到Disk,并且还支持文件的追加。实际上它是org.apache.spark.storage.BlockObjectWriter的一个实现。下面的类在需要写入数据到Disk时,就是通过它来完成的:
a)org.apache.spark.util.collection.ExternalSorter
b)org.apache.spark.shuffle.FileShuffleBlockManager
(7)org.apache.spark.storage.DiskBlockManager:管理和维护了逻辑上的Block和存储在Disk上的物理的Block的映射。一般来说,一个逻辑的Block会根据它的BlockId生成的名字映射到一个物理上的文件。这些物理文件会被hash到由spark.local.dir(或者通过SPARK_LOCAL_DIRS)设置的目录中。
(8)org.apache.spark.storage.BlockStore:存储Block的抽象类。现在它的实现有:
a)org.apache.spark.storage.DiskStore
b)org.apache.spark.storage.MemoryStore
c)org.apache.spark.storage.TachyonStore
(9)org.apache.spark.storage.DiskStore:实现了存储Block到Disk上。其中写Disk是通过org.apache.spark.storage.DiskBlockObjectWriter实现的。
(10)org.apache.spark.storage.MemoryStore:实现了存储Block到内存中。
(11)org.apache.spark.storage.TachyonStore:实现了存储Block到Tachyon上。
(12)org.apache.spark.storage.TachyonBlockManager:管理和维护逻辑上的Block和Tachyon文件系统上的文件之间的映射。这点和org.apache.spark.storage.DiskBlockManager功能类似。

3,Master和Slave的消息传递详解

通过对消息传递协议的梳理,可以更加清楚地理解Storage模块在Driver和Executor之间的控制信息是如何传递的;还可以了解Storage模块管理Block的框架和脉络。
这里的Master指的是org.apache.spark.storage.BlockManagerMasterActor,它运行在Driver端,通过保存Slave的Actor Reference向Slave发送消息;Slave指的是org.apache.spark.storage.BlockManagerSlaveActor,它运行在Executor端,每个Executor都有一个,主要的功能是接收来自Master的命令,做一些清理工作并且响应Master获取Block的状态的请求,同时Executor都会保存有org.apache.spark.storage.BlockManagerMasterActor的Actor Reference,Executor通过这个Reference和Master进行通信。

(1)Master到Slave消息详解

Slave有任何的Block的状态更新,都会主动通过Master Actor Reference上报到Master。而从Master到Slave的消息主要是由Master向Slave发送的控制信息或者获取状态的请求。控制信息主要指删除Block、RDD相关的Block、广播变量相关的数据和Shuffle相关的数据。获取状态的请求主要是获取Block的状态信息和匹配的Block ID等信息。
在Slave向Master发起注册BlockManager的请求中,每个BlockManager的信息都保存在org.apache.spark.storage.BlockManagerInfo中,因此通过BlockManagerInfo就可以得到SlaveActor的Reference,进而向它发送消息。下面通过删除RDD的例子,来详细讲解这一个过程。在RDD调用了org.apache.spark.rdd.RDD#unpersist.
1)org.apache.spark.SparkContext#unpersistRDD
代码:sc.unpersistRDD(id, blocking)
2)org.apache.spark.storage.BlockManagerMaster#removeRdd
代码:env.blockManager.master.removeRdd(rddId, blocking)
3)org.apache.spark.storage.BlockManagerMaster#askDriverWithReply
代码:val future = askDriverWithReplyFuture[Seq[Int]]
4)org.apache.spark.util.AkkaUtils$#askWithReply(java.lang.Object,akka.actor.ActorRef,int,int,scala.concurrent.duration.FiniteDuration)
代码:AkkaUtils.askWithReply(message, driverActor, AKKA_RETRY_ATTEMPTS, AKKA_RETRY_INTERVAL_MS, timeout)
5)org.apache.spark.storage.BlockManagerMasterActor#receiveWithLogging
代码:case RemoveRdd(rddId) =>sender ! removeRdd(rddId))
6)org.apache.spark.storage.BlockManagerMasterActor#removeRdd:
首先需要删除这个RDD相关的元数据信息,然后删除Slave上的RDD的信息。
删除Master保存的RDD相关的元数据信息代码:

val blocks = blockLocations.keys.flatMap(_.asRDDId).filter(_.rddId == rddId)
blocks.foreach { blockId =>val bms: mutable.HashSet[BlockManagerId] = block-    Locations.get(blockId)
bms.foreach(bm => blockManagerInfo.get(bm).foreach(_.removeBlock(blockId)))    
	blockLocations.remove(blockId)
}

删除Slave上的RDD的信息代码:

val removeMsg = RemoveRdd(rddId)
Future.sequence(
    blockManagerInfo.values.map { bm =>
		bm.slaveActor.ask(removeMsg)(akkaTimeout).mapTo[Int]    
	}.toSeq
)

在Slave接到RemoveRdd的消息后,会调用BlockManager删除RDD:

case RemoveRdd(rddId) =>
    doAsync[Int]("removing RDD " + rddId, sender) {
		blockManager.removeRdd(rddId)
}

BlockManager端的代码实现:

def removeRdd(rddId: Int): Int = {
    logInfo(s"Removing RDD $rddId")
    val blocksToRemove = blockInfo.keys.flatMap(_.asRDDId).filter(_.rddId == rddId)
    blocksToRemove.foreach { blockId => removeBlock(blockId, tellMaster = false) }
    blocksToRemove.size
}

从Master Actor传到Slave Actor的简要说明:
(1)RemoveBlock(blockId:BlockId):根据blockId删除该Executor上的Block,实际上通过调用org.apache.spark.storage.BlockManager#removeBlock删除。
(2)RemoveRdd(rddId:Int):根据rddId删除该Executor上RDD所关联的所有Block,实际上通过调用org.apache.spark.storage.BlockManager#removeRdd删除。
(3)RemoveShuffle(shuffleId:Int):根据shuffleId删除该Executor上所有和该Shuffle相关的Block,需要通过两步实现:
a)org.apache.spark.MapOutputTracker#unregisterShuffle
b)org.apache.spark.shuffle.ShuffleManager#unregisterShuffle
(4)RemoveBroadcast(broadcastId:Long,removeFromDriver:Boolean=true):根据broadcastId删除该Executor和该广播变量相关的所有Block,通过org.apache.spark.storage.BlockManager#removeBroadcast实现。
(5)GetBlockStatus(blockId:BlockId,):根据blockId向Master返回该Block的Status。这个一般都是测试使用的,注意这个操作非常耗时。实现方式:org.apache.spark.storage.BlockManager#getStatus。
(6)GetMatchingBlockIds(filter,
):根据filter向Master返回符合filter的所有BlockId。通过org.apache.spark.storage.BlockManager#getMatchingBlockIds实现。这个和第5个消息一样,也非常耗时,一般都是在测试中使用的。

(2)Slave到Master消息详解

BlockManager通过调用org.apache.spark.storage.BlockManager#reportBlockStatus来汇报一个Block的状态。那么什么情况下会汇报Block的状态呢?当然是Block的状态改变的时候。比如通过org.apache.spark.storage.BlockManager#dropFromMemory将某个Block从内存中移出;比如通过org.apache.spark.storage.BlockManager#remove-Block删除一个Block并且需要通知Master时;比如通过org.apache.spark.storage.BlockManager#dropOldBlocks移除一个Block时;比如通过org.apache.spark.storage.BlockManager#doPut写入一个新的Block时.
org.apache.spark.storage.BlockManager#reportBlockStatus之后的调用栈如下:
1)org.apache.spark.storage.BlockManager#tryToReportBlockStatus
代码:

val needReregister = !tryToReportBlockStatus(blockId, info, status, 
    droppedMemorySize)

2)org.apache.spark.storage.BlockManagerMaster#updateBlockInfo
代码:

master.updateBlockInfo( blockManagerId, blockId, storageLevel, inMemSize, 
    onDiskSize, inTachyonSize)

3)org.apache.spark.storage.BlockManagerMaster#askDriverWithReply
代码:

val res = askDriverWithReply[Boolean](UpdateBlockInfo(blockManagerId,
    blockId, storageLevel, memSize, diskSize, tachyonSize))

4)org.apache.spark.util.AkkaUtils$#askWithReply(java.lang.Object,akka.actor.ActorRef,int,int,scala.concurrent.duration.FiniteDuration)
相关代码:

AkkaUtils.askWithReply(message, driverActor, AKKA_
    RETRY_ATTEMPTS, AKKA_RETRY_INTERVAL_MS, timeout)

5)org.apache.spark.storage.BlockManagerMasterActor#receiveWithLogging
代码:

case UpdateBlockInfo(
blockManagerId, blockId, storageLevel, deserializedSize, size, tachyonSize) =>
sender !updateBlockInfo(
blockManagerId, blockId, storageLevel, deserializedSize, size, tachyonSize)

6)org.apache.spark.storage.BlockManagerMasterActor#updateBlockInfo
相关实现:这个调用会进行Master端控制信息的更新。主要更新两个数据结构:
a)blockManagerInfo中是blockManagerId为key,BlockManagerInfo为value的HashMap。
b)blockLocations中是blockId为key,mutable.HashSet[BlockManagerId]为value的HashMap。

一般来说,Slave向Master汇报信息获取查询信息,调用栈都是类似的:
1)org.apache.spark.storage.BlockManager
2)org.apache.spark.storage.BlockManagerMaster
3)org.apache.spark.storage.BlockManagerMasterActor
其中2~3实际上是需要通过网络的,即通过BlockManagerMaster持有的Master Actor Reference向Master Actor发送消息。
下面14个消息都是从Master Actor传到Slave Actor的简要说明。如果对实现感兴趣,可以深入阅读一下相关的实现,这里不再赘述。
1)RegisterBlockManager(blockManagerId:BlockManagerId,maxMemSize:Long,sender:ActorRef):由org.apache.spark.storage.BlockManagerMaster向org.apache.spark.storage.BlockManagerMasterActor发起的注册。通过注册,Master Actor会保存该BlockManger所包含的Block等信息。
2)UpdateBlockInfo(var blockManagerId:BlockManagerId,var blockId:BlockId,var storageLevel:StorageLevel,var memSize:Long,var diskSize:Long,vartachyonSize:Long):向Master汇报Block的信息,Master会记录这些信息并且供Slave查询。
3)GetLocations(blockId:BlockId):获得某个Block所在的位置信息,返回由org.apache.spark.storage.BlockManagerId组成的列表(包含Executor ID,Executor所在的hostname和开放的port号)。注意:Block可能在多个节点上都有备份。
4)GetLocationsMultipleBlockIds(blockIds:Array[BlockId]):与GetLocations(blockId:BlockId)基本相同,区别是它可以一次获取多个Block的位置信息。
5)GetPeers(blockManagerId:BlockManagerId):获得其他的BlockManager。这个在做Block的副本时会用到。BlockManager通过org.apache.spark.storage.BlockManager#doPut去执行写Block的操作。在写Block的时候如果需要写副本,那么需要调用org.apache.spark.storage.BlockManager#replicate。在复制的时候会获取其他的BlockManager的位置信息,相对于它自身,这些BlockManager就是它的副本了。
6)GetActorSystemHostPortForExecutor(executorId:String):根据executorId获取Executor的Thread Dump,是由org.apache.spark.SparkContext#getExecutorThreadDump发起的调用,它在获得了Executor的hostname和port后,会通过AKKA向Executor发送请求信息。实际上获取Thead Dump最开始是由org.apache.spark.ui.exec.Executor-ThreadDumpPage发起的。
7)RemoveExecutor(execId:String):删除Master上保存的execId对应的Executor上的BlockManager的信息。
8)StopBlockManagerMaster:在停止org.apache.spark.storage.BlockManagerMaster时调用。它会停止Master的Actor。实际上这是Spark的Driver和Executor退出的时候发起的停止动作。
9)GetMemoryStatus:获取所有Executor的内存使用的状态,包括使用的最大的内存大小、剩余的内存大小。
10)GetStorageStatus:返回每个Executor的Storage的状态,包括每个Executor最大可用的内存数和Block的信息。
11)GetBlockStatus(blockId:BlockId,askSlaves:Boolean=true):根据blockId获取Block的Status。如果askSlaves为true,那么需要到所有的Slave上查询结果,因此可能会非常耗时。而且在某种情况下可能某些Block的状态并没有上报。推荐还是作为一个测试项使用。
12)GetMatchingBlockIds(filter:BlockId=>Boolean,askSlaves:Boolean=true):和GetBlockStatus类似,只不过这个是根据filter获取。
13)BlockManagerHeartbeat(blockManagerId:BlockManagerId):前面提到过Slave和Master的心跳的实现。实际上,Slave的BlockManager并不会主动发起心跳,而是通过Executor和Driver之间的心跳来实现的。Driver在创建SparkContext的时候就会初始化org.apache.spark.HeartbeatReceiver,这是一个Actor,而Executor启动时会创建这个Actor的Reference。然后Executor就通过Actor之间的通信来维持这个心跳。Driver端在收到心跳后,会调用org.apache.spark.scheduler.TaskScheduler#executorHeartbeatReceived,而TaskScheduler会调用DAGScheduler向org.apache.spark.storage.BlockManagerMasterActor发送心跳。
14)ExpireDeadHosts:删除心跳超时的BlockManager。心跳检测的线程是在Master Actor的preStart()里启动的,默认的超时时间是60秒。可以通过spark.storage.blockManagerTimeoutIntervalMs来设置,注意时间单位是毫秒。

二、存储实现详解

1,存储级别

存储级别,对用户来说是RDD相关的持久化和缓存。这实际上也Spark最重要的特征之一。在每个节点都将RDD的Partition的数据保存在内存中时,后续的计算将会变得非常快(通常会快10倍以上)。可以说,缓存是Spark构建迭代式算法和快速交互式查询的关键。

用户可以直接调用persist()或者cache()来标记一个RDD需要持久化。实际上,cache()是使用默认存储级别的快捷方法:

/** Persist this RDD with the default storage level (`MEMORY_ONLY`). */
def persist(): this.type = persist(StorageLevel.MEMORY_ONLY)
/** Persist this RDD with the default storage level (`MEMORY_ONLY`). */
def cache(): this.type = persist()

只有触发了一个Action后,计算才会提交到集群开始真正的运算。因此RDD只有经过一次Action后,才能将RDD缓存到内存中以供以后的计算使用。这个缓存也有容错机制,如果某个缓存丢失了,那么会通过原来的计算过程进行重算。
如果用户有特殊需求,可以调用def persist(newLevel:StorageLevel)来设置需要的存储级别。不同的存储级别,可以选择持久化数据到Disk、Memory、Tachyon;还可以选择数据是否需要序列化从而节省空间;甚至可以将数据远程复制到其他节点。

2,存储级别的定义

RDD的Partition和Storage模块的Block是一一对应的关系。
在这里插入图片描述
在这里插入图片描述

3,选择合适的存储级别

Spark不同的存储级别是内存使用和CPU效率的折中。Spark官网建议按照以下步骤来选择合适的存储级别:
(1)如果你的RDD可以和默认的存储级别有很好的契合,那么就无需任何特殊的设定了。默认的存储级别是CPU最高效的选项,也是运算能够最快完成的选项。
(2)如果不行,那么需要减少内存的使用,可以使用MEMORY_ONLY_SER。这时候需要选择一个合适的序列化方案,可以参考http://spark.apache.org/docs/latest/tuning.html#data-serialization。需要在空间效率和反序列化时所需要的CPU中做一个合适的选择。
(3)尽量不要落到硬盘上,除非是计算逻辑非常复杂,或者是需要从一个超大规模的数据集过滤出一小部分数据。否则重新计算一个Partition的速度可能和从硬盘读差不多(考虑到出错的概率和写硬盘的开销,因此采用失败重算要比读硬盘持久化的数据要好)。
(4)如果你需要故障的快速恢复能力(比如使用Spark来处理Web的请求),那么可以考虑使用存储级别的多副本机制。实际上所有的存储级别都提供了Partition数据丢失时的重算机制,只不过有备份的话可以让Application直接使用副本而无须等待重新计算丢失的Partition数据。
(5)如果集群有大量的内存或者有很多的运行任务,则选择OFF_HEAP。现在处于试验阶段的OFF_HEAP有以下的优势:
a)它使得多个Executor可以共享一个内存池。
b)它显著地减少了GC的开销。
c)缓存在内存中的数据即使是产生它的Executor异常退出了也不会丢失。

4,Storage模块类图

org.apache.spark.storage.BlockStore:存储Block的抽象类。它的实现有:
(1)org.apache.spark.storage.DiskStore
(2)org.apache.spark.storage.MemoryStore
(3)org.apache.spark.storage.TachyonStore
在这里插入图片描述
通过org.apache.spark.storage.BlockStore来看一下实现类需要的接口名字及接口的功能:

private[spark] abstract class BlockStore(val blockManager: BlockManager)
    extends Logging {
    //根据StorageLevel将blockId标识的Block的内容bytes写入系统
    def putBytes(blockId: BlockId, bytes: ByteBuffer, level: StorageLevel):
		PutResult
	//将values写入系统。如果returnValues = true,需要将结果写入PutResult
    def putIterator(
		blockId: BlockId,
        values: Iterator[Any],
        level: StorageLevel,
        returnValues: Boolean): PutResult
	//同上,只不过由Iterator编程Array
    def putArray(
		blockId: BlockId,
        values: Array[Any],
        level: StorageLevel,
        returnValues: Boolean): PutResult
	// 获得Block的大小
    def getSize(blockId: BlockId): Long
    //获得Block的数据,返回类型ByteBuffer
    def getBytes(blockId: BlockId): Option[ByteBuffer]
    //获得Block的数据,返回类型Iterator[Any]
    def getValues(blockId: BlockId): Option[Iterator[Any]]
    //删除Block, 成功返回true,否则false
    def remove(blockId: BlockId): Boolean
    //查询是否包含某个Block
    def contains(blockId: BlockId): Boolean
    //退出时清理回收资源
    def clear() { }
}

5,org.apache.spark.storage.DiskStore实现详解

DiskStore通过org.apache.spark.storage.DiskBlockManager来管理文件。前面介绍过,DiskBlockManager管理和维护了逻辑上的Block和存储在Disk上物理的Block的映射。一般来说,一个逻辑的Block会根据它的blockId生成的名字映射到一个物理上的文件。这些物理文件会被hash到由spark.local.dir(或者SPARK_LOCAL_DIRS)设置的目录中。
一般来说,DiskStore会通过blockId从DiskBlockManager获取一个文件句柄,然后通过这个文件句柄来读写文件:

val file = diskManager.getFile(blockId) //获取文件句柄
val channel = new FileOutputStream(file).getChannel
while (bytes.remaining > 0) {
    channel.write(bytes)
}
channel.close()

spark.local.dir(或者SPARK_LOCAL_DIRS)可以设置多个目录。DiskBlock-Manager会为Executor在每个目录下创建一个子目录,子目录的命名方式是“spark-local-yyyyMMddHHmmss-xxxx”,其中,最后的xxxx是一个随机数,这个实现的逻辑在org.apache.spark.storage.DiskBlockManager#createLocalDirs中。而每个“spark-local-yyyyMMddHHmmss-xxxx”目录下,会根据需要生成个数至多为spark.diskStore.subDirectories(默认值为64)的子目录,子目录以数字命名(从00到文件个数)。图8-6表示了用户在spark.local.dir中设置了3个目录,每个目录下有64个子目录的目录布局。
在这里插入图片描述
根据文件名的hash值取得其应该存放的一级目录(即“spark-local-yyyyMMddHHmmss-xxxx”),然后根据该hash值取得其应该存放的二级目录(即一级目录的子目录)。主要的实现如下:

val hash = Utils.nonNegativeHash(filename)
val dirId = hash % localDirs.length
val subDirId = (hash / localDirs.length) % subDirsPerLocalDir

注意:二级目录不一定存在,因此在返回前需要确认,如果不存在则需要创建:

var subDir = subDirs(dirId)(subDirId)
if (subDir == null) {
	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() && !newDir.mkdir()) {
				throw new IOException(s"Failed to create local dir in $newDir.")
			}
			subDirs(dirId)(subDirId) = newDir
		newDir
		}
	}
}

为了避免引入大量文件造成的内存压力和随机Disk IO的性能问题,引入了Shuffle Consolidate Files机制。这里重要的是FileSegment的实现:

def getBytes(segment: FileSegment): Option[ByteBuffer] = {
    // 根据文件句柄,开始的offset和要读取的长度来读取文件
    getBytes(segment.file, segment.offset, segment.length)
}

6,org.apache.spark.storage.MemoryStore实现详解

MemoryStore实际上是使用一个Hash Map来保存Block的数据:

private val entries = new LinkedHashMap[BlockId, MemoryEntry](32, 0.75f, true)

entries保存了Block的数据,因此Block的读取和写入都是围绕entries展开的。除了在内存紧张的情况下,新的缓存的加入可能需要将部分老的缓存清除的逻辑外,这部分的实现还是很简单的。
所有对外提供的写入的接口,最终都是通过调用org.apache.spark.storage.MemoryStore的私有函数tryToPut来完成的。tryToPut会检查当前的内存是否可以容下当前的请求,如果可以,则放入这个Hash Map,期间可能会淘汰老的缓存;如果内存超过了当前的最大内存,那么就会返回调用者,如果存储级别还有Disk,那么会将当前的数据通过DiskStore写入Disk,否则缓存将不会被持久化,下次用的话还需要重新计算。

在org.apache.spark.storage.MemoryStore的ensureFreeSpace实现:

if (space > maxMemory) { //无法载入内存
    return ResultWithDroppedBlocks(success = false, droppedBlocks)
}
val actualFreeMemory = freeMemory - currentUnrollMemory
if (actualFreeMemory < space) {
    //如果可用内存不足以存储当期的Block,那么淘汰部分Block
    val rddToAdd = getRddId(blockIdToAdd)
    val selectedBlocks = new ArrayBuffer[BlockId]
    var selectedMemory = 0L
    // 保证这个过程中entries不会改变,否则同时遍历entrie时会导致异常
    entries.synchronized {
		val iterator = entries.entrySet().iterator()
        while (actualFreeMemory + selectedMemory < space && iterator.hasNext) {
			val pair = iterator.next()
            val blockId = pair.getKey
            if (rddToAdd.isEmpty || rddToAdd != getRddId(blockId)) {
				selectedBlocks += blockId //这个Block将会被移出内存
                selectedMemory += pair.getValue.size
				}
			}
		}
		if (actualFreeMemory + selectedMemory >= space) {
			for (blockId <- selectedBlocks) {
				//根据entries装入data,此处忽略一些代码
				val droppedBlockStatus = blockManager.dropFromMemory(blockId, data)
				droppedBlockStatus.foreach { status => droppedBlocks += ((blockId, status)) }
			}
		}

7,org.apache.spark.storage.TachyonStore实现详解

TachyonStore实现了Spark将缓存的数据放到Tachyon中。这个实现可以看作是实现了一个Tachyon客户端,通过这个客户端Spark可以读写Tachyon的数据。而org.apache.spark.storage.TachyonStore作为BlockStore的一个实现,并没有复杂的逻辑,关于Tachyon文件的读取和写入都是通过org.apache.spark.storage.TachyonBlockManager来完成的。

TachyonStore读取一个Block:

override def getBytes(blockId: BlockId): Option[ByteBuffer] = {
	val file = tachyonManager.getFile(blockId)
    if (file == null || file.getLocationHosts.size == 0) {
		return None
	}
    val is = file.getInStream(ReadType.CACHE)
    try {
		val size = file.length
        val bs = new Array[Byte](size.asInstanceOf[Int])
        ByteStreams.readFully(is, bs)
        Some(ByteBuffer.wrap(bs))
	} catch {//省略了异常处理的代码
    } finally {
		is.close()
	}
}

与读一个Block类似,也是从TachyonBlockManager获取Block的文件句柄,然后就可以写入了。
为每个Executor生成不同的root dir:

val storeDir = conf.get("spark.tachyonStore.baseDir", "/tmp_spark_tachyon")
val appFolderName = conf.get("spark.tachyonStore.folderName")
val tachyonStorePath = s"$storeDir/$appFolderName/${this.executorId}"

不过目录还是要加上spark-tachyon-yyyyMMddHHmmss-xxxx,然后在这个目录下面还是会有spark.tachyonStore.subDirectories(默认值也是64)个数的子目录。

8,Block存储的实现

如果用户设置的存储级别不是StorageLevel.NONE,那么在计算的时候,会首先通过org.apache.spark.CacheManager来判断结果是否已经缓存,如果是,则直接读取缓存,否则开始计算,并且将计算的结果根据存储级别进行缓存。
代码实现:

SparkEnv.get.cacheManager.getOrCompute(this, split, context, storageLevel)

如果org.apache.spark.CacheManager在缓存中没有找到计算需要的结果,那么它需要RDD开始计算:

val computedValues = rdd.computeOrReadCheckpoint(partition, context)

而计算的结果会通过调用org.apache.spark.storage.BlockManager#putArray写入缓存:

blockManager.putArray(key, arr, level, tellMaster = true, effectiveStorageLevel)

tellMaster=true表示这个更新需要上报到Master;而effectiveStorageLevel表示了存储级别。而org.apache.spark.storage.BlockManager#putArray会调用org.apache.spark.storage.BlockManager#doPut。doPut是一个BlockManager的私有接口,供自身调用,它会根据存储级别决定缓存存储的方式,比如是Memory、Disk还是Tachyon;还有是否需要远程复制到其他的节点进行备份等。

实际上BlockManager对外开放了三个接口来做数据的put:
(1)org.apache.spark.storage.BlockManager#putIterator
(2)org.apache.spark.storage.BlockManager#putArray
(3)org.apache.spark.storage.BlockManager#putBytes

对于需要远程复制的,不同的调用将会有不同的处理:
1)如果调用来自org.apache.spark.storage.BlockManager#putBytes,那么在刚开始的时候就会启动复制的动作,因此这时候数据可以直接使用,这个由doPut传入数据的类型是否是ByteBufferValues来确定。
2)如果来自其他的调用,因为复制需要获取数据,而这个数据有可能还需要序列化,因此这里的做法是调用BlockStore去缓存数据,并且对于需要远程复制的请求,告知BlockStore调用者需要缓存数据,这样就可以通过这个数据进行远程复制了。以org.apache.spark.storage.BlockStore#putIterator为例,它的参数是(blockId:BlockId,values:Iterator[Any],level:StorageLevel,returnValues:Boolean),最后一个参数就是表明调用者需要缓存的数据,这个数据通过返回值org.apache.spark.storage.PutResult返回。

三、性能调优

1,spark.local.dir

这个目录用于写中间数据,如RDD Cache、Shuffle时存储数据的位置。
首先,最基本的是我们可以配置多个路径(用逗号分隔)到多个磁盘上增加整体IO带宽。
其次,一个逻辑的Block会根据它的blockId生成的名字映射到一个物理上的文件。这些物理文件会被hash到由spark.local.dir(或者SPARK_LOCAL_DIRS)设置的目录中。因此,如果存储设备的读写速度不一样,那么可以在较快的存储设备上配置更多的目录来增加它被使用的比例,从而更好地利用快速存储设备。在Spark能够感知具体的存储设备类型前,这个变通方法可以取得一个不错的效果。
此外,还需要注意的是,在Spark 1.0以后,SPARK_LOCAL_DIRS(Standalone,Mesos)或者LOCAL_DIRS(YARN)参数会覆盖这个配置。比如YARN模式的时候,Spark Executor的本地路径依赖于YARN的配置,而不取决于这个参数。

2,spark.storage.memoryFraction

spark.executor.memory决定了每个Executor可用内存的大小,那么spark.storage.memoryFraction则决定了在这部分内存中有多少可以用于Memory Store管理RDD Cache数据,多少内存用来满足任务运行时各种其他内存空间的需要。
spark.executor.memory默认值为0.6,官方文档建议这个比值不要超过JVM Old Gen区域的比值。这也很容易理解,因为RDD Cache数据通常都是长期驻留内存的,也就是说最终会被转移到Old Gen区域(如果该RDD还没有被删除的话),如果这部分数据允许的尺寸太大,势必把Old Gen区域占满,造成频繁的全量的垃圾回收。
如何调整这个比值,取决于你的应用对数据的使用模式和数据的规模,粗略地来说,如果频繁发生全量的垃圾回收,可以考虑降低这个比值,这样RDD Cache可用的内存空间减少(剩下的部分Cache数据就需要通过Disk Store写到磁盘上了),虽然会带来一定的性能损失,但是腾出更多的内存空间用于执行任务,减少全量的垃圾回收发生的次数,反而可能改善程序运行的整体性能。

3,spark.streaming.blockInterval

这个参数用来设置Spark Streaming里Stream Receiver生成Block的时间间隔,默认为200ms。具体表现为具体的Receiver所接收的数据,以相同的时间间隔,同期性地从Buffer中生成一个StreamBlock放进队列,等待进一步被存储到BlockManager中供后续计算过程使用。从理论上来说,为了保证每个StreamingBatch间隔里的数据是均匀的,这个时间间隔应该能被Batch的间隔时间长度整除。总体来说,如果内存大小够用,Streaming的数据来得及处理,这个时间间隔的影响不大,当然,如果数据存储级别是Memory+Ser,即做了序列化处理,那么时间间隔的大小会影响序列化后数据块的大小,对于Java的垃圾回收的行为会有一些影响。
此外,spark.streaming.blockQueueSize决定了在StreamBlock被存储到Block-Mananger之前,队列中最多可以容纳多少个StreamBlock。默认为10,因为这个队列轮询的时间间隔是100ms,所以如果CPU不是特别繁忙的话,基本上应该没有问题。

文章来源:《Spark技术内幕:深入解析Spark内核架构设计与实现原理》 作者:张安站

文章内容仅供学习交流,如有侵犯,联系删除哦!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

晓之以理的喵~~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值