Learning Spark中文版--第三章--RDD编程(1)

   本章介绍了Spark用于数据处理的核心抽象概念,具有弹性的分布式数据集(RDD)。一个RDD仅仅是一个分布式的元素集合。在Spark中,所有工作都表示为创建新的RDDs、转换现有的RDD,或者调用RDD上的操作来计算结果。在底层,Spark自动将数据中包含的数据分发到你的集群中,并将你对它们执行的操作进行并行化。数据科学家和工程师都应该阅读这一章,因为RDD是Spark的核心概念。我们强烈建议你在这些例子中尝试一些
交互式shell(参见“Spark的Python和Scala shell的介绍”在第11页)。
此外,本章中的所有代码都可以在该书的GitHub库中找到

RDD Basics(基本RDD)

  在Spark中,一个RDD仅仅是一个不可变的分布式对象集合.每个RDD被切分成多个可以在不同集群节点上进行计算的分区(patition)。RDD可以包含Python,Java和Scala任何类型的对象,包括用户自定义的class文件.

>>> lines = sc.textFile("README.md")

  用户创建RDD的两种方法:通过加载外部数据集或者通过在运行的程序中分配一个对象集合。我们在样例3-1中已经见识到了这种方法,通过使用SparkContext.textFile()加载文本文件生成字符串RDD。

  一旦创建,RDD提供两种操作类型:transformation(转换)和action(开工)。Transformation会根据之前的RDD构造一个新的RDD。比如,一个常见转换是过滤匹配断言函数的数据。在3-2展示的文本文件例子中,我们通过filter创建了一个新的只保存那些包含Python单词的行的RDD.

>>> pythonLines = lines.filter(lambda line: "Python" in line)

  另一方面,Action是基于RDD计算结果的,要么将其返回给驱动程序(main函数或shell),要么将其保存到外部存储系统中(例如,HDFS).我们前面提到的一个action是first(),如示例3-3中演示的那样,他返回一个RDD中的第一个元素.

>>> pythonLines.first()
u'## Interactive Python Shell'

  Transformation和action的区别在于Spark计算RDD的方式。尽管你可以随时定义新的RDD,但Spark是以一种懒惰的方式计算他们--就是只在RDD第一次被action使用的时候进行计算。这种方法看起来很不常见,但在大数据处理中意义非凡。举个例子,思考一下Example3-2和3-3,我们定义了一个文本文件并且过滤掉不包含Python的每一行。如果在我们写lines = sc.textFile()时Spark就加载了文件的所有行,那么将会浪费大量的存储空间而且我们马上会过滤掉很多行。相反,一旦Spark洞览整个转换链,他可以计算需要作为结果的数据。实际上,对于first()action,Spark只扫描文件直到找到第一个匹配的行,而不会读取整个文件。

  最终,每当你在Spark的RDD上运行一个action,默认会重新计算。如果你想在多个action上重复使用一个RDD,RDD.persist()方法可以进行保存。我们可以让Spark在许多不同的地方持久化我们的数据,这些将在表3-6中讨论。在第一次计算之后,Spark会把RDD内容存储在内存中,并且可以在以后的action中复用他们。在硬盘上持久化RDD也是可以的。默认不持久化的行为看起来可能很不寻常,但是这对大数据意义重大:如果你不重新使用RDD,并且数据流通过Spark一次就计算出了结果,这样看来浪费存储空间没什么必要。
  实际上,你会经常使用persist()来加载数据的子集到内存中用来反复查询。举个例子,如果我们知道想要计算包含Python单词的README文本行中的多个结果,我们可以写个Example3-4中展示的脚本。

