spark RDD join的核心过程

spark RDD join的核心过程

spark join的过程是查询过程中最核心的过程,怎么做到实现两个表的关联查询耗费资源最少。可看源码如下
join的实现在 PairRDDFunctions类当中。

def join[W](other: RDD[(K, W)], partitioner: Partitioner): RDD[(K, (V, W))] = self.withScope {
this.cogroup(other, partitioner).flatMapValues( pair =>
  // _1 是左表,_2 是右表的值,这是一个笛卡尔积的过程,key 一样,左表和右表各一些数据
  for (v <- pair._1.iterator; w <- pair._2.iterator) yield (v, w)
)
}

可以看到上面,自身RDD和其它的RDD进行数据的关联,同时传进去partitioner对象

def cogroup[W](other: RDD[(K, W)], partitioner: Partitioner)
  : RDD[(K, (Iterable[V], Iterable[W]))] = self.withScope {
if (partitioner.isInstanceOf[HashPartitioner] && keyClass.isArray) {
  // hash分区方式不能用于key是数组的对象
  throw new SparkException("Default partitioner cannot partition array keys.")
}
// join操作中很核心的执行类
val cg = new CoGroupedRDD[K](Seq(self, other), partitioner)
cg.mapValues { case Array(vs, w1s) =>
  (vs.asInstanceOf[Iterable[V]], w1s.asInstanceOf[Iterable[W]])
}
}

然后创建 CoGroupedRDD 专门用于RDD的关联操作对象。我们现在完整分析CoGroupedRDD源码

override def getDependencies: Seq[Dependency[_]] = {
rdds.map { rdd: RDD[_] =>
  if (rdd.partitioner == Some(part)) {
    logDebug("Adding one-to-one dependency with " + rdd)
    // 该RDD和 join合并的分区partitioner一样
    new OneToOneDependency(rdd)
  } else {
    logDebug("Adding shuffle dependency with " + rdd)
    // 当partitioner不一样时,要对数据进行重新分区,就是shuff的过程
    new ShuffleDependency[K, Any, CoGroupCombiner](
      rdd.asInstanceOf[RDD[_ <: Product2[K, _]]], part, serializer)
  }
}
}

上面就是这个join的相关RDD依赖,如果part分区一样,就是OneToOneDependency依赖,不用进行hash拆分。否则
要关联的RDD和part的分区不一致时,就要对RDD进行重新hash分区,分到正确的分片上面,所以就要用ShuffleDependency 进行
hash分片数据,然后在正确的split分片处理业务进程中进行处理。

override def getPartitions: Array[Partition] = {
// 这里对数据进行分片,一个分片就在一台work进程中进行处理了
val array = new Array[Partition](part.numPartitions)
for (i <- 0 until array.length) {
  // Each CoGroupPartition will have a dependency per contributing RDD
  array(i) = new CoGroupPartition(i, rdds.zipWithIndex.map { case (rdd, j) =>
    // Assume each RDD contributed a single dependency, and get it
    dependencies(j) match {
      case s: ShuffleDependency[_, _, _] =>
        // 当这个数据要进行shuffler时
        None
      case _ =>
        // 当分区是一样时,就直接进行了
        Some(new NarrowCoGroupSplitDep(rdd, i, rdd.partitions(i)))
    }
  }.toArray)
}
// 这样就可以把关联的RDD拆成了numPartitions分了
array
}

上面就是对各个关联的数据进行hash分片了,就是有几个RDD,然后根据它们的key进行hash分片,分到正确的partition中,如果是 OneToOneDependency 就不用进行数据的再拆分片了,ShuffleDependency 就要通过传进去的part对key进行分片,把所有一样的key
分到同样的split数据分片当中。这样各个RDD一样的key就在一样的,就可以执行关联操作了。

