spark算子之Aggregate

Aggregate函数

一、源码定义

/**
   * Aggregate the elements of each partition, and then the results for all the partitions, using
   * given combine functions and a neutral "zero value". This function can return a different result
   * type, U, than the type of this RDD, T. Thus, we need one operation for merging a T into an U
   * and one operation for merging two U's, as in scala.TraversableOnce. Both of these functions are
   * allowed to modify and return their first argument instead of creating a new U to avoid memory
   * allocation.
   *
   * @param zeroValue the initial value for the accumulated result of each partition for the
   *                  `seqOp` operator, and also the initial value for the combine results from
   *                  different partitions for the `combOp` operator - this will typically be the
   *                  neutral element (e.g. `Nil` for list concatenation or `0` for summation)
   * @param seqOp an operator used to accumulate results within a partition
   * @param combOp an associative operator used to combine results from different partitions
   */
  def aggregate[U: ClassTag](zeroValue: U)(seqOp: (U, T) => U, combOp: (U, U) => U): U = withScope {
    // Clone the zero value since we will also be serializing it as part of tasks
    var jobResult = Utils.clone(zeroValue, sc.env.serializer.newInstance())
    val cleanSeqOp = sc.clean(seqOp)
    val cleanCombOp = sc.clean(combOp)
    val aggregatePartition = (it: Iterator[T]) => it.aggregate(zeroValue)(cleanSeqOp, cleanCombOp)
    val mergeResult = (index: Int, taskResult: U) => jobResult = combOp(jobResult, taskResult)
    sc.runJob(this, aggregatePartition, mergeResult)
    jobResult
  }

  首先,大致解释一下源码中的定义:

  因为通常为我们的spark程序计算是分布式的,所以我们通常需要聚合的数据都分部在不同的分区,不同的机器上。

  该函数它会首先对每个分区内的数据基于初始值进行一个首次聚合,然后将每个分区聚合的结果,通过使用给定的聚合函数,再次基于初始值进行分区之间的聚合,并且最终干函数的返回结果的类型,可以是与该RDD的类型相同。什么意思呢,其实这样说,还是有点蒙圈的,下面详细道来。

  从源码中可以看出,该函数是一个柯里化函数,它需要接受一共三个参数,分别是:

  • zeroValue:U

    这个值代表的是我们需要设置的初始值,该初始值可以是不与原RDD的元素的类型相同,可以是Int,String,元组等等任何我们所需要的类型,根据自己的需求来,为了方便后面的表示,假设我把它定义为数值类型的元组(0,0),注意,这里必须是具体的值,并非函数

  • seqOp: (U, T) => U

    这里需要定义一个函数,注意,是函数,U的类型与我们在第一步中定义的初始值得类型相同,所以,这里的U指的就是(Int,Int)类型

    这里的T代表的即为RDD中每个元素的值。

    该函数的功能是,在每个分区内遍历每个元素,将每个元素与U进行聚合,具体的聚合方式,我们可以自定义,不过有一点需要注意,这里聚合的时候依然要基于初始值来进行计算

  • combOp: (U, U) => U

    这里同样需要定义一个函数,这里的U即为每个分区内聚合之后的结果,如上,上一步中的U为(Int,Int)类型,则这里的U也为该类型

    该函数的主要作用就是对每个分区聚合之后的结果进行再次合并,即分区之间的合并,但是,同样,在合并的开始,也是要基于初始进行合并,其实这里我们可以发现,这里U的类型是与初始值的类型是相同的。

上面啰里啰嗦的说了这么所,感觉还是不太直观,上代码瞧瞧:

案例1:求取给定RDD的平均数

object TestAggreate {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setAppName(this.getClass.getSimpleName).setMaster("local[2]")
    val sc = new SparkContext(conf)
    sc.setLogLevel("warn")

    val rdd1 = sc.parallelize(Seq(("a", 2), ("a", 5), ("a", 4), ("b", 5), ("c", 3), ("b", 3), ("c", 6), ("a", 8)), 1)

    val r1 = rdd1.aggregate((0, 0))(
      (u, c) => (u._1 + 1, u._2 + c._2),
      (r1, r2) => (r1._1 + r2._1, r1._2 + r2._2)
    )
    println(r1)

    sc.stop()
  }
}

  这里先给出运行结果,再来解释:

  

 

   首先我们需要确定,该RDD的分区数为1,也就是说所有的数据都是在一个分区内进行计算,其次该RDD的类型是RDD[(String,Int)],我的目标是求该RDD总个数以及第二个值得总和

  1、首先定义初始值,该例子中我定义为了(0,0),是一个(Int,Int)类型的,我准备第一个0代表计数,第二个0代表对每个元素进行求和

  2、(u,c),这个函数,这里的u类型就是(Int,Int)类型,c指的就是RDD中的每个元素,每遍历一个元素c,u的第一个元素就会加1,也就是u._1 + 1,同时u的第二个元素会对c的第二个元素进行累加,也就是u._2 + c._2,不过这里的累加都是要基于初始值进行累加的,顺序是这样的:

    第一次  0+1,0+2

    第二次  1+1,2+5

    第三次  2+1,7+4

    第四次  3+1,11+5

    第五次  4+1,16+3

    第六次  5+1,19+3

    第七次  6+1,22+6

    第八次  7+1,28+8

  最终结果就是(8,36)

  3、(r1.r2),该函数是实现每个分区内的数据进行合并,因为这里只有一个分区,所以只是分区0与另外一个空分区进行合并。

  这里如果我们将分区数设置为超过1个的情况下,会怎样呢,来看下:

