RDD创建
RDD:弹性分布式数据集。
在 Spark 中,对数据的所有操作不外乎创建 RDD、转化已有 RDD 以及调用 RDD 操作进行求值。而在这一切背后,Spark 会自动将 RDD 中的数据分发到集群上,并将操作并行化执行。
可以使用两种方法创建 RDD:
- 读取一个外部数据集,
- val lines = sc.textFile("/path/to/README.md")
- 在驱动器程序里分发驱动器程序中的对象集合(比如 list 和 set)。
- val lines = sc.parallelize(List("pandas", "i like pandas"))
- 不常用,因为需要把整个数据集先放在一台机器的内存中。
RDD操作
创建完成后,RDD支持两种类型的操作:
- 转化操作(transformation):由一个RDD生成一个新的RDD,比如 map() 和 filter()。Spark 会使用谱系 图(lineage graph)来记录这些不同 RDD 之间的依赖关系。
• val inputRDD = sc.textFile("log.txt")
val errorsRDD = inputRDD.filter(line => line.contains("error"))
- 行动操作(action):对RDD计算出一个结果,并把结果返回到驱动器程序中或存到外部存储系统中,比如 count() 和 first()。
• # 使用行动操作对错误进行计数
println("Input had " + badLinesRDD.count() + " concerning lines")
println("Here are 10 examples:")
badLinesRDD.take(10).foreach(println)
区分一个函数是转化操作还是行动操作,可以看返回值类型:转化操作返回的是 RDD,而行动操作返回的是其他的数据类型。
区别两种操作的原因是,Spark是惰性计算RDD的,只有第一次在一个action中用到时,才会真正计算。默认情况下,Spark的RDD会在每次action时重新计算,需要重用同一个RDD,可以用RDD.persist()缓存。默认不进行持久化是为了节省存储空间。
总结,Spark的工作方式:
- 从外部数据创建出输入 RDD。
- 使用诸如 filter() 这样的转化操作对 RDD 进行转化,以定义新的 RDD。
- 告诉 Spark 对需要被重用的中间结果 RDD 执行 persist() 操作。
- 使用行动操作来触发一次并行计算,Spark 会对计算进行优化后再执行。
向Spark传递函数
在 Scala 中,我们可以把定义的内联函数、方法的引用或静态方法传递给 Spark,就像 Scala 的其他函数式 API 一样。
我们还要考虑其他一些细节,比如所传递的函数及其引用的数据需要是可序列化的(实现了 Java 的 Serializable 接口)。
除此以外,传递一个对象的方法或者字段时,会包含对整个对象的引用(可能很大)。我们可以把需要的字段放到一个局部变量中,来避免传递包含该字段的整个对象,
例子:
class SearchFunctions(val query: String) {
def isMatch(s: String): Boolean = {
s.contains(query)
}
def getMatchesFunctionReference(rdd: RDD[String]): RDD[String] = {
// 问题:"isMatch"表示"this.isMatch",因此我们要传递整个"this"
rdd.map(isMatch)
}
def getMatchesFieldReference(rdd: RDD[String]): RDD[String] = { // 问题:"query"表示"this.query",因此我们要传递整个"this" rdd.map(x => x.split(query))
}
def getMatchesNoReference(rdd: RDD[String]): RDD[String] = {
// 安全:只把我们需要的字段拿出来放入局部变量中 val query_ = this.query
rdd.map(x => x.split(query_))
}
}
如果在 Scala 中出现了 NotSerializableException,通常问题就在于我们传递了一个不可序列 化的类中的函数或字段。记住,传递局部可序列化变量或顶级对象中的函数始终是安全的。
常见的转化操作
- map():接收一个函数,把这个函数用于 RDD 中的每个元素,将函数的返回结果作为结果RDD 中对应元素的值。
- flatMap():对每个输入元素生成多个输出元素。不过返回的不是一个元素,而是一个返回值序列的迭代器。
- filter():接收一个函数,并将 RDD 中满足该函数的 元素放入新的 RDD 中返回。
- sample(withRe placement, fra ction, [seed]):对 RDD 采样,以及是否替换
尽管 RDD 本身不是严格意义上的集合,但它也支持许多数学上的集合操作,比如合并和相交操作。
RDD 中最常缺失的集合属性是元素的唯一性,因为常常有重复的元素。如果只 要唯一的元素,我们可以使用 RDD.distinct() 转化操作来生成一个只包含不同元素的新 RDD,不过开销很大。
常见的集合操作:
- distinct():去重,distinct操作的开销很大,因为它需要将所有数据通过网络进行 混洗(shuffle),以确保每个元素都只有一份。
- union(other):它会返回一个包含两个 RDD 中所有元素的 RDD。
- intersection(other) 方法,只返回两个 RDD 中都有的元素。性能比union要差很多,因为它需要去重。
- subtract(other) :移除。接收一个 RDD 作为参数,返回 一个由只存在于第一个 RDD 中而不存在于第二个 RDD 中的所有元素组成的 RDD。和 intersection() 一样,它也需要数据混洗。
- cartesian(other) :计算两个 RDD 的笛卡儿积。转化操作会返回所有可能的 (a, b) 对,其中 a 是源 RDD 中的元素,而 b 则来自另一个 RDD。求大规模 RDD 的笛卡儿积开销巨大。
常见的行动操作
- reduce():接收一个函数作为参数,这个函数要操作两个相同元素类型的 RDD 数据,并返回一个同样类型的新元素。
- fold():接收一个与 reduce() 接收的函数签名相同的函数,再加上一个 “初始值”来作为每个分区第一次调用时的结果。你所提供的初始值应当是你提供的操作 的单位元素;也就是说,使用你的函数对这个初始值进行多次计算不会改变结果(例如 +对应的 0,* 对应的 1,或拼接操作对应的空列表)。
- 注意:fold() 和 reduce() 都要求函数的返回值类型需要和我们所操作的 RDD 中的元素类型相同。例如,在计算平均值时,需要记录遍历过程中的计数以及元素的数量,这就需要我们返回一 个二元组。可以先对数据使用 map() 操作,来把元素转为该元素和 1 的二元组,也就是我 们所希望的返回类型。这样 reduce() 就可以以二元组的形式进行归约了。
- aggregate() :返回值类型可以与所操作的 RDD 类型不同。需要提供我们期待返回的类型的初始值。然后通过一个函数把 RDD 中的元素合并起来放入累加器。考虑到每个节点是在本地进行累加的,最终,还需要提供第二个函数来将累加器两两合并。# 计算 RDD 的平均值val result = input.aggregate((0, 0))(
(acc, value) => (acc._1 + value, acc._2 + 1),
(acc1, acc2) => (acc1._1 + acc2._1, acc1._2 + acc2._2))
val avg = result._1 / result._2.toDouble- collect():它会将整个 RDD 的内容返回,要求所有数据都必须能一同放入单台机器的内存中。一般在单元测试中使用。
- take(n) :返回 RDD 中的 n 个元素,并且尝试只访问尽量少的分区,因此该操作会得到一个不均衡的集合。需要注意的是,这些操作返回元素的顺序与你预期的可能不一样。
- top() :从 RDD 中获取前几个元素。top() 会使用数据 的默认顺序,但我们也可以提供自己的比较函数。
- takeSample(withReplacement, num, seed) :从数据中获取一个采样,并指定是否替换。
- foreach():对 RDD 中的每个元素进行操作,不把 RDD 发回本地,不把任何结果返回到驱动器程序中。
- count(): 用来返回元素的个数。
- countByValue():返回一个从各值到值对应的计数的映射表。
RDD类型转换
有些函数只能用于特定类型的 RDD,比如 mean() 和 variance() 只能用在数值 RDD 上, 而 join() 只能用在键值对 RDD 上。
在 Scala 中,将 RDD 转为有特定函数的 RDD(比如在 RDD[Double] 上进行数值操作)是 由隐式转换来自动处理的。我们需要加上 import org.apache.spark. SparkContext._ 来使用这些隐式转换。可以在 SparkContext 对象的 Scala 文档中查看所列出的隐式转换。这些隐式转换可以隐式地将一个 RDD 转为各种封装类,比如 DoubleRDDFunctions(数值数据的 RDD)和 PairRDDFunctions(键值对 RDD),这样我们就有了诸如 mean() 和variance() 之类的额外的函数。
持久化(缓存)
Spark RDD 是惰性求值的,而有时我们希望能多次使用同一个 RDD。
默认情况下 persist() 会把数据以序列化的形式缓存在 JVM 的堆空间中。
unpersist()方法可以手动把持久化的 RDD 从缓存中移除。
可以为 RDD 选择不同的持久化级别:
如有必要,可以通过在存储级别的末尾加上“_2”来把持久化数据存为两份
例子:
如果要缓存的数据太多,内存中放不下,Spark 会自动利用最近最少使用(LRU)的缓存策略把最老的分区从内存中移除。对于仅把数据存放在内存中的缓存级别,下一次要用到 已经被移除的分区时,这些分区就需要重新计算。
import org.apache.spark.storage.StorageLevel
val result = input.map(x => x * x)
# persist()调用本身不会触发强制求值。
result.persist(StorageLevel.DISK_ONLY)
println(result.count())
println(result.collect().mkString(","))
但是对于使用内存与磁盘的缓存级别的分区来说,被移除的分区都会写入磁盘。不论哪一种情况,都不必担心作业因为缓存太多数据而被打断。不过,缓存不必要的数据会导致有用的数据被移出内存,带来更多重算的时间开销。