Spark的数据结构——RDD

RDD 的 5 个特征

下面来说一下 RDD 这东西,它是 Resilient Distributed Datasets 的简写。

咱们来看看 RDD 在源码的解释。

  • A list of partitions: 在大数据领域,大数据都是分割成若干个部分,放到多个服务器上,这样就能做到多线程的处理数据,这对处理大数据量是非常重要的。分区意味着,可以使用多个线程了处理。
  • A function for computing each split:作用在每个分区里面的函数,当我们读取数据之后,当然是要对其加工的,加工的定义就是我们编写的函数,这些函数主要包含转化算子、控制算子、行动算子。
  • A list of dependencies on other RDDs。一个 Spark Application 下面可以有多个 Job ,一个 action 算子就可以分出一个 job ,一个 job 里面又可以分出若干个 stage , 一个 stage 中又有多个 RDD ,RDD 之间是用上下游关系的,就像流水线的工序,公休之间也会有先后之分的,例如,手机装壳之后才能上螺丝,这种上下游关系,使用依赖描述的,依赖又分为窄依赖和宽依赖。 那两个 RDD 为例,rdd2 依赖于 rdd1 ,
  • 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 源代码

RDD 的代码是非常多的,一个 RDD.scala 类就有 2000 多行。我们只捡能说明问题的就行了。

abstract class RDD[T: ClassTag](
    // SparkContext 是代码的运行环境,SparkContext 中有一个 TaskSchedule 和 DAGSchedule ,前者是申请资源,后者是将 job 分割为多个 Stage ,然后提交给相应的 Executor
    @transient private var _sc: SparkContext,
    // deps 代表了上游算子依赖,上游可能有多个依赖,所以这里是一个 Seq .
    // 这个 Seq 就是 RDD 中依赖的具体体现
    @transient private var deps: Seq[Dependency[_]]
  ) extends Serializable with Logging {
  // compute 函数代表了 RDD 第二个特征,作用在 partition 上面的函数。
  @DeveloperApi
  def compute(split: Partition, context: TaskContext): Iterator[T]
  // 此函数是 RDD 第一个特征的具体表现,各个 RDD 的具体实现,可以根据它获得 RDD 中的分区
  protected def getPartitions: Array[Partition]
  // 还是依赖相关的函数
  protected def getDependencies: Seq[Dependency[_]] = deps
  // 此函数对应了 RDD 的第 5 个特征。各个 RDD 的实现类,在此函数中,实现就近数据的查找。
  protected def getPreferredLocations(split: Partition): Seq[String] = Nil
  // 此函数对应了 RDD 的第四个特征,针对 PairRDDFunction 的分区器。
  @transient val partitioner: Option[Partitioner] = None

  def sparkContext: SparkContext = sc

  val id: Int = sc.newRddId()

  final def dependencies: Seq[Dependency[_]] = {
    ... 
  }

  final private def internalDependencies: Option[Seq[Dependency[_]]] = {
    ... 
  }

  final def partitions: Array[Partition] = {
     ...
  }

  final def preferredLocations(split: Partition): Seq[String] = {
    checkpointRDD.map(_.getPreferredLocations(split)).getOrElse {
      getPreferredLocations(split)
    }
  }

  final def iterator(split: Partition, context: TaskContext): Iterator[T] = {
  }
}

object RDD {

  private[spark] val CHECKPOINT_ALL_MARKED_ANCESTORS =
    "spark.checkpoint.checkpointAllMarkedAncestors"
  implicit def rddToPairRDDFunctions[K, V](rdd: RDD[(K, V)])
    (implicit kt: ClassTag[K], vt: ClassTag[V], ord: Ordering[K] = null): PairRDDFunctions[K, V] = {
    new PairRDDFunctions(rdd)
  }
  // 此方法对应了 RDD 的第四个特征,有了它,只要将 RDD 中的数据转化为 tuple2 的数据格式,就能自动调用 PairRDDFunction 中的函数。
  implicit def rddToAsyncRDDActions[T: ClassTag](rdd: RDD[T]): AsyncRDDActions[T] = {
    new AsyncRDDActions(rdd)
  }
}

还有更重要的一点,就是第二个特征,作用在分区上的函数,RDD 加上 PairRDDFunction 上的函数有很多,可以在上一篇 Spark 核心API 中找到。