bject TestAggreate {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setAppName(this.getClass.getSimpleName).setMaster("local[2]")
    val sc = new SparkContext(conf)
    sc.setLogLevel("warn")

    val rdd1 = sc.parallelize(Seq(("a", 2), ("a", 5), ("a", 4), ("b", 5), ("c", 3), ("b", 3), ("c", 6), ("a", 8)), 4)

    val r1 = rdd1.aggregate((0, 0))(
      (u, c) => (u._1 + 1, u._2 + c._2),
      (r1, r2) => (r1._1 + r2._1, r1._2 + r2._2)
    )
    println(r1)

    sc.stop()
  }
}

  这种情况下,我们将RDD分在了四个分区内,每个分区内分配两个数据,具体每个分区内有哪几个元素,可以这样查看:

rdd1.foreachPartition(part => {
      val partitionId = TaskContext.getPartitionId()
      part.foreach(x => {
        println((partitionId, x._1, x._2))
      })
    })

  

   从上面可以看出,分区数据分别是存在了part0((a,2),(a,5)),part1((a,4),(b,5)),part1((c,3),(b,3)),part3((c,6),(a,8)),这种情况下的合并过程是这样的:

  1、每个分区内合并,结果就是 part0(0+1+1,0+2+5)  part1(0+1+1,0+4+5)  part2(0+1+1,0+3+3)  part3(0+1+1,0+6+8)

  2、(part0,part1) => (part0._1 + part1._1,part0._2+part1._2),然后使用该结果,在依次与part2,part3进行合并,结果就为(0+2+2+2+2,0+7+9+6+14),结果(8,36)

  这里我在测试的过程中发现一个问题,就是说在分区数大于1的情况下,当我最后将分区合并的函数中的聚合过程,相互颠倒过来的话,也就是,正常,我应该得到(8,36),但是我聚合的时候想得到(36,8)这个结果,下面这段代码:

val r1 = rdd1.aggregate((0, 0))(
      (u, c) => (u._1 + 1, u._2 + c._2),
      (r1, r2) => (r1._2 + r2._2, r1._1 + r2._1)
    )

   上面标红的代码,我颠倒了顺序,我的预期的得到(36,8),但是结果却是随机产生的结果,像这样:

  

     

 

   上面是执行了两次,产生了两次不同的结果,但是显然是错误的。但是具体它是怎么计算出来的,博主现在目前还没有研究出来。

案例2:求和

  该案例主要是测试一下初始值的变化对结果产生的影响,进一步证明,不管是在分区内进行聚合还是分区之间进行聚合的时候,都会使用到初始值,案例1中的初始值我们都设置的是0,此时我将其设置成2在来看看结果,测试代码:

object TestAggreate {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setAppName(this.getClass.getSimpleName).setMaster("local[2]")
    val sc = new SparkContext(conf)
    sc.setLogLevel("warn")

    val rdd1 = sc.parallelize(Seq(("a", 2), ("a", 5), ("a", 4), ("b", 5), ("c", 3), ("b", 3), ("c", 6), ("a", 8)), 4)

    val r1 = rdd1.aggregate((0))(
      (u, c) => (u + c._2),
      (r1, r2) => (r1 + r2)
    )
    println(r1)

    sc.stop()
  }
}

  结果:(46),但是实际之和加起来是36,显然多出了10,这个10是怎么来的呢?

  计算方式如下:

  1、首先这里是4个分区,每个分区进行聚合,而之前说过,分区内聚合都是要以初始值为基准的,也就是说要在初始值得基础上进行相加:

    part0 (2+2+5)

    part1(2+4+5)

    part2(2+3+3)

    part3(2+6+8)

  2、其次是分区之间的聚合,分区之间的聚合也是要在初始值的基础上相加的,即

    2+part0+part1+part2+part3

  结果即为46,

  所以说,如果我们想要得到预想的结果,对于该函数生成的结果还要减去如下数:

  result-initValue*(partitions+1)

总结一下:

  该函数是spark中的一个高性能的算子,它实现了先进性分区内的聚合之后在进行了对每个分区的聚合结果再次进行聚合的操作,这样的在大数据量的情况下,大大减少了数据在各个节点之间不必要的网络IO,大大提升了性能,相比于groupBy的函数,在特定情况下,性能提升数十倍不止,不过在使用的过程中一定要对该函数所对应的每个参数的含义了如指掌,这样运用起来才能得心应手。

转载于:https://www.cnblogs.com/Gxiaobai/p/11437739.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值