spark源码学习(十):map端计算结果缓存处理(二)
在介绍下面的内容之前,先介绍几个相关的概念:
(1) bypassMergeThreshold:表示是在map端做合并还是在reduce端做合并的一个参考数值。当partitions的个数小于这个阈值的时候,不需要在executor执行聚合和排序操作,只需要把各个partition写到executor的存储文件,最后在reduce端在做串联。可以设置set
spark.shuffle.sort.bypassMergeThreshold=num来修改这个数值。
(2) bypassMergeSort:他是一个布尔值,标志是否直接 将各个partition写入Executor的存储文件。当我们没有定义aggregator,ordering函数,并且partition的数量小于上面的那个阈值函数的时候,会把map中间结果直接输出到磁盘,此时并不会占用太多的内存,避免了OOM。
有了上面的预备,来看看map到底是怎样处理中间的计算结果的,大致上分为三种方式:
<1>map端的计算结果在缓存中执行聚合和排序。
<2>map会直接把各个partition写到自己的存储文件,最后由reduce统一进行合并和排序。
<3>map端计算结果的简单缓存。
上面的第二种情况就是当bypassmergeSort的数值为true的时候才会执行的。啰嗦了这么多,我们想知道,map任务到底是怎样执行的呢?在taskrunner.run函数中我们会最终会看到runtask函数,这是一个被子类实现的函数,具体的例如shuffleMapTask中的writer.write( ) 方法就是写map的中间结果,具体的进入到iterator会发现是迭代的去执行computeOrReadCheckpoint函数,有检查点的话就去读,否则就重新计算(compute函数)。compute函数是rdd中最重要的一个方法,主要是在rdd的每一个partition上运行task任务。我们知道在触发一个active之后,会进行stage的依赖划分以及相应的resultStage和activeJob的产生,之后运行的task是从最后一个RDD去开始运行的,但是任务怎么可以这样倒着运行?不可能吧,其实在运行最后一个resultTask的时候是倒着迭代iterator-->computeOrReadCheckpoint-->compute-->iteraotr(父类的)-->.....迭代去执行的。例如在wordcount中直到我们发现了HadoopRDD.compute函数才会从头到位的去执行任务。如下图所示:
现在开始进入正题,shuffleMapTask具体是怎样执行任务的。首先进入write.write方法去看看究竟。我们经过一段时间按照上图的迭代,就会发现进入了HadoopRdd的compute方法,至于这个class是哪里来的,以后讨论,这里不管它,进入sortShuffleWriter的write实现,然后在进入sort.insertAll( )方法的实现,在进入最最核心的map.changeValue方法的实现,这个方法会创建两个关键createCombiner和CombineValue,前一个是创建一个(#,1)的数据类型,后一个主要是执行对应value的相加,也就相当于map(x=>(x,1))和reducebykey操作。changeValue就会去执行这个两个函数,显示出函数式编程的威力,具体的下面与讨论。
/** * Set the value for key to updateFunc(hadValue, oldValue), where oldValue will be the old value * for key, if any, or null otherwise. Returns the newly updated value. */ def changeValue(key: K, updateFunc: (Boolean, V) => V): V = { assert(!destroyed, destructionMessage) //这里的k就是需要合并的value对应的key-->oldValue // oldValue和经过迭代器iterator得到的value进行相加得到newValue //这里表示的是当前的key是null,那么当然找不到对应的value与之相加,那么就直接返回就可以 //返回的就是(#,1) val k = key.asInstanceOf[AnyRef] if (k.eq(null)) { if (!haveNullValue) { incrementSize()//增加data的大小 } //haveNullValue代表的是false,也就是执行createCombiner函数 nullValue = updateFunc(haveNullValue, nullValue) haveNullValue = true return nullValue } //计算得到当期的key插入data数组中的位置 var pos = rehash(k.hashCode) & mask var i = 1//相当于hashCode while (true) { val curKey = data(2 * pos) if (k.eq(curKey) || k.equals(curKey)) {//这里的代表的是执行combineValue类型的函数,当前的CurKey在data中可以找到对应的k //data(2 * pos + 1).asInstanceOf[V]代表的是oldValue //那和oldValue相加的数值在哪里呢? //由insertAll中迭代iterator的while循环得到kv=records.next()得到的(key,value) val newValue = updateFunc(true, data(2 * pos + 1).asInstanceOf[V]) data(2 * pos + 1) = newValue.asInstanceOf[AnyRef] return newValue } else if (curKey.eq(null)) {//这里代表的是执行CreateCombiner函数,当前的CurKey在data中找不到对应的k //当前的CurKey等于null //既然等于null(代表原来没有)那么在合并之后当然就要incrementSize() val newValue = updateFunc(false, null.asInstanceOf[V]) data(2 * pos) = k data(2 * pos + 1) = newValue.asInstanceOf[AnyRef] incrementSize() return newValue } else {//当前的CurKey不等于null并且不等于k,迭代一直找到和上面两种情况相匹配的条件 val delta = i pos = (pos + delta) & mask i += 1 } } null.asInstanceOf[V] // Never reached but needed to keep compiler happy }