Example3-4.Persisting an RDD in memory
>>> pythonLines.persist
>>> pythonLines.count()
2
>>> pythonLines.first()
u'## Interactive Python Shell'

  总的来讲,每个Spark项目或者shell对话都包含以下步骤:

  • 1.从外部数据创建一些输入RDD
  • 2.通过使用transformation如filter()来转换RDD成为新的RDD
  • 3.使用persist()来持久化任何需要被重用的中间RDD
  • 4.启动actioncount()first()来启动并行计算,然后Spark会对这些计算进行优化和执行


    在本章剩下的部分,我们会详细讲解这些步骤的细节,并且会包含一些Spark中最常用的RDD操作。

Creating RDDs(创建RDD)

  Spark提供了创建RDD的两种方式:加载外部数据和在驱动程序中将集合并行化。

  最简单的创建RDD的方式是在程序中获取一个存在的集合并且将它传入SparkContext中的parallelize()方法,Example3-5到3-7都展示了这种方法。这种方法在你学习Spark时非常有用,因为你可以快速在shell上创建你的RDD并且在自创的RDD上演示一些操作。需要注意的是,在样本和测试之外,这种方式并没有大量使用,因为它需要你将整个数据集保存在一台机器的内存中。

Example 3-5. parallelize() method in Python
lines = sc.parallelize(["pandas", "i like pandas"])
Example 3-6. parallelize() method in Scala
val lines = sc.parallelize(List("pandas", "i like pandas"))
Example 3-7. parallelize() method in Java
JavaRDD<String> lines = sc.parallelize(Arrays.asList("pandas", "i like pandas"));

  更常用的创建RDD的方法是加载外部数据。加载外部数据在第五章中有详细介绍。我们早就看到了加载文本文件作为字符串RDD的方法,SparkContext.textFile(),Example3-8到3-10中展示了:

Example 3-8. textFile() method in Python
lines = sc.textFile("/path/to/README.md")
Example 3-9. textFile() method in Scala
val lines = sc.textFile("/path/to/README.md")
Example 3-10. textFile() method in Java
JavaRDD<String> lines = sc.textFile("/path/to/README.md");

RDD Operations (RDD 操作)

  我们之前讨论过,RDD支持两种操作:transformation(转换)和action(开工)。Transformation会返回一个新的RDD,如map()filter()。Action是返回一个结果给驱动程序或者把结果写入存储并且驱动一个计算的操作,如count()first()。Spark对待transformation和action很不同,所以理解你使用的操作是哪种类型非常重要。如果你曾对给定的函数是transformation或action感到迷惑,你可以看他们的返回类型:transformation返回RDD,actions返回其他数据类型。

Transformations (转换)

  Transformation(转换)是使用RDD时返回一个新RDD的操作。如在29页“Lazy Evaluation(惰性求值)”中讨论的,转换的RDD是延迟计算的,只有在action中使用他们的时候才进行真正的计算。许多transformation是element-wize(逐元素的),也就是说,他们一次处理一个元素,但是不是所有的transformation(转换)都这样。

  举个例子,假如我们有一个记录一定数量信息的日志文件,log.txt。我们想挑选出错误的信息。我们可以使用之前看到的filter()进行转换。下面是展示:

Example 3-11. filter() transformation in Python
inputRDD = sc.textFile("log.txt")
errorsRDD = inputRDD.filter(lambda x: "error" in x)


Example 3-12. filter() transformation in Scala
val inputRDD = sc.textFile("log.txt")
val errorsRDD = inputRDD.filter(line => line.contains("error"))


Example 3-13. filter() transformation in Java
JavaRDD<String> inputRDD = sc.textFile("log.txt");
JavaRDD<String> errorsRDD = inputRDD.filter(
    new Function<String, Boolean>() {
        public Boolean call(String x) { return x.contains("error"); }
    }
});

  注意,filter()不会改变现有的输入RDD。相反,他会返回一个指向全新RDD的指针。输入RDD仍然可以在程序后面的处理中重复使用,比如使用这个RDD搜索其他单词。例如,我们使用这个RDD搜索warning单词,然后使用另一个transformation(转换),union(),将包含error和warning的内容结合进行输出。下面有示例:

