更新,第三章完整版PDF可下载:Learning Spark 第三章 RDD编程 已翻译整理完毕,PDF可下载
接着慢慢写吧,续上一篇:Learning Spark - LIGHTNING-FAST DATA ANALYSIS 第三章 - (1)
动作
我们已经知道如何从各种变化创建RDD,但有时候我们想实际对数据做点什么。动作是第二种操作。它们是返回一个最终值给驱动程序或者写入外部存储系统的操作。动作迫使对调用的RDD的变换请求进行求值,因为需要实际产生输出。
继续前一章的日志的例子,我们想打印一些关于badlinesRDD的信息。为此,我们将使用count()和take()两个动作。count()将返回其中的记录数,而take()返回RDD中的一些元素,见例子3-15至3-17。
示例3-15:Python中用count进行错误计数
print “Input had” + badLinesRDD.count() + “ concerning lines”
print “Here are 10 examples:”
for line in badLinesRDD.take(10):
print line
示例3-16:Scala中用count进行错误计数
println(“Input had” + badLinesRDD.count() + “ concerning lines”)
println(“Here are 10 examples:”)
badLinesRDD.take(10).foreach(println)
示例3-17:Java中用count进行错误计数
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过滤到一个比较小的数据集并且想要在本地处理时,collect()会有用。记住,你使用collec()的话整个数据集必须适合单机的内存大小,所以collect()不适合对大数据集使用。
大多数情况下,RDD都不能collect()到驱动程序,因为RDD一般都太大。这样的话,通常是将所有数据输出到分布式存储系统中,如HDFS或S3等。你可以用saveAsTextFile(),saveAsSequenceFile()或者任何其他各种内置格式的动作来保存RDD的内容。我们会在第五章讨论数据输出的不同选择。
值得注意的是,每次我们调用一个新动作,整个RDD都必须“从头开始”计算。要避免低效,用户可以像44页提到的“持久化(缓存)”那样持久化中间结果。
延迟求值
如前所述,RDD的变换是延迟求值,这意味着Spark直到看到一个动作才会进行求值。这对新用户来说可能有点反直觉,而使用过函数语言如Haskell或LINQ-like之类数据处理框架的用户会熟悉一些。
延迟求值表示当我们对RDD调用变换时(比如map()),该操作不会立即执行。相反的,Spark内部记录元数据来指明该操作被请求。与其认为RDD包含了特殊的数据,不如认识RDD是由累积的数据变换如何计算的指令组成。加载数据到RDD也是一样的。所以我们调用sc.textFile()的时候数据直到有必要时才实际加载。如同变换一样,该操作(在这是指读取数据)可以发生多次。
尽管变换是延迟的,你可以在任何时候通过调用类似count()的动作来强制Spark执行。这是测试你的部分程序的一个简单方式。
Spark使用延迟求值机制,通过对一组操作一起执行来减少其不得不接管数据的次数。像Hadoop MapReduce这样的系统,开发人员经常不得不花大量的时间思考如何将操作分组一起执行来最小化MapReduce的次数。而在Spark中,编写单个复杂的map操作来替代有许多单个操作组成的操作链并没有明显的好处。因此,用户可以自由的组织他们的程序为更小的,更可控的操作。
传递函数到Spark
大多数的Spark的变换和一些动作都依赖于向Spark传入函数,这些函数被Spark用于计算数据。各个语言对于传递参数到Spark的机制有些细微的差异。
Python
在Python中,传递函数到Spark有三种方式。对于较短的函数,可以通过lambda表达式来传递,见示例3-2和3-18所示。或者,也可以用顶级函数或者局部定义的函数。
示例3-18:Python中传入函数
word = rdd.filter(lambda s: “error” in s)
def containsError(s):
return “errors” in s
word = rdd.filter(containsError)
当传递函数时,该函数包含要序列化的对象,那么有个问题要注意。当你传递的函数是一个对象的成员,或者包含了一个对象的字段的引用(比如self.field),Spark是发送整个对象到worker节点。这可能会比你需要的信息多得多(见示例3-19)。有时候这会导致你的程序出错,当你的类中包含了python不知道如何pickle的对象的话。
示例3-19:传入带字段引用的函数(别这么干!)
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"
return rdd.filter(self.isMatch)
def getMatchesMemberReference(self, rdd):
# Problem: references all of "self" in "self.query"
return rdd.filter(lambda x: self.query in x)
替代的做法是仅取出需要的字段保存到局部变量并传入,如示例3-20。
示例3-20:Python传入函数,没有字段引用
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。就像示例3-20那么处理,我们抽取需要的字段到局部变量来避免传递整个对象,见示例3-21。
示例3-21:Scala传入函数
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"
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_))
}
}
如果发生了NotSerializableException异常,通常是引用了不可序列化的类中的方法或字段。注意,传递顶级对象的局部可序列化的变量或函数总是安全的。
Java
在Java中,函数是实现了org.apache.spark.api.java包中的Spark函数接口的对象。基于函数的返回类型有些不同的接口。表3-1中列出了最基本的函数接口以及一些我们需要的返回类似key/value的特定数据类型的函数接口。
表格 3-1 标准Java函数接口
函数名 | 实现的方法 | 用法 |
Function<T, R> | R call(T) | 一个输入一个输出,用于map(),filter()之类的操作 |
Function2<T1, T2, R> | R call(T1, T2) | 两个输入一个输出,用于aggregate(),fold()之类的操作 |
FlatMapFunction<T, R> | Iterable<R> call(T) | 一个输入零个或多个输出,用于flagMap()之类的操作 |
我们可以在类内部定义匿名的内联函数类,见示例3-22,或者定义命名类,见示例3-23。
示例3-22:Java通过内部匿名类传递函数
RDD<String> errors = lines.filter(new Function<String, Boolean>() {
public Boolean call(String x) { return x.contains("error"); }
});
示例3-23:Java通过命名类传递函数
class ContainsError implements Function<String, Boolean>() {
public Boolean call(String x) { return x.contains("error"); }
}
RDD<String> errors = lines.filter(new ContainsError());
选择哪种风格是个人习惯。但是我们发现在组织大型程序的时候,顶级命名类通常更清晰。使用顶级命名类的另一个好处是你可以定义构造参数,如示例3-24。
示例3-24:Java带参数的函数类
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表达式,我们的搜索例子可以像3-25这样写。
示例3-25:Java8的lambda表达式传递函数
RDD<String> errors = lines.filter(s -> s.contains("error"));
如果你对使用Java8的lambda表达式有兴趣,可以看看Oracle的文档和Databricks的关于Spark如何使用lambda表达式的博客。
用匿名内部类或者lambda表达式都可以引用方法内部的final变量。所以,你可以传递这些变量,就像Python和Scala中的一样。