override def compute(s: Partition, context: TaskContext): Iterator[(K, Array[Iterable[_]])] = {
// 在其中一个work进程中执行这一分区数据了
val split = s.asInstanceOf[CoGroupPartition]
// 依赖这么多RDD
val numRdds = dependencies.length

// A list of (rdd iterator, dependency number) pairs
// 拿这个分片的数据进行计算
val rddIterators = new ArrayBuffer[(Iterator[Product2[K, Any]], Int)]
for ((dep, depNum) <- dependencies.zipWithIndex) dep match {
  case oneToOneDependency: OneToOneDependency[Product2[K, Any]] @unchecked =>
    // 依赖于 depNum 那个RDD的分片数据
    val dependencyPartition = split.narrowDeps(depNum).get.split
    // Read them from the parent
    // 在这个work进程中读取这个分片数据
    val it = oneToOneDependency.rdd.iterator(dependencyPartition, context)
    rddIterators += ((it, depNum))

  case shuffleDependency: ShuffleDependency[_, _, _] =>
    // Read map outputs of shuffle
    // 说明之前对这个RDD 的数据进行分片hash过的了
    // 然后这里专门去拉取该分片对应的数据回来
    val it = SparkEnv.get.shuffleManager
      .getReader(shuffleDependency.shuffleHandle, split.index, split.index + 1, context)
      .read()
    rddIterators += ((it, depNum))
}
// 创建一个多个RDD的合并器
val map = createExternalMap(numRdds)
for ((it, depNum) <- rddIterators) {
  map.insertAll(it.map(pair => (pair._1, new CoGroupValue(pair._2, depNum))))
}
context.taskMetrics().incMemoryBytesSpilled(map.memoryBytesSpilled)
context.taskMetrics().incDiskBytesSpilled(map.diskBytesSpilled)
context.internalMetricsToAccumulators(
  InternalAccumulator.PEAK_EXECUTION_MEMORY).add(map.peakMemoryUsedBytes)
// 结果就这样排好序的了
new InterruptibleIterator(context,
  map.iterator.asInstanceOf[Iterator[(K, Array[Iterable[_]])]])
}

再来研究一下compute方法,这个方法就是对当前要计算的split进行处理的,上面已经对多个RDD进行hash分片了,然后把
相同的key都分片到这里来了,如果是oneToOneDependency就直接读取那个分片数据,否则就要启动对RDD的shuffle的过程
把一个RDD通过hash分到多个分片当中,然后该函数拉取自己需求的那一个分片数据。当该split需求的分片数据准备好后,就创建
下面的ExternalAppendOnlyMap 类进行对数据的排序关联功能了。

private def createExternalMap(numRdds: Int)
: ExternalAppendOnlyMap[K, CoGroupValue, CoGroupCombiner] = {
// 创建一个多个RDD的合并器,key value rdd_index
//  value._2 应该是rdd的index value._1 应该是value
// 初始化 rdd_index --> value
val createCombiner: (CoGroupValue => CoGroupCombiner) = value => {
  val newCombiner = Array.fill(numRdds)(new CoGroup)
  newCombiner(value._2) += value._1
  newCombiner
}
// 中间过程数据的合并 rdd_index --> value
val mergeValue: (CoGroupCombiner, CoGroupValue) => CoGroupCombiner =
  (combiner, value) => {
  combiner(value._2) += value._1
  combiner
}
// 最后所有 rdd数据的合并
val mergeCombiners: (CoGroupCombiner, CoGroupCombiner) => CoGroupCombiner =
  (combiner1, combiner2) => {
    var depNum = 0
    while (depNum < numRdds) {
      combiner1(depNum) ++= combiner2(depNum)
      depNum += 1
    }
    combiner1
  }
// key 在这个 对象里面自己管控,各业务方法只要缓存value和rdd_index的关系就好了
new ExternalAppendOnlyMap[K, CoGroupValue, CoGroupCombiner](
  createCombiner, mergeValue, mergeCombiners)
}

可以看到这里有一个公共的多RDD数据联合器,只要把数据往里面插入进去,就自动进行数据的关联操作了。
最后就返回排好序的InterruptibleIterator对象,实现多RDD的联合join。

下面再看下 left out join

def leftOuterJoin[W](
  other: RDD[(K, W)],
  partitioner: Partitioner): RDD[(K, (V, Option[W]))] = self.withScope {
this.cogroup(other, partitioner).flatMapValues { pair =>
  if (pair._2.isEmpty) {
    // 当是右表为空时,左表也要输出
    pair._1.iterator.map(v => (v, None))
  } else {
    for (v <- pair._1.iterator; w <- pair._2.iterator) yield (v, Some(w))
  }
}
}

可以看到,当是 left out join 时,底层也是一样关联的,只是在外面通过判断左表有值时,也进行输出。
同时,right out join也一样。

总结

  1. 传入多个RDD对象
  2. 判断该RDD对象是否和给定的part分区函数一致,如果是就直接拉取对应的分区,否则就shuffle,hash分片数据,然后拉取
  3. 把相同partition分片后的数据发到对应work进程中进行读取
  4. 然后在该work业务进程中,单独对这hash分片一致的数据进行关联操作
  5. 最后返回有序iterator对象

你可能感兴趣的:(spark)