本书在5.7节曾介绍过存储体系的创建,那时只为帮助读者了解SparkEnv,现在是时候对Spark的存储体系进行详细的分析了。简单来讲,Spark存储体系是各个Driver、Executor实例中的BlockManager所组成的。但是从一个整体出发,把各个节点的BlockManager看成存储体系的一部分,那么存储体系还有更多衍生内容,比如块传输服务、map任务输出跟踪器、Shuffle管理器等。
在正式介绍存储体系的内容之前,如果能对存储体系从整体上有个宏观的认识,无疑有利于读者对后续内容的理解。图1能够从整体上表示存储体系架构。
从图1,我们可以看到BlockManager依托于很多组件的服务,这些组件包括:
小贴士:在Spark 1.x.x版本中还提供了TachyonStore,TachyonStore负责向Tachyon中存储数据。Tachyon也诞生于UCBerkeley的AMP实验室,是以内存为中心的高容错的分布式文件系统,能够为内存计算框架(比如Spark、MapReduce等)提供可靠的内存级的文件共享服务。从软件栈的层次来看,Tachyon是位于现有大数据计算框架和大数据存储系统之间的独立的一层。它利用底层文件系统作为备份,对于上层应用来说,Tachyon就是一个分布式文件系统。自Spark 2.0.0版本开始,TachyonStore已经被移除。Tachyon发展到1.0.0版本时改名为Alluxio。更多Alluxio的信息,请访问http://www.alluxio.org。
存储体系中有一些概念,需要详细说明。正所谓“磨刀不误砍柴工”,有了对这些概念的理解,后面的内容理解起来会更加容易。
根据之前的了解,我们知道在Driver或者Executor中有任务执行的环境SparkEnv。每个SparkEnv中都有BlockManager,这些BlockManager位于不同的节点和实例上。BlockManager之间需要通过RpcEnv、shuffleClient以及BlockTransferService相互通信,所以大家需要互相认识,正如每个人都有身份证号一样,每个 BlockManager都有其在Spark集群内的唯一标识。BlockManagerId就是BlockManager的身份证。
Spark通过BlockManagerId中的host、port、executorId等信息来区分BlockManager。BlockManagerId中的属性包括:
理解了BlockManagerId中的属性,来看看它提供的方法:
代码清单1 writeExternal的实现
override def writeExternal(out: ObjectOutput): Unit = Utils.tryOrIOException {
out.writeUTF(executorId_)
out.writeUTF(host_)
out.writeInt(port_)
out.writeBoolean(topologyInfo_.isDefined)
// we only write topologyInfo if we have it
topologyInfo.foreach(out.writeUTF(_))
}
代码清单2 readExternal的实现
override def readExternal(in: ObjectInput): Unit = Utils.tryOrIOException {
executorId_ = in.readUTF()
host_ = in.readUTF()
port_ = in.readInt()
val isTopologyInfoAvailable = in.readBoolean()
topologyInfo_ = if (isTopologyInfoAvailable) Option(in.readUTF()) else None
}
了解操作系统存储的读者,应该知道文件系统的文件在存储到磁盘上时,都是以块为单位写入的。操作系统的块都是以固定的大小读写的,例如512字节、1024字节、2048字节、4096字节等。
在Spark的存储体系中,数据的读写也是以块为单位,只不过这个块并非操作系统的块,而是设计用于Spark存储体系的块。每个Block都有唯一的标识,Spark把这个标识抽象为BlockId。抽象类BlockId的定义见代码清单3。
代码清单3 BlockId的定义
@DeveloperApi
sealed abstract class BlockId {
def name: String
def asRDDId: Option[RDDBlockId] = if (isRDD) Some(asInstanceOf[RDDBlockId]) else None
def isRDD: Boolean = isInstanceOf[RDDBlockId]
def isShuffle: Boolean = isInstanceOf[ShuffleBlockId]
def isBroadcast: Boolean = isInstanceOf[BroadcastBlockId]
override def toString: String = name
override def hashCode: Int = name.hashCode
override def equals(other: Any): Boolean = other match {
case o: BlockId => getClass == o.getClass && name.equals(o.name)
case _ => false
}
}
根据代码清单3,BlockId中定义了以下方法:
BlockId有很多子类,例如:RDDBlockId、ShuffleBlockId、BroadcastBlockId等。BroadcastBlockId曾在《Spark内核设计的艺术》一书的5.5节介绍广播管理器BroadcastManager时介绍过, BlockId的子类都是用相似的方式实现的。
前文提到Spark的存储体系包括磁盘存储与内存存储。Spark将内存又分为堆外内存和堆内存。有些数据块本身支持序列化及反序列化,有些数据块还支持备份与复制。Spark存储体系将以上这些数据块的不同特性,抽象为存储级别(StorageLevel)。
分析StorageLevel,也是从其成员属性开始。StorageLevel中的成员属性如下:
有了对StorageLevel中属性的了解,现在来看看StorageLevel提供了哪些方法?
(useMemory || useDisk) && (replication > 0)
代码清单4 toInt的实现
def toInt: Int = {
var ret = 0
if (_useDisk) {
ret |= 8
}
if (_useMemory) {
ret |= 4
}
if (_useOffHeap) {
ret |= 2
}
if (_deserialized) {
ret |= 1
}
ret
}
根据代码清单4,toInt方法实际是把StorageLevel的_useDisk、_useMemory、_useOffHeap、_deserialized这4个属性设置到四位数字的各个状态位。例如,1000表示存储级别为允许写入磁盘;1100表示存储级别为允许写入磁盘和堆内存;1111表示存储级别为允许写入磁盘、堆内存及堆外内存,并且需要反序列化。
代码清单5 writeExternal的实现
override def writeExternal(out: ObjectOutput): Unit = Utils.tryOrIOException {
out.writeByte(toInt)
out.writeByte(_replication)
}
代码清单6 readExternal的实现
override def readExternal(in: ObjectInput): Unit = Utils.tryOrIOException {
val flags = in.readByte()
_useDisk = (flags & 8) != 0
_useMemory = (flags & 4) != 0
_useOffHeap = (flags & 2) != 0
_deserialized = (flags & 1) != 0
_replication = in.readByte()
}
了解了StorageLevel原生类提供的这些方法,我们再来看看StorageLevel的伴生对象。由于StorageLevel的构造器是私有的,所以StorageLevel的伴生对象中已经预先定义了很多存储体系需要的StorageLevel,见代码清单7。
代码清单7 内置的StorageLevel
val NONE = new StorageLevel(false, false, false, false)
val DISK_ONLY = new StorageLevel(true, false, false, false)
val DISK_ONLY_2 = new StorageLevel(true, false, false, false, 2)
val MEMORY_ONLY = new StorageLevel(false, true, false, true)
val MEMORY_ONLY_2 = new StorageLevel(false, true, false, true, 2)
val MEMORY_ONLY_SER = new StorageLevel(false, true, false, false)
val MEMORY_ONLY_SER_2 = new StorageLevel(false, true, false, false, 2)
val MEMORY_AND_DISK = new StorageLevel(true, true, false, true)
val MEMORY_AND_DISK_2 = new StorageLevel(true, true, false, true, 2)
val MEMORY_AND_DISK_SER = new StorageLevel(true, true, false, false)
val MEMORY_AND_DISK_SER_2 = new StorageLevel(true, true, false, false, 2)
val OFF_HEAP = new StorageLevel(true, true, true, false, 1)
代码清单7所示的代码调用StorageLevel的构造器创建了多种存储级别。StorageLevel私有构造器的参数从左至右分别为_useDisk、_useMemory、_useOffHeap、_deserialized、_replication。
BlockInfo用于描述块的元数据信息,包括存储级别、Block类型、大小、锁信息等。按照惯例,我们先看看BlockInfo的成员属性:
有了对BlockInfo的了解,现在看看BlockInfo提供的方法:
BlockResult用于封装从本地的BlockManager中获取的Block数据以及与Block相关联的度量数据。BlockResult中有以下属性:
样例类BlockStatus用于封装Block的状态信息,包括: