spark源码学习(九):map端计算结果缓存处理(一)


      在前面我们谈到了在map任务结束之后,map任务会对结果进行三种方式的处理,这里来看看具体的代码,就是进入ExternalSorter的insertAll方法去看看。这里的代码主要分为三个部分,三个if代码块儿分别对应着在map端执行局合,直接写入partition的存储块儿,简单的对计算结果进行缓存。代码如下所示:


def insertAll(records: Iterator[_ <: Product2[K, V]]): Unit = {
    val shouldCombine = aggregator.isDefined

    if (shouldCombine) {
      // 使用 AppendOnlyMap在内存中执行聚合
      val mergeValue = aggregator.get.mergeValue
      val createCombiner = aggregator.get.createCombiner
	  //使用到的key-value对
      var kv: Product2[K, V] = null
	  //定义一个偏函数update,最重要的函数
      val update = (hadValue: Boolean, oldValue: C) => {
        if (hadValue) mergeValue(oldValue, kv._2) else createCombiner(kv._2)
      }
      while (records.hasNext) {
	  //这里主要用于从records中读取数据
        addElementsRead()
        kv = records.next()
		//数据格式((partitionIndex,key),update(boolean,oldValue)
        map.changeValue((getPartition(kv._1), kv._1), update)
		//如果内存使用过大,就会把数据写入磁盘,同时新建SizeTrackingAppendMapOnly
        maybeSpillCollection(usingMap = true)
      }
    } 
	else if (bypassMergeSort) {
	//直接把结果写到partition文件中去
      if (records.hasNext) {
        spillToPartitionFiles(
          WritablePartitionedIterator.fromIterator(records.map { kv =>
            ((getPartition(kv._1), kv._1), kv._2.asInstanceOf[C])
          })
        )
      }
    } 
	else {
    //对map端的结果数据进行简单的缓存
      while (records.hasNext) 
	  {
        addElementsRead()
        val kv = records.next()
        buffer.insert(getPartition(kv._1), kv._1, kv._2.asInstanceOf[C])
        maybeSpillCollection(usingMap = false)
      }
    }
  }

       发现在代码的刚刚开始的地方出现;aggregator.get.***,这是什么东西,进入源码看看如下

连接spark 显示乱码 spark appendonlymap_代码块


这里会发现,其实他的三个构造参数都是三个函数,主要是用来对key,value数据块儿进行操作的,具体的不解释,上面的注释写的很清楚。如果进一步去看的话,你会发现这个class会出现在shuffleRDD中,会有一个setAggregator的方法,很明朗,我们这里在定义Aggregator的时候使用的是case class,会自动的提供set,get方法。要是继续的深入去看,我们就应该去看看PairRDDFunctions中的combinByKey函数啦,这个方法会显示出函数式编程的巨大威力。在这里不进行讨论combinebykey,点击这里去看看其他大神的介绍。

     上面的代码中的第一个代码块儿已经介绍过了,就是把数据在map端就直接进行合并排序,这样可以大幅度减少reduce的压力,那么接下来就继续看看第二代码块儿的内容--不适用map端的缓存,也不执行聚合和排序,而是直接把数据spill到各自对应的partition中去,让reduce来处理。如图所示:

连接spark 显示乱码 spark appendonlymap_代码块_02

最核心的代码当然就是SpillToPartitionFiles啦,主要用于溢出分区文件,就是把数据从缓存写到磁盘上。代码如下

连接spark 显示乱码 spark appendonlymap_连接spark 显示乱码_03

 进入上面代码的最后一句,看看这个迭代器是干什么的:

连接spark 显示乱码 spark appendonlymap_连接spark 显示乱码_04

回到最上面的代码块儿,我们在来看看那个fromIterato的用法和具体的参数:

连接spark 显示乱码 spark appendonlymap_连接spark 显示乱码_05


你会发现,这里的recordes也是一个iterator,只不过加上了视图界定而已。

还要注意这句话:writer.write(cur._1._2, cur._2)写如的数据格式,在代码fromiterator中的数据格式是Iterator[((Int, _), _)这样的,但是在写write方法的时候却直接忽略了partitionIndex,直接key,value写入磁盘,没有理会分区索引,所以,最终的结果如下图所示:

连接spark 显示乱码 spark appendonlymap_连接spark 显示乱码_06


哈哈,不再有分区partition了吧。这种情况会把bucket合并到同一个文件,节约了磁盘I/O,最终才能提升性能,到这里,刚开始的那块儿代码的第二个判断宽块儿就介绍完了。第三个下片继续。