Example 3-14. union() transformation in Python
errorsRDD = inputRDD.filter(lambda x: "error" in x)
warningsRDD = inputRDD.filter(lambda x: "warning" in x)
badLinesRDD = errorsRDD.union(warningsRDD)

  union()方法和filter方法有些不同,它操作两个RDD。Transformation(转换)实际上可以对任意数量的输入RDD进行操作。

实际上Example3-14更好的完成方式是直接用filter()一次过滤包含error或warning的行。

  最后,当你使用transformation(转换)派生出新的RDD,Spark会跟踪不同的RDD之间的依赖关系集,称为依赖关系图(lineage graph)。使用依赖关系图的信息来计算每个需要的RDD并且可以恢复那些丢失的持久化RDD信息。下图就是Example3-14的依赖关系图image

Actions(开工)

  我们了解了怎么使用transformation来创建RDD,但是某些情况,我们想通过数据集合做一些事情。Action是第二种RDD操作。Action会返回一个最终的值给驱动程序或者将数据写入外部存储系统。Action强制要求对RDD所需要的transformation(转换)进行求值,因为action需要实际地生成输出。

  回到之前的日志例子,我们可能想要打印一些关于badLinesRDD的信息。为此,我们将使用两个action,count()用来返回总数,take(num)用来收集RDD中(参数)指定个数的数据(take(2)收集两个)。下面有示例:

Example 3-15. Python error count using actions
print "Input had " + badLinesRDD.count() + " concerning lines"
print "Here are 10 examples:"
for line in badLinesRDD.take(10):
    print line


Example 3-16. Scala error count using actions
println("Input had " + badLinesRDD.count() + " concerning lines")
println("Here are 10 examples:")
badLinesRDD.take(10).foreach(println)


Example 3-17. Java error count using actions
System.out.println("Input had " + badLinesRDD.count() + " concerning lines")
System.out.println("Here are 10 examples:")
for (String line: badLinesRDD.take(10)) {
    System.out.println(line);
}

  在这个例子中,我们在驱动程序使用take()来取得一些RDD中的元素。然后本地遍历他们并在驱动程序打印一些信息。RDD还有collect()方法来收集整个RDD。如果你的程序把RDD筛选到很小的范围,并且你希望本地处理它,collect()就很有用了。记住一点,collect()方法会使你的整个数据集放在一台机器上,所以不要在大数据集中使用collect()

  大多数情况下的RDD不能直接在驱动程序中被collect(),因为他们太大了。这些情况下,把数据写入分布式存储系统是比较常见的,如HDFS和亚马逊的S3。你可以使用saveAsTextFilesaveAsSequenceFile(),或者各式各样的内置格式的action来保存RDD的内容。我们会在第五章介绍导出数据的不同选择。

  需要注意的是,每次调用一个新的action,整个RDD必须从头开始计算。为了避免这种低效率操作,可以持久化中间结果,44页的“Persistence(Caching)”会详细介绍。

Lazy Evaluation (惰性求值)

  如你之前读到的,RDD上的transformation(转换)是惰性求值,意味着Spark除非看到一个action(开工),否则不会开始开工。对于新的使用者来说这可能有点违反直觉,但是对于那些使用过函数式语言(如Haskell)或LINQ之类的数据处理框架的用户会非常熟悉。

  惰性求值意味着当我们在RDD上调用一个transformation(转换)(如调用map()),这个操作不是立即执行。相反,Spark内部记录了元数据来标明这个操作被请求过。与其把RDD当做一个包含特定数据的集合,不如把他看做一个通过一系列transformation来计算数据的指令集合。惰性求值把RDD打造成了指令而不是集合,action按照顺序执行这些指令,得到最终的结果。把数据加载到RDD中和transformation的惰性求值是一样的。所以,你可能明白了,当我们调用sc.textFile(),数据没有真正加载直到必要的时候。与transformation(转换)一样,(在本例中,读取数据的操作)可以多次发生。

