Learning Spark - LIGHTNING-FAST DATA ANALYSIS 第四章 - (2)

来源:互联网 发布:如何做好软件项目经理 编辑:程序博客网 时间:2024/05/16 17:45

接着续,每天5分钟:Learning Spark - LIGHTNING-FAST DATA ANALYSIS 第四章 - (1)


聚合

当数据集被表述成键值对,通常是想要对所有元素按相同的键进行聚合统计。我们已经看到了fold()combin()reduce()这些动作作用于基本RDD。类似的也存在对pair RDD的每个键的变换。Spark提供了按相同的键合并的一组类似的操作。这些返回RDD的操作是变换,而不是动作。

 

reduceByKey()reduce()非常相似,都带了一个函数参数用于合并。reduceByKey()运行多个reduce()操作,数据集中的每个键对应一个,以便对相同的键进行合并。因为数据集可以有非常多的键,所以reduceByKey()没有实现成返回一个值到用户程序的动作,而是返回一个由每个键及对应合并后的值组成的新RDD

 

foldByKey()fold()十分相似,都有一个和RDD中数据同类型的零值和合并函数。和fold()一样,为foldByKey()提供的零值应用到合并函数与其他元素相加时应该没有副作用。

如示例4-74-8所示,我们可以把reduceByKey()mapValues()一起使用来计算每个键的平均值,这跟用fold()map()一起计算整个RDD的均值(见图4-2)十分相似。对于平均值,我们还可以用一个更特殊函数来得到相同的结果,后面会讲到。

 

示例4-7. Python中用reduceByKey()mapValues计算每个键的均值

rdd.mapValues(lambda x: (x, 1)).reduceByKey(lambda x, y: (x[0] + y[0], x[1] + y[1]))


示例4-7. Scala中用reduceByKey()mapValues计算每个键的均值
rdd.mapValues(x => (x, 1)).reduceByKey((x, y) => (x._1 + y._1, x._2 + y._2))

 

 

图 4-2 每个键的平均值的数据流

 

这些来自MapReduce的熟悉的合并概念需要指出的是调用reduceByKey()foldByKey()会在每个机器上本地自动合并,然后再对每个键进行全局计算。用户不用再指定合并函数。而更一般的combineByKey()接口则允许你可以自定义合并行为。

 

 

我们在示例4-94-11中用类似方式来实现经典的分布式单词计数问题。使用前一章中的flatMap()来生成一个单词和数值1pair RDD,然后像示例4-74-8那样使用reduceByKey()来计算这些单词的总数。

 

示例4-9. Python中的Word count
rdd = sc.textFile("s3://...")
words = rdd.flatMap(lambda x: x.split(" "))
result = words.map(lambda x: (x, 1)).reduceByKey(lambda x, y: x + y)


示例4-10. Scala中的Word count
val input = sc.textFile("s3://...")
val words = input.flatMap(x => x.split(" "))
val result = words.map(x => (x, 1)).reduceByKey((x, y) => x + y)


示例4-11. Java中的Word count
JavaRDD<String> input = sc.textFile("s3://...")
JavaRDD<String> words = rdd.flatMap(new FlatMapFunction<String, String>() {
    public Iterable<String> call(String x) { return Arrays.asList(x.split(" ")); }
});
JavaPairRDD<String, Integer> result = words.mapToPair(
    new PairFunction<String, String, Integer>() {
        public Tuple2<String, Integer> call(String x) { return new Tuple2(x, 1); }
}).reduceByKey(
    new Function2<Integer, Integer, Integer>() {
        public Integer call(Integer a, Integer b) { return a + b; }
});

实际上,我们可以对第一个RDD使用countByValue()更快的实现单词计数:input.flatMap(x => x.split(“ ”)).countByValue()

 

combineByKey()是针对每个键的更一般的聚合函数。大多数其他的对每个键合并的函数都是用它实现的。和aggregate()一样,combinByKey()允许用户返回和输入数据类型不同的值。

 

