Spark源码分析: RDD

RDD

1.注释

org.apache.spark.rdd.RDD 类源代码中有详细的注释:

  • A Resilient Distributed Dataset (RDD), the basic abstraction in Spark.
    翻译:弹性的 分布式 数据集是 Spark 基础的抽象。
    解释:弹性的(可复原的),说明数据集具有容错性、可修复性。

          分布式,说明数据集可以分布在不同的机器上
    
  • Represents an immutable, partitioned collection of elements that can be operated on in parallel.
    翻译:RDD 是不可变的 分区的 可并行处理的 元素集合
    解释:不可变的,这和 Scala 的设计理念相同,数据集一旦构建完成,就不能再修改,这样能轻松解决多个线程读数据的一致性问题。

          分区的=可并行处理的=分布式
    
  • This class contains the basic operations available on all RDDs, such as map, filter, and persist.
    翻译:这个抽象类包含了所有 RDD 都应该有的基本操作,比如 mapfilterpersist
    解释:这三个操作分别是:批量转换、筛选、持久化

  • In addition, [[org.apache.spark.rdd.PairRDDFunctions]] contains operations available only on RDDs of key-value
    pairs, such as groupByKey and join;
    翻译:另外 PairRDDFunctions 对象中包含了 键值对型(KV型) RDD 的操作,例如 groupByKeyjoin
    解释:KV 型可以支持按 Key 分组、关联等操作

  • [[org.apache.spark.rdd.DoubleRDDFunctions]] contains operations available only on RDDs of
    Doubles;
    翻译:DoubleRDDFunctions提供可 double 数据集的操作;
    解释:数值型数据集有求和、平均、分布图等统计性操作

  • and [[org.apache.spark.rdd.SequenceFileRDDFunctions]] contains operations available on RDDs that
    can be saved as SequenceFiles.
    翻译:SequenceFileRDDFunctions 提供了顺序存储操作

  • All operations are automatically available on any RDD of the right type (e.g. RDD[(Int, Int)]) through implicit.
    翻译:所有的的类通过隐式转换自动地用于RDD实例中
    解释:RDD 伴生对象里包含了隐式转换函数,用implicit 修饰。隐式转换是 Scala 的语法特性。

  • Internally, each RDD is characterized by five main properties:

    • A list of partitions

    • A function for computing each split

    • A list of dependencies on other RDDs

    • Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned)

    • Optionally, a list of preferred locations to compute each split on (e.g. block locations for an HDFS file)

      翻译:在 RDD 中,包含这样的5个属性(也就说要实现抽象方法或给空对象赋值):

      • 一个分区的列表(getPartitions)
      • 一个用于计算分区中数据的函数(compute)
      • 一个对其他 RDD 的依赖列表(getDependencies)
      • 可选:KV 型 RDD 应该有一个分区器,例如 hash-分区器(partitioner)
      • 可选:分区数据计算完后优先存储的位置,例如 HDFS 的某个块(getPreferredLocations)

初始参数:上下文和一组依赖

abstract class RDD[T: ClassTag](
    @transient private var _sc: SparkContext,
    @transient private var deps: Seq[Dependency[_]]
  ) extends Serializable with Logging 

2.Dependency

Dependency代表了RDD之间的依赖关系,即血缘

继承关系如下:

RDD x 中每个 partition 可以依赖于 parent RDD 中一个或者多个 partition。而且这个依赖可以是完全依赖或者部分依赖。部分依赖指的是 parent RDD 中某 partition 中一部分数据与 RDD x 中的一个 partition 相关,另一部分数据与 RDD x 中的另一个 partition 相关。下图展示了完全依赖和部分依赖。

前三个是完全依赖,RDD x 中的 partition 与 parent RDD 中的 partition/partitions 完全相关。最后一个是部分依赖,RDD x 中的 partition 只与 parent RDD 中的 partition 一部分数据相关,另一部分数据与 RDD x 中的其他 partition 相关。

在 Spark 中,完全依赖被称为 NarrowDependency,部分依赖被称为 ShuffleDependency。其实 ShuffleDependency 跟 MapReduce 中 shuffle 的数据依赖相同(mapper 将其 output 进行 partition,然后每个 reducer 会将所有 mapper 输出中属于自己的 partition 通过 HTTP fetch 得到)。

  • 第一种 1:1 的情况被称为 OneToOneDependency。
  • 第二种 N:1 的情况被称为 N:1 NarrowDependency。
  • 第三种 N:N 的情况被称为 N:N NarrowDependency。不属于前两种情况的完全依赖都属于这个类别。
  • 第四种被称为 ShuffleDependency。

对于 NarrowDependency,具体 RDD x 中的 partitoin i 依赖 parrent RDD 中一个 partition 还是多个 partitions,是由 RDD x 中的 getParents(partition i) 决定(下图中某些例子会详细介绍)。还有一种 RangeDependency 的完全依赖,不过该依赖目前只在 UnionRDD 中使用,下面会介绍。

所以,总结下来 partition 之间的依赖关系如下:

  • NarrowDependency (使用黑色实线或黑色虚线箭头表示)OneToOneDependency (1:1)NarrowDependency (N:1)NarrowDependency (N:N)RangeDependency (只在 UnionRDD 中使用)
  • ShuffleDependency (使用红色箭头表示)

代码实现如下:

