Spark每日半小时(8)——PairRDD的聚合操作

当数据集以键值对形式组织的时候,聚合具有相同键的元素进行一些统计是很常见的操作。前面讲解过基础RDD上的fold()、combine()、reduce()等行动操作,pairRDD上则有响应的针对键的转化操作。Spark有一组类似的操作,可以组合具有相同键的值。这些操作返回RDD,因此他们是转化操作而不是行动操作。

reduceByKey()

reduceByKey()与reduce()相当类似:他们都接收一个函数,并使用该函数对值进行合并。reduceByKey()会为数据集中的每个键进行并行的归约操作,每个归约操作会将键相同的值合并起来。因为数据集中可能有大量的键,所以reduceByKey()没有被实现为向用户程序返回一个值的行动操作。实际上,它会返回一个有各键和对应键归约出来的结果值组成的新的RDD。

foldByKey()

foldByKey()则与fold()相当类似:他们都使用一个与RDD和合并函数中的数据类型相同的零值作为初始值。与fold()一样,foldByKey()操作所使用的合并函数对零值与另一个元素进行合并,结果仍为该元素。

我们可以使用reduceByKey()和mapValue()来计算每个键的对应值的均值。这和使用fold()和map()计算整个RDD平均值的过程很相似。对于求平均值,可以使用更加专用的函数来获取同样的结果,后面就会讲到。

我们也可以使用下面展示的方法来解决经典的分布式单词计数问题。可以使用之前讲过的flatMap()来生成以单词为键、以数字1为值的pairRDD,然后使用reduceByKey()对所有的单词进行计数。

/**
 * @author DKing
 * @description
 * @date 2019/6/4
 */
public class CalculateWordsCount {
    public static void main(String[] args) {
        SparkConf conf = new SparkConf().setAppName("wordCount").setMaster("yarn");
        JavaSparkContext sc = new JavaSparkContext(conf);

        JavaRDD<String> input = sc.textFile("hdfs://...");
        JavaRDD<String> words = input.flatMap(
                (FlatMapFunction<String, String>) s
                        -> (Iterator<String>) Arrays.asList(s.split(" "))
        );
        JavaPairRDD<String, Integer> wordMap = words.mapToPair(
                (PairFunction<String, String, Integer>) s -> new Tuple2<>(s, 1)
        );
        JavaPairRDD<String, Integer> result = wordMap.reduceByKey(
                (Function2<Integer, Integer, Integer>) (integer, integer2) -> integer + integer2
        );
    }
}

combineByKey()

combineByKey()是最为常用的基于键进行聚合的函数。大多数基于键聚合的函数都是用它实现的。和aggregate()一样,combineByKey()可以让用户返回与输入数据的类型不同的返回值。

要理解combineByKey(),要先理解它再处理数据时时如何处理每个元素的。由于combineByKey()会遍历分区中的所有元素,因此每个元素的键要么还没有遇到过,要么就和之前的某个元素的键相同。

如果这是一个新的元素,combineByKey()会使用一个叫做createCombiner()的函数来创建那个键对应的累加器的初始值。需要注意的是,这一过程会在每个分区中第一次出现各个键时发生,而不是再整个RDD中第一次出现一个键时发生。

如果这是一个再处理当前分区之前已经遇到的键,他会使用mergeValue()方法将该键的累加器对应的当前值与这个新的值进行合并。

由于每个分区都是独立处理的,因此对于同一个键可以有多个累加器。如果两个或者更多的分区都有对应同一个键的累加器,就需要使用用户提供的mergeCombiners()方法将各个分区的结果进行合并。

combineByKey()有多个参数分别对应聚合操作的各个阶段,因而非常适合用来解释聚合操作各个阶段的功能划分。为了更好地演示combineByKey()是如何工作的,下面来看看如何计算各个键对应的平均值。

/**
 * @author DKing
 * @description
 * @date 2019/6/4
 */
public class CombineByKeyTest {
    public static void main(String[] args) {
        Function<Integer, AvgCount> createAcc = integer
                -> new AvgCount(integer, 1);
        Function2<AvgCount, Integer, AvgCount> addAndCount =
                (Function2<AvgCount, Integer, AvgCount>) (avgCount, integer) -> {
                    avgCount.total += integer;
                    avgCount.num += 1;
                    return avgCount;
                };

        Function2<AvgCount, AvgCount, AvgCount> combine =
                (Function2<AvgCount, AvgCount, AvgCount>) (avgCount, avgCount2) -> {
                    avgCount.total += avgCount2.total;
                    avgCount.num += avgCount2.num;
                    return avgCount;
                };
        AvgCount initial = new AvgCount(0, 0);
        JavaPairRDD nums = null;
        JavaPairRDD<String, AvgCount> avgCounts =
                nums.combineByKey(createAcc, addAndCount, combine);
        Map<String, AvgCount> countMap = avgCounts.collectAsMap();
        for (Map.Entry<String, AvgCount> entry : countMap.entrySet()) {
            System.out.println(entry.getKey() + ":" + entry.getValue().avg());
        }
    }

    private static class AvgCount implements Serializable {
        private int total;
        private int num;

        public AvgCount(int total, int num) {
            this.total = total;
            this.num = num;
        }

        public float avg() {
            return this.total / this.num;
        }
    }
}

有很多函数可以进行基于键的数据合并。它们中的大多数都是在combineByKey()的基础上实现的,为用户提供了更简单的接口。

并行度调优

到目前为止,我们已经讨论了所有的转化操作的分发方式,但是还没有探讨Spark是怎样确定如何分割工作的。每个RDD都有固定数目的分区,分区数决定了在RDD上执行操作时的并行度。

在执行聚合或分组操作时,可以要求Spark使用给定的分区数。Spark始终尝试根据集群的大小推断出一个有意义的默认值,但是有时候你可能要对并行度进行调优来获取更好的性能表现。

在Java中自定义reduceByKey()的并行度

sc.parallelize(Arrays.asList(1, 2, 3, 4));        //默认并行度
sc.parallelize(Arrays.asList(1, 2, 3, 4), 10);    //自定义并行度

有时,我们希望在除分组操作之外的操作中也能改变RDD的分区。对于这样的情况,Spark提供了repartition()函数。他会把数据通过网络进行混洗,并创建出新的分区集合。切记,对数据进行重新分区是代价相对比较大的操作。Spark也有一个优化版的repartition(),叫做coalesce()。你可以使用Java或者Scala中的rdd.partitions.size()查看RDD的分区数,并确保调用coalesce()时将RDD合并到比现在的分区数更少的分区中。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值