下面以 Workd Count 为例子,画图来说明 RDD 的特性。

val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName(this.getClass.getCanonicalName.init)
val sc: SparkContext = new SparkContext(conf)
sc.textfile("hdfs://nameservice/user/test_data/file.txt")
.flapMap(_.split(","))
.map((_,1))
.reduceByKey(_+_)
.foreach(println)

先来看看 textFile 底层是什么?

// SparkContext 
def textFile(
    path: String,
    minPartitions: Int = defaultMinPartitions): RDD[String] = withScope {
  assertNotStopped()
  hadoopFile(path, classOf[TextInputFormat], classOf[LongWritable], classOf[Text],
    minPartitions).map(pair => pair._2.toString).setName(path)
}
def hadoopFile[K, V](
    path: String,
    inputFormatClass: Class[_ <: InputFormat[K, V]],
    keyClass: Class[K],
    valueClass: Class[V],
    minPartitions: Int = defaultMinPartitions): RDD[(K, V)] = withScope {
  assertNotStopped()
  FileSystem.getLocal(hadoopConfiguration)
  val confBroadcast = broadcast(new SerializableConfiguration(hadoopConfiguration))
  val setInputPathsFunc = (jobConf: JobConf) => FileInputFormat.setInputPaths(jobConf, path)
  // 最后返回的是 HadoopRDD ,这是我们认识的第一个 RDD 
  new HadoopRDD(
    this,
    confBroadcast,
    Some(setInputPathsFunc),
    inputFormatClass,
    keyClass,
    valueClass,
    minPartitions).setName(path)

以 HadoopRDD 为例子,我们来看看 RDD 的五个特性。
第一个特征是分区,来看一下 HadoopRDD 的 getPartitions 方法。

override def getPartitions: Array[Partition] = {
  val jobConf = getJobConf()
  try {
    // 获取 splite , 这其实就是将一个 HDFS 文件切分成若干个分区。 
    val allInputSplits = getInputFormat(jobConf).getSplits(jobConf, minPartitions)
    val inputSplits = if (ignoreEmptySplits) {
      allInputSplits.filter(_.getLength > 0)
    } else {
      allInputSplits
    }
    // 根据分区大小来提示优化策略
    if (inputSplits.length == 1 && inputSplits(0).isInstanceOf[FileSplit]) {
      ...
    }
    // 将 FileSplite 组成 hadoopPartition
    val array = new Array[Partition](inputSplits.size)
    for (i <- 0 until inputSplits.size) {
      array(i) = new HadoopPartition(id, i, inputSplits(i))
    }
    array
  } catch {
    ... 
  }
}

从 getPatitions 方法,可以看到使用 hadoop-client 的接口,将 HDFS 的文件切成若干 HadoopPartition ,然后返回一个数组 Array[Partition]。

第二个特征是作用在分区上的函数,那就来到来 compute 函数。

override def compute(theSplit: Partition, context: TaskContext): InterruptibleIterator[(K, V)] = {
  // 构造 NextIterator 迭代器
  val iter = new NextIterator[(K, V)] {
    ...
    private var reader: RecordReader[K, V] = null
    ...
    reader =
      try {
        inputFormat.getRecordReader(split.inputSplit.value, jobConf, Reporter.NULL)
      } catch {
         ... 
      }

    private val key: K = if (reader == null) null.asInstanceOf[K] else reader.createKey()
    private val value: V = if (reader == null) null.asInstanceOf[V] else reader.createValue()
    // 重新 getNext 方法,此方法其实就是从 HDFS 的文件中哪里一行数据,
    // K 为对应此行在文件中的位置,
    // V 为此行的数据
    override def getNext(): (K, V) = {
      try {
        finished = !reader.next(key, value)
      } catch {
         ... 
      }
      (key, value)
    }
    // 关闭 HDFS 客户端和服务器端的连接
    override def close(): Unit = {
    }
  }
  new InterruptibleIterator[(K, V)](context, iter)
}

上面的代码中 inputSplit 其实是 FileInputSplit ,reader 是 LineRecordReader 。HadoopRDD 的功能就是从 HDFS 中取数据,向后发送,所以没有数据处理的逻辑。

第三个特征是描述 RDD 中的依赖。HadoopRDD 是第一个 RDD 所以它前面已经没有了 RDD 。从下面 HadoopRDD 的定义就能看出来。Dependency 为 Nil

class HadoopRDD[K, V](
   sc: SparkContext,
   broadcastedConf: Broadcast[SerializableConfiguration],
   initLocalJobConfFuncOpt: Option[JobConf => Unit],
   inputFormatClass: Class[_ <: InputFormat[K, V]],
   keyClass: Class[K],
   valueClass: Class[V],
   minPartitions: Int)
 // RDD构造函数是 RDD(SparkContext , Dependency)
 // 从下面的代码中,可以看到 Dependency 为 Nil 。
 extends RDD[(K, V)](sc, Nil){
 ...
}

preferedLocation 和 key-value RDDS 的特征在 HadoopRDD 没有体现出来。

下面再看 flatMap ,

  def flatMap[U: ClassTag](f: T => TraversableOnce[U]): RDD[U] = withScope {
    val cleanF = sc.clean(f)
    new MapPartitionsRDD[U, T](this, (_, _, iter) => iter.flatMap(cleanF))
  }

从上面代码中可以看到,MapPartitionsRDD 是 flatMap 的 RDD。还是这五个特征来看 MapPartitionsRDD ,

// MapPartitionsRDD#getPartitions
override def getPartitions: Array[Partition] = firstParent[T].partitions
// RDD#firstParent
protected[spark] def firstParent[U: ClassTag]: RDD[U] = {
  dependencies.head.rdd.asInstanceOf[RDD[U]]
}

从上面的代码,可以看出,getPartitions 方法取出是第一个父 RDD 的分区,这是第一个特征。

第二个特征是作用在分区上面的计算,flatMap 是将 line 分裂成单个的单词,所以这里需要函数,就是 f ,
f 其实是在 flatMap 函数中定义的 (_, _, iter) => iter.flatMap(cleanF) , 而 cleanF 就是
我们自定义的 _.split(“\s”) 的,而接收它的是一个 iterator 的 flatMap ,这个 flatMap 是 scala
原生的,并不是 RDD#flatMap。

override def compute(split: Partition, context: TaskContext): Iterator[U] =
  f(context, split.index, firstParent[T].iterator(split, context))

从代码中,看到 f 的第三个入参是第一个父 RDD 的迭代器。

第三个特征是依赖关系,可以从 MapPartitionsRDD 的定义看出。

private[spark] class MapPartitionsRDD[U: ClassTag, T: ClassTag](
    var prev: RDD[T],
    f: (TaskContext, Int, Iterator[T]) => Iterator[U],  // (TaskContext, partition index, iterator)
    preservesPartitioning: Boolean = false,
    isFromBarrier: Boolean = false,
    isOrderSensitive: Boolean = false)
  extends RDD[U](prev) {...}
  
 def this(@transient oneParent: RDD[_]) =
    this(oneParent.context, List(new OneToOneDependency(oneParent)))

现在只要弄清楚 pre 是那个 RDD 就可以了,当我们调用 sc.textFile(path).flatMap(_.split(“\s”)),其实 textFile 返回的是 HadoopRDD,所以是 HadoopRDD 调用的 flatMap ,所以 prev 就是 HadoopRDD 的引用。到这里,问题应该就清晰了,OneToOneDependency保存的父 RDD ,再有明显可以看出是窄依赖,一对一嘛。

preferedLocation 和 key-value RDD 同样都没体现出来。

下面来看看 map((_,1)) 使用了什么 RDD。

 def map[U: ClassTag](f: T => U): RDD[U] = withScope {
   val cleanF = sc.clean(f)
   new MapPartitionsRDD[U, T](this, (_, _, iter) => iter.map(cleanF))
 }

从代码来看,f 函数被 map 调用了,对应的 RDD 也是 MapPartitionRDD ,就是迭代器调用的方法发生了改变。还是按照老办法,把五个特征找出来。

第一个是 getPartitions 返回还是第一个 parent RDD 的分区。
第二个是 compute 中调用的是第一个 parentRDD 分区的迭代器。
第三个是 dependency 是 flatMap 对应的 MapPartiionRDD
preferedLocation 和 key-value RDD 同样都没体现出来。

最后是 reduceByKey(+) , reduceByKey 是 PairRDDFunction 的函数,这是咋回事,map((_,1)) 返回的不是 MapPartitionRDD 吗?怎么又变成 PairRDDFunction 了,这就要讲到 Scala 的隐式转化, 请看下面的代码:

object RDD {
...
  implicit def rddToPairRDDFunctions[K, V](rdd: RDD[(K, V)])
    (implicit kt: ClassTag[K], vt: ClassTag[V], ord: Ordering[K] = null): PairRDDFunctions[K, V] = {
    new PairRDDFunctions(rdd)
  }
...
}  

当调用某个类的方法,发现此类没有这个方法,则就取找隐式方法,这里隐式方法是 rddToPairRDDFunctions,它最终将 MapPartitionRDD 转化为了 PairRDDFunction ,这样就实现了自动化的转化,所以这里能够调用 reduceByKey 方法,这也对应了 RDD 的第四个特性,key-value RDD 。看到这里,它的意思就是将那些数据类型为 (key , value) 的 RDD 自动转化为 PairRDDFunctiono , 并且调用上面的方法。

接着看 reduceByKey 的源码,

def reduceByKey(func: (V, V) => V): RDD[(K, V)] = self.withScope {
  reduceByKey(defaultPartitioner(self), func)
}
def reduceByKey(partitioner: Partitioner, func: (V, V) => V): RDD[(K, V)] = self.withScope {
  combineByKeyWithClassTag[V]((v: V) => v, func, func, partitioner)
}

从上面的源码看出,reduceByKey 底层使用的是 combinerByKey() , combinerByKey 在之前的文章已经讲过了,

  def combineByKeyWithClassTag[C](
      createCombiner: V => C,
      mergeValue: (C, V) => C,
      mergeCombiners: (C, C) => C,
      partitioner: Partitioner,
      mapSideCombine: Boolean = true,
      serializer: Serializer = null)(implicit ct: ClassTag[C]): RDD[(K, C)] = self.withScope {
....
    val aggregator = new Aggregator[K, V, C](
      self.context.clean(createCombiner),
      self.context.clean(mergeValue),
      self.context.clean(mergeCombiners))
    if (self.partitioner == Some(partitioner)) {
      ... 
    } else {
      new ShuffledRDD[K, V, C](self, partitioner)
        .setSerializer(serializer)
        .setAggregator(aggregator)
        .setMapSideCombine(mapSideCombine)
    }
  }

从代码得,combineByKey 底层使用的是 ShuffleRDD 。

override def getPartitions: Array[Partition] = {
  Array.tabulate[Partition](part.numPartitions)(i => new ShuffledRDDPartition(i))
}
override def compute(split: Partition, context: TaskContext): Iterator[(K, C)] = {
  val dep = dependencies.head.asInstanceOf[ShuffleDependency[K, V, C]]
  val metrics = context.taskMetrics().createTempShuffleReadMetrics()
  SparkEnv.get.shuffleManager.getReader(
    dep.shuffleHandle, split.index, split.index + 1, context, metrics)
    .read()
    .asInstanceOf[Iterator[(K, C)]]
}

在 compute 中并没有从依赖中取出迭代器,而是调用了 ShuffleManager#getReader 方法,这是因为 combineByKey 是做分区操作的,所以要将相同 key 的数据通过网络发送到不同的机器上,其实就是 Map-Reduce 计算引擎的 shuffle 过程,这里也是一样的,这也是 ShuffleRDD 名称的由来。

paritition 的特性:

  1. parttion 特性,在 compute 函数中,使用 ShuffleManager 拿到 shuffle 到本分区的数据。这里是根据 key 进行了重新的分区
  2. compute 的特性,ShuffleRDD 的计算函数是封装在了 aggregator 成员变量了,而 aggregator 又被保存到了ShuffleDependency 中,其实是在 BlockStoreShuffleReader 中调用了 combinerBykey 中的我们自定义的函数。
  3. 依赖的是 ShuffleDependency ,就是宽依赖。
  4. key-value RDD 的特性 ,其实就是 PairRDDFunction 的隐式转化,在 reduceByKey 中体现的比较明显。
  5. preferedLocation 还是没有找对应的逻辑。

下面以图的方式来总结一下,HadoopRDD、MapPartitionsRDD、ShuffleRDD 这三个 RDD 在 word count 这个例子中的对应关系。

wordcount的 RDD 结构图

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值