Dependency有两个子类,一个子类为窄依赖:NarrowDependency;一个为宽依赖ShuffleDependency

NarrowDependency也是一个抽象类,它实现了getParents 重写了 rdd 函数,它有两个子类,一个是 OneToOneDependency,一个是 RangeDependency

abstract class NarrowDependency[T](_rdd: RDD[T]) extends Dependency[T] {
  /**
   * Get the parent partitions for a child partition.
   * @param partitionId a partition of the child RDD
   * @return the partitions of the parent RDD that the child partition depends upon
   */
  def getParents(partitionId: Int): Seq[Int]

  override def rdd: RDD[T] = _rdd
}1234567891012345678910

OneToOneDependency,可以看到getParents实现很简单,就是传进一个partitionId: Int,再把partitionId放在List里面传出去,即去parent RDD 中取与该RDD 相同 partitionID的数据

class OneToOneDependency[T](rdd: RDD[T]) extends NarrowDependency[T](rdd) {
  override def getParents(partitionId: Int): List[Int] = List(partitionId)
}

RangeDependency,用于union。与上面不同的是,这里我们要算出该位置,设某个parent RDD 从 inStart 开始的partition,逐个生成了 child RDD 从outStart 开始的partition,则计算方式为: partitionId - outStart + inStart

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
    }
  }
}12345678910111234567891011

ShuffleDependency,需要进行shuffle

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
  // Note: It's possible that the combiner class tag is null, if the combineByKey
  // methods in PairRDDFunctions are used instead of combineByKeyWithClassTag.
  private[spark] val combinerClassName: Option[String] =
    Option(reflect.classTag[C]).map(_.runtimeClass.getName)
//获取shuffleID
  val shuffleId: Int = _rdd.context.newShuffleId()
//向注册shuffleManager注册Shuffle信息
  val shuffleHandle: ShuffleHandle = _rdd.context.env.shuffleManager.registerShuffle(
    shuffleId, _rdd.partitions.length, this)

  _rdd.sparkContext.cleaner.foreach(_.registerShuffleForCleanup(this))
}
) 

分析如下:

  1. 这个类有三个泛型类型,K=key,V=value,C=combiner;
  2. 洗牌依赖只能用于Product2[K, V]及其父类,即 KV 数据;
  3. 成员有 分区器(partitioner) 、序列器(serializer)、排序器(keyOrdering)、聚合器(aggregator)、map 端聚合开关(mapSideCombine);
  4. _rdd.context.newShuffleId() 获得一个自增的 ID;
  5. _rdd.context.env.shuffleManager.registerShuffle 获得几个洗牌的句柄。通过core/shuffle/sort/SortShuffleManager代码可以知道,一共有三种句柄:
    1. 分区数很少(小于变量spark.shuffle.sort.bypassMergeThreshold,默认200)时,用BypassMergeSortShuffleHandle,直接发送数据合并,不用耗时的序列化和反序列化;
    2. 否则,如果能序列化,则用SerializedShuffleHandle,用序列化和反序列化,降低网络 IO;
    3. 否则,使用基础的BaseShuffleHandle。

3.Partion

/**
 * An identifier for a partition in an RDD.
 */
trait Partition extends Serializable {
  /**
   * Get the partition's index within its parent RDD
   */
  def index: Int

  // A better default implementation of HashCode
  override def hashCode(): Int = index

  override def equals(other: Any): Boolean = super.equals(other)
}

Partition具体表示RDD每个数据分区。

Partition提供trait类,内含一个index和hashCode()方法,具体子类实现与RDD子类有关

4.Partitioner

Partitioner决定KV形式的RDD如何根据key进行partition

abstract class Partitioner extends Serializable {
  def numPartitions: Int
  def getPartition(key: Any): Int
}

在ShuffleDependency里对应一个Partitioner,来完成宽依赖下,子RDD如何获取父RDD。

默认Partitioner

Partitioner的伴生对象提供defaultPartitioner方法,逻辑为:

传入的RDD(至少两个)中,遍历(顺序是partition数目从大到小)RDD,如果已经有Partitioner了,就使用。如果RDD们都没有Partitioner,则使用默认的HashPartitioner。而HashPartitioner的初始化partition数目,取决于是否设置了Spark.default.parallelism,如果没有的话就取RDD中partition数目最大的值。

如果上面这段文字看起来费解,代码如下:

def defaultPartitioner(rdd: RDD[_], others: RDD[_]*): Partitioner = {
  val rdds = (Seq(rdd) ++ others)
  val hasPartitioner = rdds.filter(_.partitioner.exists(_.numPartitions > 0))
  if (hasPartitioner.nonEmpty) {
    hasPartitioner.maxBy(_.partitions.length).partitioner.get
  } else {
    if (rdd.context.conf.contains("spark.default.parallelism")) {
      new HashPartitioner(rdd.context.defaultParallelism)
    } else {
      new HashPartitioner(rdds.map(_.partitions.length).max)
    }
  }
}

5.Persist

默认cache()过程是将RDD persist在内存里,persist()操作可以为RDD重新指定StorageLevel

class StorageLevel private(  
    private var useDisk_ : Boolean,  
    private var useMemory_ : Boolean,  
    private var useOffHeap_ : Boolean,  
    private var deserialized_ : Boolean,  
    private var replication_ : Int = 1) 

你可能感兴趣的:(Spark源码分析: RDD)