尽管transformation(转换)是惰性的,你可以强制Spark随时通过运行action(动作)去执行他们,如count()。这是很便捷的测试你程序一部分的方法。

  Spark使用惰性求值来减少通过把操作分组处理数据传递的次数。像Hadoop的MapReduce系统,开发者经常花费大量时间思考怎么把操作分组来最小化MapReduce的次数。在Spark中,把数据处理编写成一个复杂的map(Hadoop中的计算组件)不如写成多个简单操作并将其串联在一起。因此,用户可以自由地将他们的程序组织成更小,更易于管理的操作。(这段话挺不好理解的,大体解释一下,因为hadoop中,你写的map函数是类似加工站的东西,数据通过它必须受到它的逻辑处理,即时求值。有时候我们不想写太多map类,我们会把一个map写的非常复杂,并且这种复杂的map可能复用性比较差,所以我们会去思考如何把某些操作归纳到一个map提高他的复用性,然后把map进行分组,是一件很麻烦的事情,但是transformation(转换)特性是惰性求值,我们只表示出这个RDD会怎么操作数据,但是只有在action的时候才会真正处理数据,并且filter()之类的函数高度抽象,我们不用将RDD进行分组,只需要根据逻辑去组合map()、filter()这些提供好的函数,这样会大大减少了对操作进行分组的编码任务)

Passing Functions to Spark (把函数传递给Spark)

  大多数Spark的transformation(转换)和一些action(动作)依赖于使用函数来计算数据。每个核心语言都有一种稍微不同的机制来将函数传递给Spark。

Python

在Python中,我们有三种操作来将函数传递给Spark。对于短一些的函数,我们可以用lambda表达式,正如Example3-2和3-18中演示的那样。或者,我们可以传递顶级函数或局部定义的函数。

//Example 3-18. Passing functions in Python

word = rdd.filter(lambda s: "error" in s)
def containsError(s):
    return "error" in s
word = rdd.filter(containsError)

  在传递函数时有一个问题需要注意,函数中包含的对象会无意间序列化了。当你传递的函数是一个对象的成员或者包含对象中引用的字段(如self.field),Spark会将整个对象传递给工作节点,有可能比你需要的信息大很多。如果你的类(class)包含Python不知道如何pickle(python用来序列化的包)的对象,这可能导致你的程序运行失败。

示例:

Example 3-19. Passing a function with field references (don’t do this!)


class SearchFunctions(object):
    def __init__(self, query):
        self.query = query
    def isMatch(self, s):
        return self.query in s
    def getMatchesFunctionReference(self, rdd):
        # Problem: references all of "self" in "self.isMatch"
        #问题:在self.isMatch方法中引用了整个self对象(就是对象自身)
        return rdd.filter(self.isMatch)
    def getMatchesMemberReference(self, rdd):
        # Problem: references all of "self" in "self.query"
        #问题:在self.iquery方法中引用了整个self对象(就是对象自身)
        return rdd.filter(lambda x: self.query in x)

  相反,直接把你需要的字段从对象中提取到局部变量然后再将函数传递,如Example3-20。

Example 3-20. Python function passing without field references

class WordFunctions(object):
    ...
    def getMatchesNoReference(self, rdd):
        # Safe: extract only the field we need into a local variable
        #安全:把我们需要的字段导入了局部变量
        query = self.query
        return rdd.filter(lambda x: query in x)

Scala

  在scala中,我们可以将函数定义为内联函数,对方法的引用,或者静态函数,就像我们使用其他Scala函数API那样。还有一些注意事项,尽管我们需要传递的函数和数据引用是可序列化的(实现java的序列化接口)。而且,就像在Python中一样,传递一个对象的方法或字段会包含对整个对象的引用,尽管这不是很明显,因为我们不需要用self(python中类似java的this关键字)来编写这些引用。如Example3-20中的Python示例,我们可以将需要的字段导入局部变量中来避免传递整个对象。示例Example3-21:

