Spark调度系统——RDD详解

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对象
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值