RDD(Resilient Distributed Datasets,弹性分布式数据集)代表可并行操作元素的不可变分区集合。
1 为什么需要RDD
1.1 数据处理模型
RDD是一个容错的、并行的数据结构,可以控制将数据存储到磁盘或内存,能够获取数据的分区。RDD提供了一组类似于Scala的操作,比如map、flatMap、filter、reduceByKey、join、mapPartitions等,这些操作实际是对RDD进行转换(transformation)。此外,RDD还提供了collect、foreach、count、reduce、countByKey等操作完成数据计算的动作(action)
当前的大数据应用场景非常丰富,如流式计算、图计算、机器学习等,它们既有相信之处 ,又各有不同。为了能够对所有场景下的数据处理使用统一的方式,抽象出RDD这一模型。
1.2 依赖划分原则
一个RDD包含一个或者多个分区,每个分区实际是一个数据集合的片段。在构建DAG的过程中,会将RDD用依赖关系串联起来。每个RDD都有其依赖(除了最顶级RDD的依赖是空列表),这些依赖分为窄依赖(即NarrowDependency)和Shuffle依赖(即ShuffleDependcy,也称为宽依赖)两种。为什么要对依赖进行区分?从功能角度讲它们是不一样的。窄依赖会被划分到同一个Stage中,这样它们就能以管道的方式迭代执行。ShuffleDependency由于所依赖的分区Task不止一个,所以往往需要跨节点传输数据。从容灾角度讲,它们恢复计算结果的方式不同。窄依赖只需要重新执行父RDD的丢失分区的计算即可恢复,而ShuffleDependency则需要恢复所有父RDD的丢失分区。
1.3 数据处理效率
RDD的计算过程允许在多个节点并发执行。如果数据量很大,可以适当增加分区数量,这种根据硬件条件对并发任务数量的控制,能更好地利用各种资源,也能有效提高 Spark的数据处理效率。
1.4 容错处理
传统关系型数据库往往采用日志记录的方式来容灾容错,数据恢复都依赖于重新执行日志。Hadoop为了避免单机故障概率较高的问题,通常将数据备份到其它机器容灾。由于所有备份机器同时出故障的概率比单机故障概率低很多,所以在发生宕机等问题时能够从备份机读取数据。RDD本身是一个不可变的(Scala中为immutable)数据集,当某个Worker节点上的Task失败时,可以利用DAG重新调度计算这些失败的Task(执行已成功的Task可以从CheckPoint中读取,而不用重新计算)。在流式计算的场景中,Spark需要记录日志和CheckPoint,以便利用CheckPoint和日志对数据恢复。
2 RDD实现分析
抽象类RDD定义了所有RDD的规范,下面从属性开始,逐步了解RDD的实现。
- _sc:指SparkContext。_sc由@transient修饰,所以此属性不会被序列化。
- deps:构造器参数之一,是Dependency的序列,用于存储当前RDD的依赖。RDD的子类在实现时不一定会传递此参数。由于deps由@transient修饰,所以此属性不会被序列化
- partitioner:当前RDD的分区计算器。partitioner由@transient修饰,所以此属性不会被序列化
- id:当前RDD的唯一身份标识。此属性通过调用SparkContext的nextRddId属性生成
- name:RDD的名称。name由@transient修饰,所以此属性不会被序列化
- dependencies:与deps相同,但是可以被序列化
- partitions_:存储当前RDD的所有分区的数组。partitions_由@transient修饰,所以此属性不会被序列化
- creationSite:创建当前RDD的用户代码。由@transient修饰,此属性不会被序列化
- scope:当前RDD的操作作用域。由@transient修饰
- checkpointData:当前RDD的检查点数据
- checkpointAllMarkedAncestors:是否对所有标记了需要保存检查点的祖先保存检查点。
- doCheckpointCalled:是否已经调用了doCheckpoint方法设置检查点。此属性可以阻止对RDD多次设置检查点。
RDD采用了模板方法的模式设计,抽象类RDD中定义了模板方法及一些未实现的接口,这些接口将需要RDD的各个子类分别实现。
- comput:对RDD的分区进行计算。此方法的定义如下:
@DeveloperApi
def compute(split: Partition, context: TaskContext): Iterator[T]
- getPartitions:获取当前RDD的所有依赖
- getDependencies:获取当前RDD的所有依赖。
- getPreferredLocations:获取某一分区的偏好位置
RDD中除定义了以上接口外,还实现了一些模板方法
2.1 partitions
用于获取RDD的分区数组
//org.apache.spark.rdd.RDD
final def partitions: Array[Partition] = {
checkpointRDD.map(_.partitions).getOrElse {
if (partitions_ == null) {
partitions_ = getPartitions
partitions_.zipWithIndex.foreach { case (partition, index) =>
require(partition.index == index,
s"partitions($index).partition == ${partition.index}, but it should equal $index")
}
}
partitions_
}
}
根据代码,partitions方法查找分区数组的优先级为:从CheckPoint查找 -> 读取partitions_属性 -> 调用getPartitions方法获取。
2.2 preferredLocations
preferredLocations方法优先调用CheckPoint中保存的RDD的getPreferredLocations方法获取指定分区的偏好位置,当没有保存CheckPoint时,调用自身的getPreferredLocations方法获取指定分区的偏好位置。
final def preferredLocations(split: Partition): Seq[String] = {
checkpointRDD.map(_.getPreferredLocations(split)).getOrElse {
getPreferredLocations(split)
}
}
2.3 dependencies
用于获取当前RDD的所有依赖的序列
final def dependencies: Seq[Dependency[_]] = {
checkpointRDD.map(r => List(new OneToOneDependency(r))).getOrElse {
if (dependencies_ == null) {
dependencies_ = getDependencies
}
dependencies_
}
}
代码执行步骤如下:
- 1)从CheckPoint中获取RDD,并将这些RDD封装为OneToOneDependency列表。如果从CheckPoint中获取到RDD的依赖,则返回RDD的依赖,否则进入下一步
- 2)如果dependencies_等于null,那么调用子类实现的getDependencies方法获取当前RDD的依赖后赋予dependencies,最后返回dependencies_。
3 RDD依赖
DAG中的各个RDD之间存在着依赖关系。换言之,正是RDD之间的依赖关系构建了由RDD所组成的DAG。Spark使用Dependency来表示RDD之间的依赖关系,Dependency的定义如下:
//org.apache.spark.Dependency
@DeveloperApi
abstract class Dependency[T] extends Serializable {
def rdd: RDD[T]
}
抽象类Dependency只定义了一个名为rdd的方法,此方法返回当前依赖的RDD。
3.1 窄依赖
如果RDD与上游RDD的分区是一对一的关系,那么RDD和其上游RDD之间的依赖关系属于窄依赖(NarrowDependency)。NarrowDependency继承了Dependency,以表示窄依赖。
//org.apache.spark.Dependency
@DeveloperApi
abstract class NarrowDependency[T](_rdd: RDD[T]) extends Dependency[T] {
def getParents(partitionId: Int): Seq[Int]
override def rdd: RDD[T] = _rdd
}
NarrowDependency定义了一个类型为RDD的构造器参数_rdd,NarrowDependency重写了Dependency的rdd方法,让其返回_rdd。NarrowDependency还定义了一个获取某一分区的所有低级别分区序列的getParents方法。NarrowDependency一共有两个子类,它们的实现如下:
@DeveloperApi
class OneToOneDependency[T](rdd: RDD[T]) extends NarrowDependency[T](rdd) {
override def getParents(partitionId: Int): List[Int] = List(partitionId)
}
@DeveloperApi
class RangeDependency[T](rdd: RDD[T], inStart: Int, outStart: Int, length: Int)
extends NarrowDependency[T](rdd) {
override def getParents(partitionId: Int): List[Int] = {
if (partitionId >= outStart && partitionId < outStart + length) {
List(partitionId - outStart + inStart)
} else {
Nil
}
}
}
根据OneToOneDependency重写的getParents方法,子RDD的分区与依赖的父RDD的分区相同。如下图:
RangeDependency重写了Dependency的getParents方法,RangeDependency的分区是一对一的,且索引为partitionId的子RDD分区与索引为partitionId - outStart + inStart 的父RDD分区相对应(outStart代表子RDD 的分区范围起始值,inStart代表父RDD的分区范围起始值)
3.2 Shuffle依赖
RDD与上游RDD的分区如果不是一对一的关系,或者RDD的分区依赖于上游RDD的多个分区,那么这种依赖关系就叫做Shuffle依赖(ShuffleDependency)
//org.apache.spark.Dependency
@DeveloperApi
class ShuffleDependency[K: ClassTag, V: ClassTag, C: ClassTag](
@transient private val _rdd: RDD[_ <: Product2[K, V]],
val partitioner: Partitioner,
val serializer: Serializer = SparkEnv.get.serializer,
val keyOrdering: Option[Ordering[K]] = None,
val aggregator: Option[Aggregator[K, V, C]] = None,
val mapSideCombine: Boolean = false)
extends Dependency[Product2[K, V]] {
override def rdd: RDD[Product2[K, V]] = _rdd.asInstanceOf[RDD[Product2[K, V]]]
private[spark] val keyClassName: String = reflect.classTag[K].runtimeClass.getName
private[spark] val valueClassName: String = reflect.classTag[V].runtimeClass.getName
private[spark] val combinerClassName: Option[String] =
Option(reflect.classTag[C]).map(_.runtimeClass.getName)
val shuffleId: Int = _rdd.context.newShuffleId()
val shuffleHandle: ShuffleHandle = _rdd.context.env.shuffleManager.registerShuffle(
shuffleId, _rdd.partitions.length, this)
_rdd.sparkContext.cleaner.foreach(_.registerShuffleForCleanup(this))
}
ShuffleDependency还重写了父类Dependency的rdd方法,其实现将_rdd转换为RDD[Product2[K,V]]后返回。ShuffleDependency在构造的过程还将自己注册到SparkContext的ContextCleaner中。
4 分区计算器Partitioner
RDD之间的依赖关系如果是Shuffle依赖,那么上游RDD该如何确定每个分区的输出将交由下游RDD的哪些分区呢?或者下游RDD的各个分区将具体依赖于上游RDD的哪些分区呢?Spark提供了分区计算器来解决这个问题。ShuffleDependency的partitioner属性的类型是Partitioner,抽象类Partitioner定义了分区计算器的接口规范,ShuffleDependency的分区取决于Partitioner的具体实现。Partitioner的定义如下:
//org.apache.spark.Partitioner
abstract class Partitioner extends Serializable {
def numPartitions: Int
def getPartition(key: Any): Int
}
Partitoner的numPartitions方法用于获取分区数据。Partitioner的getPartition方法用于将输入的key映射到下游RDD的从0到numPartitions-1这一范围内的某一个分区。
Spark除上图列出的Partitioner子类,还有很多Partitioner匿名实现类,下面以HashPartitioner的实现进行分析
//org.apache.spark.Partitioner
class HashPartitioner(partitions: Int) extends Partitioner {
require(partitions >= 0, s"Number of partitions ($partitions) cannot be negative.")
def numPartitions: Int = partitions
def getPartition(key: Any): Int = key match {
case null => 0
case _ => Utils.nonNegativeMod(key.hashCode, numPartitions)
}
override def equals(other: Any): Boolean = other match {
case h: HashPartitioner =>
h.numPartitions == numPartitions
case _ =>
false
}
override def hashCode: Int = numPartitions
}
根据代码,HashPartitioner增加了一个名为partitions的构造器参数作为分区数,重写的numPartitions方法只是返回了partitions。重写的getPartition方法实际以key的hadCode和numPartitions作为参数调用了Utils工具类的nomNegativeMod方法。nonNegativeMod方法将对key的hashCode和numPartitions进行取模运算,得到key对应的分区索引。使用哈希和取模的方式,可以方便地计算下游RDD的各个分区将具体处理哪些key。由于上游RDD所处理的key的哈希值在取模后很可能产生数据倾斜,所以HashPartitioner并不是一个均衡的分区计算器。
根据HashPartitioner的实现,ShuffleDependency中的分区依赖关系不再是一对一的,而是取决于key,并且当前RDD的某个分区将可能依赖于ShuffleDependency的RDD的任何一个分区。
5 RDDInfo
RDDInfo用于描述RDD的信息
//org.apache.spark.storage.RDDInfo
@DeveloperApi
class RDDInfo(
val id: Int,
var name: String,
val numPartitions: Int,
var storageLevel: StorageLevel,
val parentIds: Seq[Int],
val callSite: String = "",
val scope: Option[RDDOperationScope] = None)
extends Ordered[RDDInfo] {
var numCachedPartitions = 0
var memSize = 0L
var diskSize = 0L
var externalBlockStoreSize = 0L
def isCached: Boolean = (memSize + diskSize > 0) && numCachedPartitions > 0
override def toString: String = {
import Utils.bytesToString
("RDD \"%s\" (%d) StorageLevel: %s; CachedPartitions: %d; TotalPartitions: %d; " +
"MemorySize: %s; DiskSize: %s").format(
name, id, storageLevel.toString, numCachedPartitions, numPartitions,
bytesToString(memSize), bytesToString(diskSize))
}
override def compare(that: RDDInfo): Int = {
this.id - that.id
}
}
- id:RDD的id
- name:RDD的名称
- numPartitions:RDD的分区数据
- storageLevel:RDD的存储级别
- parentIds:RDD的父RDD的id序列。说明一个RDD会有零到多个父RDD
- callSite:RDD的用户调用栈信息
- scope:RDD的操作范围
- numCachedPartitions:缓存的分区数据
- memSize:使用的内存大小
- diskSize:使用的磁盘大小
- externalBlockStoreSize:Block存储在外部的大小
RDDInfo的伴生对象中定义了fromRdd方法,用于从RDD构建出对应的RDDInfo
//org.apache.spark.storage.RDDInfo
private[spark] object RDDInfo {
def fromRdd(rdd: RDD[_]): RDDInfo = {
val rddName = Option(rdd.name).getOrElse(Utils.getFormattedClassName(rdd))
val parentIds = rdd.dependencies.map(_.rdd.id)
new RDDInfo(rdd.id, rddName, rdd.partitions.length,
rdd.getStorageLevel, parentIds, rdd.creationSite.shortForm, rdd.scope)
}
}
其执行步骤如下
- 1)获取当前RDD的名称(即name属性)作为RDDInfo的name属性,如果RDD还没有名称,那么调用Utils工具类的getFormattedClassName方法生成RDDInfo的name属性
- 2)获取当前RDD依赖的所有父RDD的身份标识作为RDDInfo的parentIds属性
- 3)创建RDDInfo对象