Example 3-21. Scala function passing

class SearchFunctions(val query: String) {
    def isMatch(s: String): Boolean = {
        s.contains(query)
    }
    def getMatchesFunctionReference(rdd: RDD[String]): RDD[String] = {
        // Problem: "isMatch" means "this.isMatch", so we pass all of "this"
        //问题:isMatch意味着this.isMatch,所以我们传递了整个this
        rdd.map(isMatch)
    }
    def getMatchesFieldReference(rdd: RDD[String]): RDD[String] = {
        // Problem: "query" means "this.query", so we pass all of "this"
        //差不多意思。。不翻了
        rdd.map(x => x.split(query))
    }
    def getMatchesNoReference(rdd: RDD[String]): RDD[String] = {
        // Safe: extract just the field we need into a local variable
        //安全:把需要的字段导入了局部变量
        val query_ = this.query
        rdd.map(x => x.split(query_))
    }
}

  如果在Scala中产生了NotSerializableException,通常的原因就是引用了不可序列化的类中一个方法或者字段。注意传递的局部可序列化变量和函数如果是高层对象的成员就是安全的(top-level object,怎么解释呢- -,抓瞎。就是你直接在REPL中定义一个函数而不是类中定义,这个函数就是top-level函数,类似的,top-level object应该就是直接定义的类不是内部类之类的(不一定准确,仅供参考))。

Java

  在java中,函数是很明确地实现了Spark的org.apach.spark.api.java.function包的function接口。里面有很多不同的基于函数返回类型的接口。我们在表3-1中展示了大多数基本函数接口,43页“Java”中会详细介绍特别的返回数据类型如key、value形式其他函数接口。

Table 3-1.标准Java函数接口

函数名 实现方法 用法
Function<T,R> R call(T) 输入一个输入返回一个输出,类似map和filter操作
Function<T1,T2,R> R call(T1,T2) 输入两个输入返回一个输出,类似aggregate或fold
FlatMapFunction<T,R> Iterable<R> call(T) 输入一个输入返回0或多个输出,类似flatMap

  我们可以像匿名内部类一样定义我们自己的内联函数类(Example3-22)或创建一个实现类(Example3-23)

Example 3-22. Java function passing with anonymous inner class

RDD<String> errors = lines.filter(new Function<String, Boolean>() {
    public Boolean call(String x) { return x.contains("error"); }
});

Example 3-23. Java function passing with named class

class ContainsError implements Function<String, Boolean>() {
    public Boolean call(String x) { return x.contains("error"); }
}
RDD<String> errors = lines.filter(new ContainsError());

  选择哪种方式属于个人偏好,但是我们发现匿名内部类(顶级命名函数)这种方式在大型程序中更整洁。顶级函数的另一个优点就是你可以设置构造器参数,示例:

Example 3-24. Java function class with parameters
class Contains implements Function<String, Boolean>() {
    private String query;
    public Contains(String query) { this.query = query; }
    public Boolean call(String x) { return x.contains(query); }
}
RDD<String> errors = lines.filter(new Contains("error"));

  在Java8中,你可以使用lambda表达式简介地实现函数接口。由于编写本书时,Java8刚出没多久,所以我们的样例使用老版本的冗长的语法。但是,使用lambda表达式的话,我们的搜索例子会变成Example3-25那样:

Example 3-25. Java function passing with lambda expression in Java 8

RDD<String> errors = lines.filter(s -> s.contains("error"));

  如果你对Java8的lambda表达式,可以参考Oracle’s documen‐
tation(Oracle的文档) 和 the Databricks blog post on how to use lambdas with Spark(Databricks(Spark的公司)发布的如何在Spark中使用lambda的博客)。

匿名内部类和lambda表达式都可以引用方法中的任何final变量,所以你可以像在Python和Scala中一样将这些变量传递给Spark。

posted @ 2018-02-22 17:46  Tikko  阅读(785)  评论(0编辑  收藏  举报