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的函数,在特定情况下,性能提升数十倍不止,不过在使用的过程中一定要对该函数所对应的每个参数的含义了如指掌,这样运用起来才能得心应手。

spark算子之Aggregate的更多相关文章

  1. spark算子篇-aggregate 系列

    aggregate aggregate 是比较常用的 行动 操作,不是很好懂,这里做个解释. aggregate(zeroValue, seqOp, combOp) zeroValue 是一个初始值, ...

  2. (转)Spark 算子系列文章

    http://lxw1234.com/archives/2015/07/363.htm Spark算子:RDD基本转换操作(1)–map.flagMap.distinct Spark算子:RDD创建操 ...

  3. Spark算子总结及案例

    spark算子大致上可分三大类算子: 1.Value数据类型的Transformation算子,这种变换不触发提交作业,针对处理的数据项是Value型的数据. 2.Key-Value数据类型的Tran ...

  4. Spark算子总结(带案例)

    Spark算子总结(带案例) spark算子大致上可分三大类算子: 1.Value数据类型的Transformation算子,这种变换不触发提交作业,针对处理的数据项是Value型的数据. 2.Key ...

  5. UserView--第二种方式(避免第一种方式Set饱和),基于Spark算子的java代码实现

      UserView--第二种方式(避免第一种方式Set饱和),基于Spark算子的java代码实现   测试数据 java代码 package com.hzf.spark.study; import ...

  6. UserView--第一种方式set去重,基于Spark算子的java代码实现

    UserView--第一种方式set去重,基于Spark算子的java代码实现 测试数据 java代码 package com.hzf.spark.study; import java.util.Ha ...

  7. spark算子之DataFrame和DataSet

    前言 传统的RDD相对于mapreduce和storm提供了丰富强大的算子.在spark慢慢步入DataFrame到DataSet的今天,在算子的类型基本不变的情况下,这两个数据集提供了更为强大的的功 ...

  8. Spark算子---实战应用

    Spark算子实战应用 数据集 :http://grouplens.org/datasets/movielens/ MovieLens 1M Datase 相关数据文件 : users.dat --- ...

  9. spark算子集锦

    Spark 是大数据领域的一大利器,花时间总结了一下 Spark 常用算子,正所谓温故而知新. Spark 算子按照功能分,可以分成两大类:transform 和 action.Transform 不 ...

随机推荐

  1. 【Database】MySQL实战45讲

    01 | 基础架构:一条SQL查询语句是如何执行的? 1. MySQL 的基本架构图: MySQL可以分成: Server层 和 存储引擎层 两部分. Server层:包含连接器.查询缓存.分析器.优 ...

  2. Codeforces 961E - Tufurama 树状数组

    转自:https://blog.csdn.net/my_sunshine26/article/details/79831362 题目大意: i从1开始 基本思路: 完全没思路,所以上来就二分,果不其然 ...

  3. QML学习笔记(八)— QML实现列表侧滑覆盖按钮

    QML实现列表右边滑动删除按钮,并覆盖原有的操作按钮,点击可实现删除当前项 本文链接:QML实现列表侧滑覆盖按钮 作者:狐狸家的鱼 GitHub:八至 列表实现在另一篇博客已经提及,列表可选中.拖拽. ...

  4. 什么是http协议(二)

    一.概念 协议是指计算机通信网络中两台计算机之间进行通信所必须共同遵守的规定或规则,超文本传输协议(HTTP)是一种通信协议,它允许将超文本标记语言(HTML)文档从Web服务器传送到客户端的浏览器. ...

  5. mysql里面如何用sql语句让字符串转换为数字

    sql语句将字符串转换为数字默认去掉单引号中的空格,遇到空格作为字符串截止, SELECT '123 and 1=1' +0 结果为123 MySQL里面如何用sql语句让字符串的‘123’转换为数字 ...

  6. javaweb登录验证码的实现

    第一种 第一步:  JSP <li><input name="validCode"  id="validCode" type="te ...

  7. PHP基础知识总结(二) 数据类型、数组、变量和运算符

    一.PHP是服务器端的脚本语言.在服务器端解析完成后,向前端浏览器发送html. PHP文件的部署位置在:xampp\htdocs文件夹下,当然这个位置可以修改.二.PHP基本语法 1.文件后缀名是p ...

  8. leetcode中的一些二分搜索树

    235(最近公共祖先) /** * Definition for a binary tree node. * struct TreeNode { * int val; * TreeNode *left ...

  9. 用 Flask 来写个轻博客 (9) — M(V)C_Jinja 语法基础快速概览

    #目录 前文列表 扩展阅读 Jinja 变量名 注释 控制语句 if 语句 循环 过滤器 无参数调用 带参数调用 宏 定义宏 调用宏 结果 兼容 JavaScript 前文列表 用 Flask 来写个 ...

  10. Jeecg_Jflow整合记录

    系统组织机构 t_s_deparselect * from t_s_departselect * from t_s_depart where id='402888fd6a8c24e9016a8c531 ...