要理解combineByKey(),想想它是如何处理每个元素会有些帮助。combineByKey()会遍历一个分区中的所有元素。每个元素都有一个键,要么之间没出息过,要么之前已经出现过。如果是新元素,combineByKey()会创建combiner(),也就是我们提供给它的函数来为这个新的键创建初值用于累积计算。这里有一点很重要,这个第一次是每个分区中新的键的第一次出现,而不是整个RDD第一次出现。

 

如果在这个分区这个键之前出现过,就对这个键进行多次累计就行了。当我们合并各个分区的数据时,如果两个以上的分区对于同一个键都有累积值,可以用用户提供的mergeCombiner()合并即可。

我们可以在combineByKey()中禁用map-side聚合,如果我们知道这对我们没有好处的话。比如groupByKey()禁用了map-side聚合,因为聚合函数(追加到list中)并不会节省空间。如果要禁用,需要指定partitioner。目前你可以通过传入rdd.partitioner给源RDD来指定。

 

由于combineByKey()有很多不一样的参数,很适合一个解释性的例子。要更好的理解combineByKey()如何工作,我们看看为每个键计算平均值,见示例4-124-14和图4-3

 

示例4-12. Python中使用combineByKey()计算每个键的平均值
sumCount = nums.combineByKey((lambda x: (x,1)),
    (lambda x, y: (x[0] + y, x[1] + 1)),
    (lambda x, y: (x[0] + y[0], x[1] + y[1])))
sumCount.map(lambda key, xy: (key, xy[0]/xy[1])).collectAsMap()


示例4-13. Scala中使用combineByKey()计算每个键的平均值
val result = input.combineByKey(
    (v) => (v, 1),
    (acc: (Int, Int), v) => (acc._1 + v, acc._2 + 1),
    (acc1: (Int, Int), acc2: (Int, Int)) => (acc1._1 + acc2._1, acc1._2 + acc2._2)
).map{ case (key, value) => (key, value._1 / value._2.toFloat) }
result.collectAsMap().map(println(_))


示例4-12. Java中使用combineByKey()计算每个键的平均值
public static class AvgCount implements Serializable {
    public AvgCount(int total, int num) { total_ = total; num_ = num; }
    public int total_;

    public int num_;
    public float avg() { return total_ / (float) num_; }
}
Function<Integer, AvgCount> createAcc = new Function<Integer, AvgCount>() {
    public AvgCount call(Integer x) {
        return new AvgCount(x, 1);
    }
};
Function2<AvgCount, Integer, AvgCount> addAndCount =
    new Function2<AvgCount, Integer, AvgCount>() {
        public AvgCount call(AvgCount a, Integer x) {
            a.total_ += x;
            a.num_ += 1;
            return a;
    }
};
Function2<AvgCount, AvgCount, AvgCount> combine =
    new Function2<AvgCount, AvgCount, AvgCount>() {
        public AvgCount call(AvgCount a, AvgCount b) {
            a.total_ += b.total_;
            a.num_ += b.num_;
            return a;
    }
};
AvgCount initial = new AvgCount(0,0);
JavaPairRDD<String, AvgCount> avgCounts =
    nums.combineByKey(createAcc, addAndCount, combine);
Map<String, AvgCount> countMap = avgCounts.collectAsMap();
for (Entry<String, AvgCount> entry : countMap.entrySet()) {
    System.out.println(entry.getKey() + ":" + entry.getValue().avg());
} 

 

 

图 4-3 combineByKey()示例的数据流

 

Spark中有很多根据键来合并数据的操作。大多数都是用combineByKey()来实现的,只是提供了一个更简单的接口。无论如何,用Spark中的这些特定的聚合函数都会比单纯的对数据分组后再归约要快得多。

调整并行级别

到目前为止,我们谈到的所有的变换都是分布式的。但是没有真正的了解过Spark是如何决定来怎样对处理进行分割的。每个RDD都有一个固定分分区数,这就决定了当对RDD进行操作时的并行度。

 

当进行聚合或者分组操作,我们可以要求Spark指定一个分区数。Spark总是会根据集群的大小来试图推断出一个合适的默认值,但有时你会想要调整并行级别以获得更好的性能。

 

当创建分组后的或者聚合后的RDD时,本章讨论的大多数操作都接受给定一个分区数作为第二个参数来使用。见示例4-154-16

 

