(二)常用Shuffle类算子:groupByKey、reduceByKey、aggregateByKey 和 sortByKey

       在数据分析场景中,典型的计算类型分别是分组、聚合和排序。而 groupByKey、reduceByKey、aggregateByKey 和 sortByKey 这些算子的功能,恰恰就是用来实现分组、聚合和排序的计算逻辑。

       这些算子看上去相比其他算子的适用范围更窄,也就是它们只能作用(Apply)在 Paired RDD 之上,所谓 Paired RDD,它指的是元素类型为(Key,Value)键值对的 RDD

      但是在功能方面,可以说,它们承担了数据分析场景中的大部分职责。因此,掌握这些算子的用法,是我们能够游刃有余地开发数据分析应用的重要基础。

      先来说说 groupByKey,坦白地说,相比后面的 3 个算子,groupByKey 在我们日常开发中的“出镜率”并不高。之所以要先介绍它,主要是为后续的 reduceByKey 和 aggregateByKey 这两个重要算子做铺垫。

groupByKey:分组收集

       groupByKey 的字面意思是“按照 Key 做分组”,但实际上,groupByKey 算子包含两步,即分组和收集。

       具体来说,对于元素类型为(Key,Value)键值对的 Paired RDD,groupByKey 的功能就是对 Key 值相同的元素做分组,然后把相应的 Value 值,以集合的形式收集到一起。换句话说,groupByKey 会把 RDD 的类型,由 RDD[(Key, Value)]转换为 RDD[(Key, Value 集合)]。

       这么说比较抽象,我们还是用一个小例子来说明 groupByKey 的用法。还是我们熟知的 Word Count,对于分词后的一个个单词,假设我们不再统计其计数,而仅仅是把相同的单词收集到一起,那么我们该怎么做呢?按照老规矩,咱们还是先来给出代码实现:

object ShuffleTest {
  def main(args: Array[String]): Unit = {
    // 这里的下划线"_"是占位符,代表数据文件的根目录
    val file: String = "D:\\testCode\\words.txt"
    // 读取文件内容
    //设置spark的配置文件信息
    val sparkConf: SparkConf = new SparkConf().setAppName("WordCount")
.setMaster("local[2]")
    //构建sparkcontext上下文对象,它是程序的入口,所有计算的源头
    val sc: SparkContext = new SparkContext(sparkConf)
    //读取文件
    val lineRDD: RDD[String] = sc.textFile(file)
    lineRDD.foreach(println)
    // 以行为单位做分词val
    val words: RDD[String] = lineRDD.flatMap(line => line.split(" "))

    words.foreach(println)
    // 过滤掉空字符串
    val cleanWordRDD: RDD[String] = words.filter(word => !word.equals(""))
    // 把RDD元素转换为pairedRDD的形式
    val kvRDD: RDD[(String, String)] = cleanWordRDD.map(word => (word, word))
    //按照单词做分组收集
    val wordsGroup:RDD[(String,Iterable[String])] = kvRDD.groupByKey()
    wordsGroup.foreach(println)

  }

}

得到的结果如下,对每个字段分组了

 reduceByKey:分组聚合

      reduceByKey 的字面含义是“按照 Key 值做聚合”,它的计算逻辑,就是根据聚合函数 f 给出的算法,把 Key 值相同的多个元素,聚合成一个元素。在Word Count 的实现中,我们使用了 reduceByKey 来实现分组计数


// 把RDD元素转换为(Key,Value)的形式
val kvRDD:RDD[(String, Int)]=cleanWordRDD.map(word=>(word,1))
 
// 按照单词做分组计数
val wordCounts:RDD[(String,Int)]=kvRDD.reduceByKey((x:Int,y:Int)=>x+y)

比如再实现下每次根据单词取出一个随机数,最后拿出每个单词的最大随机数

    val kvRandomRDD:RDD[(String,Int)]=cleanWordRDD.map(word=>(word,nextInt(100)))
    kvRandomRDD.foreach(println)
    def getmax(x:Int,y:Int)={
       math.max(x,y)
    }

    val wordsRandom= kvRandomRDD.reduceByKey(getmax)
    wordsRandom.foreach(println)

两次结果为

 

reduceByKey相对groupByKey的优势:

        尽管 reduceByKey 也会引入 Shuffle,但相比 groupByKey 以全量原始数据记录的方式消耗磁盘与网络,reduceByKey 在落盘与分发之前,会先在 Shuffle 的 Map 阶段做初步的聚合计算

      在数据分区 0 的处理中,在 Map 阶段,reduceByKey 把 Key 同为 Streaming 的两条数据记录聚合为一条,聚合逻辑就是由函数 f 定义的、取两者之间 Value 较大的数据记录,这个过程我们称之为“Map 端聚合”。相应地,数据经由网络分发之后,在 Reduce 阶段完成的计算,我们称之为“Reduce 端聚合”,在工业级的海量数据下,相比 groupByKey,reduceByKey 通过在 Map 端大幅削减需要落盘与分发的数据量,往往能将执行效率提升至少一倍。

      应该说,对于大多数分组 & 聚合的计算需求来说,只要设计合适的聚合函数 f,你都可以使用 reduceByKey 来实现计算逻辑。不过,术业有专攻,reduceByKey 算子的局限性,在于其 Map 阶段与 Reduce 阶段的计算逻辑必须保持一致,这个计算逻辑统一由聚合函数 f 定义。

