Spark的RDD连续转换操作有时需要注意强行触发action执行操作,否则(Tansformation)的惰性(lazy)机制会导致结果错误

最近通过spark做一些数据处理,遇到一些诡异的现象

我开发了一个随机生成海量数据点的程序,因为要保证这些点具有自增序号,不适合直接map分布式做(几十亿的数据,map计算需要分区(不主动分区估计也会自动分区,spark自带的数据累加逻辑只能对单个partition分区内有效),需要在driver里进行序号计算,所以就想通过数组分批生成数据,转换成RDD,在依次拼接(union)起来,就是下面的代码。

 val array = ArrayBuffer[(String,String)]()
 var i=0l
 var rdd:RDD[(String,String)] = sc.makeRDD(array)
 
 for(i<- 1l to size)
 {
        val name = "王".toString.concat((i % 1000).toString)
        array +=((i.toString, name))
        if(i%part_size == 0)
        {
            val rdd1 = sc.makeRDD(array)
            rdd1.cache
            val pre_rdd = rdd
            rdd= rdd.union(rdd1)
            rdd.cache()
            array.clear()
            rdd1.unpersist()
            pre_rdd.unpersist()
          }

    }
    if(array.length>0)
    {
      val rdd1 = sc.parallelize(array)
      rdd1.cache
      val pre_rdd = rdd
      rdd=rdd.union(rdd1)
      rdd.cache()
      pre_rdd.unpersist()
      rdd1.unpersist()
    }

好了,经验丰富或者了解相关基础知识的同学,知道上面代码有问题后,应该很快能看出问题在哪儿了,其他人是不是看着计算逻辑挺正常?

但如果我输入size=8,part_size=5,就是输出8个点,分批计算,每批算5个点。不管分几批,对结果应该没影响,最终结果就是

(1,xxxx)(2,xxxx)(3,xxxx)....(8,xxxx)共8个点

实际结果是:(6,xxxx)(7,xxxx)(8,xxxx)(6,xxxx)(7,xxxx)(8,xxxx)(6,xxxx)(7,xxxx)(8,xxxx)九个点,震惊了有没有?

仔细分析,看得出, 是把第二批重复了三次,根据这个线索,按图索骥查询资料,发现这一切是spark的RDD惰性计算(lazy机制)的锅。

因为spark的RDD操作分为两种操作模式,转换操作(transformation)和行动操作(action),具体可以参考以下两篇文章,能得到一个清晰的初步了解:

RDD转换操作transformation和行动操作action文章1

RDD转换操作transformation和行动操作action文章2

扫描二维码关注公众号,回复: 3257251 查看本文章

总而言之,就是transformation操作主要是对每个RDD中的元素进行处理并生成新的RDD;而action则主要是对RDD进行最后的操作,比如遍历、reduce、保存到文件等(虽然最终可能还是保存到一个新的RDD上,但至少从设计上是具备输出能力的),并可以返回结果给Driver程序。这样因为transformation肯定不会输出,spark就设定了惰性机制(lazy特性),当没有出现action操作的时候,所有RDD转换操作不会执行,程序会为其生成DAG,直到遇到action才触发,这样做的好处时有利于加强并行计算,减少中间结果。比如程序里进行了大量的转换操作,最后才reduce并输出,前面转换操作就可以生成DAG后,不同stage并行计算,甚至可能复用中间结果提高计算效率。这是为了spark的优化机制服务的。

所以,如果我们有如下代码:

val rdd1=makeRDD(...)
val rdd2=rdd1.map(...).filter(...)
val rdd3=makeRDD(...)
val rdd4= rdd1.join(rdd3)
val rdd5=rdd2.join(rdd4).groupByKey(...)
val rdd6=rdd5.reduceByKey(...)
val rdd7= rdd4.reduce(...)

只有执行到RDD6的时候,rdd1到rdd5才会被优化执行计算出来,而且是并行的,是依次得:

array->rdd1

 rdd1-->rdd2

array->rdd3

===rdd4===

array->rdd1--> 

                                  -->join -->rdd4

  array->rdd13-->

===rdd5===

array-->rdd1-->rdd2                          -->

                                                                       -->join-->groupByKey-->rdd5

 array->rdd1--> 

                                  -->join -->rdd4 -->

 array->rdd13-->

可以看得出,漴副计算特别多,中间RDD并未重复利用,这就是为什么就算结果正确,在lazy计算中也要引入cache或者persisit来缓存中间结果以便重复利用。

那么回到我们刚开始提到的例子,当整个代码真正开始执行的时候,已经是函数返回rdd后在别的地方被调用的时候了,此时array里只有{(6,xxxx)(7,xxxx)(8,xxxx)},这样三次从array里makeRDD出来的rdd自然都一样,然后拼出了错误的结果。

要想结果正确,可以这么改:

val array = ArrayBuffer[(String,String)]()
 var i=0l
 var rdd:RDD[(String,String)] = sc.makeRDD(array)
 
 for(i<- 1l to size)
 {
        val name = "王".toString.concat((i % 1000).toString)
        array +=((i.toString, name))
        if(i%part_size == 0)
        {
            val rdd1 = sc.makeRDD(array)
            rdd1.cache
            val pre_rdd = rdd
            rdd= rdd.union(rdd1)
            rdd.count //action操作触发之前的RDD操作执行
            rdd.cache()
            array.clear()
            rdd1.unpersist()
            pre_rdd.unpersist()
          }

    }
    if(array.length>0)
    {
      val rdd1 = sc.parallelize(array)
      rdd1.cache
      val pre_rdd = rdd
      rdd=rdd.union(rdd1)
      rdd.count
      rdd.cache()
      pre_rdd.unpersist()
      rdd1.unpersist()
    }

虽然rdd.count从数据逻辑上看是毫无意义的,但对于spark转换计算却是一个不得已的办法。

人为刻意去触发操作执行的情况,在我们spark开发中需要额外注意,像这种RDD连续操作中因为同一数据源变量(array)发生变化导致需要中间专门提前触发操作执行的是一种典型场景,还有另一种场景就是RDD转换操作中有随机数生成的逻辑,参考下面的代码;

val rdd1 = makeRDD(array)
val rdd2 = rdd1.map{ t=>
      val  ran= ((new Random).nextLong().abs % len)
      (ran,t)}
val rdd3 = rdd2.groupByKey()

rdd2.collect().foreach(println)
rdd3.collect().foreach(println)

打印结果发现,rdd3和rdd2的结果匹配不上,为什么呢?就因为当我们最后collect的时候,rdd2和rdd3分别按照自己的DAG规划,并行计算,rdd3依赖rdd2,算rdd3的时候会把rdd2又计算一次,为什么呢?因为rdd3的计算前全都是转换操作,不会和driver发生数据交互,虽然rdd2也单独算过一次了,但对于其他转换操作来说也不会来复用,实际上RDD3计算的时候,代码相当于执行了:

val rdd3 = rdd1.map{ t=>
      val  ran= ((new Random).nextLong().abs % len)
      (ran,t)}.groupByKey().collect().foreach(println)

所以就相当于计算rdd3时 ,随机数key重新生成了一次,自然和rdd2的输出对不上了。

这里面就得在rdd3的转化前,或者说在具有随机因子的转化操作后(生成rdd2),马上触发一个执行操作,生成rdd2.并且cache起来,让后面调用rdd2的语句直接从cache里取 中间结果,避免重复计算:

val rdd1 = makeRDD(array)
val rdd2 = rdd1.map{ t=>
      val  ran= ((new Random).nextLong().abs % len)
      (ran,t)}
rdd2.count //action行动操作,触发上面的语句执行,这一句在本段代码场景中不是必须的,
           //因为后面cache和collect共同发挥做作用,也起到了在执行rdd3操作时会取rdd2的cache
rdd2.cache //这个是至关重要的,包含随机数的转换计算,不仅要提前触发,还要被缓存起来才行
val rdd3 = rdd2.groupByKey()

rdd2.collect().foreach(println)
rdd3.collect().foreach(println)

注意里面的注释,解决这个问题,实际上用到了rdd转换操作中的cache机制的作用,实际上cahce操作,也是在action操作触发后才执行,所以说,是否及时触发action操作还是很重要。

好了,如果看了本文,有助于你更直观的理解RDD的tansfromation,action,cache,persisit的设计理念以及实践中的价值或者用不好产生的问题,那我的文章就没白写,毕竟讲这几个概念的文章还是很多的。

猜你喜欢

转载自blog.csdn.net/officercat/article/details/82114271
今日推荐