示例4-15. Python中自定义并行度执行reduceByKey()
data = [("a", 3), ("b", 4), ("a", 1)]
sc.parallelize(data).reduceByKey(lambda x, y: x + y) # Default parallelism
sc.parallelize(data).reduceByKey(lambda x, y: x + y, 10) # Custom parallelism


示例4-16. Scala自定义并行度执行reduceByKey()
val data = Seq(("a", 3), ("b", 4), ("a", 1))
sc.parallelize(data).reduceByKey((x, y) => x + y) // Default parallelism
sc.parallelize(data).reduceByKey((x, y) => x + y) // Custom parallelism

有时我们需要对RDD的分组或聚合操作的上下文之外来改变分区。对这些情况,Spark提供了repartition()函数。它可以跨网络对数据进行shuffle,并创建一组新的分区。记住,对数据进行重新分区是一个相当昂贵的操作。Spark也有一个repartition()的优化版本叫做coalesce()。它可以避免数据移动,但是仅限于减少RDD的分区数的情况。要知道你是否能安全的调用coalesce()函数,可以在Java/Scala中调用rdd.partitions.size()或者在Python中调用rdd.getPartitions()来检查RDD的分区数,以确保你正在合并到比当期更少的分区。

 

数据分组

有主键的数据的惯用法是根据键来对数据分组。比如查看一个客户的所有订单。

 

如果数据已经以我们想要的方式有了主键,groupByKey()就可以用RDD中的主键对我们的数据进行分组。对于一个由类型为K的主键和类型为V的值组成的RDDgroupByKey()后我们得到的RDD类型为[K, Iterable[V]]

 

groupBy()用于非键值对的数据或者是使用当前主键之外的不同条件分组的情况。它传入一个函数应用到源RDD的每个元素,并将结果作为主键。

 

如果你发现你写的代码先groupByKey()然后对返回值执行reduce()fold(),那么你大概可以通过调用某个基于主键的聚合函数更高效的得到相同的结果。应该对数据按每个主键进行归约得到每个主键对应的归约后的值的RDD,而不是归约数据到一个内存值中。例如,rdd.reduceByKey(func)生成了和rdd.groupByKey().mapValues(value => value.reduce(func))相同的RDD,但是更高效。因为避免了为每个主键创建列表的步骤。

除了从单个RDD对数据分组外,我们还可以用cogroup()对多个RDD有共同的主键的数据进行分组。cogroup()涉及两个共享相同类型主键的RDD,对应的值类型分别是VW,返回RDD[(K, (Iterable[V], Iterable[W]))]。如果某个主键在一个RDD中的没有而在另一个RDD中存在,则对应的迭代器就仅是个空。cogroup()给我们对多个RDD进行分组的能力。

 

cogroup()是我们下一节要讲到的连接操作的基本组件。

 

cogroup()可不止是用于实现连接。我们还可以用它来根据主键实现交集。另外,cogroup()还可以一次性对3个或更多的RDD进行处理。

 

连接

我们得到了有主键的数据后,一个最有用的操作就是可以和其他有主键的数据一起使用。对pair RDD来说,连接数据到一起恐怕是最常见的操作。Spark支持全系列的连接操作,包括右外连接,左外连接,交叉连接和内连接。

 

最简单的连接是内连接1。只有在两个pair RDD中都存在的主键才会输出。当某个输入的数据的同一个主键有多个值时,结果RDD会包含两个RDD按主键配对的所有可能的条目。看看示例4-17来简单理解一下。

 

示例4-17. Scala shell中进行内连接
storeAddress = {
  (Store("Ritual"), "1026 Valencia St"), (Store("Philz"), "748 Van Ness Ave"),
  (Store("Philz"), "3101 24th St"), (Store("Starbucks"), "Seattle")}
storeRating = {
  (Store("Ritual"), 4.9), (Store("Philz"), 4.8))}
storeAddress.join(storeRating) == {
  (Store("Ritual"), ("1026 Valencia St", 4.9)),
  (Store("Philz"), ("748 Van Ness Ave", 4.8)),
  (Store("Philz"), ("3101 24th St", 4.8))}

 