reduceByKey的不足:要求在map和reduce阶段计算的逻辑不一致无法解决

        当一种计算场景需要在两个阶段执行不同计算逻辑的时候,reduceByKey 就爱莫能助了。比方说,还是第 1 讲的 Word Count,我们想对单词计数的计算逻辑做如下调整:在 Map 阶段,以数据分区为单位,计算单词的加和;而在 Reduce 阶段,对于同样的单词,取加和最大的那个数值。显然,Map 阶段的计算逻辑是 sum,而 Reduce 阶段的计算逻辑是 max。对于这样的业务需求,reduceByKey 已无用武之地,这个时候,就轮到 aggregateByKey 这个算子闪亮登场了。

aggregateByKey:更加灵活的聚合算子

      相比其他算子,aggregateByKey 算子的参数比较多。要在 Paired RDD 之上调用 aggregateByKey,你需要提供一个初始值,一个 Map 端聚合函数 f1,以及一个 Reduce 端聚合函数 f2,aggregateByKey 的调用形式如下所示:

val rdd: RDD[(Key类型,Value类型)] = _
rdd.aggregateByKey(初始值)(f1, f2)

       初始值可以是任意数值或是字符串,而聚合函数我们也不陌生,它们都是带有两个形参和一个输出结果的普通函数。就这 3 个参数来说,比较伤脑筋的,是它们之间的类型需要保持一致,具体来说:初始值类型,必须与 f2 的结果类型保持一致;f1 的形参类型,必须与 Paired RDD 的 Value 类型保持一致;f2 的形参类型,必须与 f1 的结果类型保持一致。

如图

        熟悉了 aggregateByKey 的用法之后,接下来,我们用 aggregateByKey 这个算子来实现刚刚提到的“先加和,再取最大值”的计算逻辑,代码实现如下所示:

// 把RDD元素转换为(Key,Value)的形式
val kvRDD: RDD[(String, Int)] = cleanWordRDD.map(word => (word, 1))
 
// 显示定义Map阶段聚合函数f1
def f1(x: Int, y: Int): Int = {
   return x + y
}
 
// 显示定义Reduce阶段聚合函数f2
def f2(x: Int, y: Int): Int = {
   return math.max(x, y)
}
 
// 调用aggregateByKey,实现先加和、再求最大值
val wordCounts: RDD[(String, Int)] = kvRDD.aggregateByKey(0) (f1, f2)

结果为

再通过这个算子看看过程

          在运行时,与 reduceByKey 相比,aggregateByKey 的执行过程并没有什么两样,最主要的区别,还是 Map 端聚合与 Reduce 端聚合的计算逻辑是否一致。值得一提的是,与 reduceByKey 一样,aggregateByKey 也可以通过 Map 端的初步聚合来大幅削减数据量,在降低磁盘与网络开销的同时,提升 Shuffle 环节的执行性能。

sortByKey:排序

         在这一讲的最后,我们再来说说 sortByKey 这个算子,顾名思义,它的功能是“按照 Key 进行排序”。给定包含(Key,Value)键值对的 Paired RDD,sortByKey 会以 Key 为准对 RDD 做排序。算子的用法比较简单,只需在 RDD 之上调用 sortByKey() 即可:


val rdd: RDD[(Key类型,Value类型)] = _
rdd.sortByKey()

在默认的情况下,sortByKey 按照 Key 值的升序(Ascending)对 RDD 进行排序,如果想按照降序(Descending)来排序的话,你需要给 sortByKey 传入 false。

总结下来,关于排序的规则,你只需要记住如下两条即可:

升序排序:调用 sortByKey()、或者 sortByKey(true);

降序排序:调用 sortByKey(false)。

总结

         一来,这 4 个算子的作用范围,都是 Paired RDD;二来,在计算的过程中,它们都会引入 Shuffle。而 Shuffle 往往是 Spark 作业执行效率的瓶颈,因此,在使用这 4 个算子的时候,对于它们可能会带来的性能隐患,我们要做到心中有数

        groupByKey 是无参算子,你只需在 RDD 之上调用 groupByKey() 即可完成对数据集的分组和收集。但需要特别注意的是,以全量原始数据记录在集群范围内进行落盘与网络分发,会带来巨大的性能开销。因此,除非必需,你应当尽量避免使用 groupByKey 算子

        利用聚合函数 f,reduceByKey 可以在 Map 端进行初步聚合,大幅削减需要落盘与分发的数据量,从而在一定程度上能够显著提升 Shuffle 计算的执行效率。对于绝大多数分组 & 聚合的计算需求,只要聚合函数 f 设计得当,reduceByKey 都能实现业务逻辑。reduceByKey 也有其自身的局限性,那就是其 Map 阶段与 Reduce 阶段的计算逻辑必须保持一致

        对于 Map 端聚合与 Reduce 端聚合计算逻辑不一致的情况,aggregateByKey 可以很好地满足这样的计算场景。aggregateByKey 的用法是 aggregateByKey(初始值)(Map 端聚合函数,Reduce 端聚合函数),对于 aggregateByKey 的 3 个参数,你需要保证它们之间类型的一致性。一旦类型一致性得到满足,你可以通过灵活地定义两个聚合函数,来翻着花样地进行各式各样的数据分析。

        最后,对于排序类的计算需求,你可以通过调用 sortByKey 来进行实现。sortByKey 支持两种排序方式,在默认情况下,sortByKey() 按 Key 值的升序进行排序,sortByKey() 与 sortByKey(true) 的效果是一样的。如果想按照降序做排序,你只需要调用 sortByKey(false) 即可。

猜你喜欢

转载自blog.csdn.net/someInNeed/article/details/121553197
今日推荐