1 “连接”是一个数据库术语,表示从两个表中使用共同的值来合并字段。

有时我们不需要两个RDD的连接的结果中都有同一个主键。例如,我们想连接用户信息和推荐信息。即使没有任何推荐信息,我们也不希望丢掉客户信息。leftOuterJoin(other)rightOuterJoin(other)都能按主键对两个pair RDD进行连接,并且其中一个可以没有对应另一个的主键。

 

对于leftOuterJoin(),结果RDD中的每个主键都在源RDD中存在。而每个主键关联的值是源RDD中的值和另一个pair RDD中的Option(Java中是Optional)类型值的二元组。在Python中如果值不存在就使用None,如果存在,则就是用该值,不必封装。和join()一样,对于每个主键可以有多个记录,此时我们得到的是两个列表的笛卡尔积。

OptionalGoogleGuava库的一部分,表示一个可能丢失的值。我们可以通过isPresent()检查是否存在,用get()来获取提供了值的数据。

 

rightOuterJoin()几乎和leftOuterJoin()一样,除了主键必须存在于另一个RDD中,而可选值是来自源RDD而不是另一个RDD

 

重新看一下示例4-17,对我们用来做join()的两个pair RDD进行leftOuterJoin()rightOuterJoin()看看,见示例4-18

 

示例4-18. leftOuterJoin() rightOuterJoin()
storeAddress.leftOuterJoin(storeRating) ==
    {(Store("Ritual"),("1026 Valencia St",Some(4.9))),
    (Store("Starbucks"),("Seattle",None)),
    (Store("Philz"),("748 Van Ness Ave",Some(4.8))),
    (Store("Philz"),("3101 24th St",Some(4.8)))}
storeAddress.rightOuterJoin(storeRating) ==
    {(Store("Ritual"),(Some("1026 Valencia St"),4.9)),
    (Store("Philz"),(Some("748 Van Ness Ave"),4.8)),
    (Store("Philz"), (Some("3101 24th St"),4.8))}

数据排序

有序的数据在许多情况下很有用,特别是当你要生成下行输出时。我们可以对提供了键值对并且定义了主键顺序的RDD进行排序。一旦排序好了,对有序数据接下来调用collect()或者save()的结果都是有序的。

由于我们经常会想对RDD逆序,所以sortByKey()函数有个ascending参数来指示是否升序(默认是true)。有时我们会想以一种完全不同的顺序排序,为此我们可以提供我们自己的比较函数。示例4-194-21中,我们将RDD中的数字转换成字符串,使用字符串函数进行比较。

 

示例4-19. Python中使用自定义排序,将整数当成字符串排序
rdd.sortByKey(ascending=True, numPartitions=None, keyfunc = lambda x: str(x))


示例4-20. Scala中使用自定义排序,将整数当成字符串排序
val input: RDD[(Int, Venue)] = ...
implicit val sortIntegersByString = new Ordering[Int] {
    override def compare(a: Int, b: Int) = a.toString.compare(b.toString)
}
rdd.sortByKey()


示例4-21. Java中使用自定义排序,将整数当成字符串排序
class IntegerComparator implements Comparator<Integer> {
    public int compare(Integer a, Integer b) {
        return String.valueOf(a).compareTo(String.valueOf(b))
    }
}
rdd.sortByKey(comp)

Pair RDD上可用的动作

和变换一样,基本RDD的所有传统动作都适用于pair RDD。而pair RDD有些额外的动作,用到了数据的键值对的特性,见表4-3

 

表格 4-3 Pair RDD的动作(示例:({(1,2), (3,4), (3,6)})

函数名

目的

示例

结果

countByKey()

计算每个主键的元素个数

rdd.countByKey()

{(1, 1), (3, 2)}

collectAsMap()

收集数据为map结构方便查找

rdd.collectAsMap()

Map{(1, 2), (3, 4),  (3, 6)}

lookup(key)

返回关联key的所有值

rdd.lookup(3)

[4, 6]

 

保存pair RDD的动作也有多个,在第五章中会描述。


0 0