《Spark快速大数据分析》笔记Ch3 RDD编程

来源:互联网 发布:长期出差 知乎 编辑:程序博客网 时间:2024/06/11 22:38

介绍Spark对数据的核心抽象——弹性分布式数据集。RDD其实就是分布式的元素集合。在Spark中,对数据的所有操作不外乎创建RDD、转化已有RDD以及调用RDD操作进行求值。Spark会自动将RDD中的数据分发到集群上,并将操作并行化执行。
1、RDD基础
Spark中的RDD就是一个不可变的分布式对象集合。每个RDD都被分为多个分区,这些分区运行在集群中的不同节点上。RDD可以包含Python、Java、Scala中任意类型的对象,甚至可以包含用户自定义的对象。
用户可以使用两种方法创建RDD:读取一个外部数据集,或在驱动器程序里分发驱动器程序中的对象集合(比如list和set)。
使用SparkContext,textFile()来读取文本文件作为一个字符串RDD的示例,Python:

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

创建出来后,RDD支持两种类型的操作:转化操作和行动操作。转化操作会由一个RDD生成一个新的RDD。例如,根据谓词匹配情况筛选数据就是一个常见的转化操作。

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

行动操作会对RDD计算出一个结果,并把结果返回到驱动器程序中,或把结果存储到外部存储系统中。
转化操作和行动操作的区别在于Spark计算RDD的方式不同。虽然你可以在任何时候定义新的 RDD, 但 Spark 只会惰性计算这些 RDD。它们只有第一次在一个行动操作中用到时,才会真正计算。一旦 Spark 了解了完整的转化操作链之后,它就可以只计算求结果时真正需要的数据。
最后,默认情况下,Spark的RDD会在你每次对它们进行行动操作时重新计算。如果想在多个行动操作中重用同一个RDD,可以使用 RDD.persist()让Spark把这个RDD缓存下来。默认不进行持久化。
总的来说,每个 Spark 程序或 shell 会话都按如下方式工作。
(1) 从外部数据创建出输入 RDD。
(2) 使用诸如 filter() 这样的转化操作对 RDD 进行转化,以定义新的 RDD。
(3) 告诉 Spark 对需要被重用的中间结果 RDD 执行 persist() 操作。
(4) 使用行动操作( 例如 count() 和 first() 等)来触发一次并行计算, Spark 会对计算进行优化后再执行。
2、创建RDD
Spark 提供了两种创建 RDD 的方式:读取外部数据集,以及在驱动器程序中对一个集合进行并行化。
创建 RDD 最简单的方式就是把程序中一个已有的集合传给 SparkContext 的 parallelize()方法。

lines = sc.parallelize(["pandas", "i like pandas"])

更常用的方式是从外部存储中读取数据来创建 RDD。将文本文件读入为一个存储字符串的 RDD 的方法SparkContext.textFile()。

lines = sc.textFile("/path/to/README.md")

3、RDD操作
RDD支持两种操作:转化操作和行动操作。RDD的转化操作是返回一个新的RDD的操作,比如map()和filter(),而行动操作则是向驱动器程序返回结果或把结果写入外部系统的操作,会触发实际的计算,比如count()和first()。转化操作返回的是RDD,而行动操作返回的是其他的数据类型。
3.1转化操作
RDD的转化操作是返回新RDD的操作。转化出来的RDD是惰性求值得,只有在行动操作中用到这些RDD时才会被计算。
举个例子,假定我们有一个日志文件log.txt,内含有若干消息,希望选出其中的错误消息。

inputRDD = sc.textFile("log.txt")errorsRDD = inputRDD.filter(lambda x: "error" in x)

filter() 操作不会改变已有的 inputRDD 中的数据。实际上,该操作会返回一个全新的 RDD。 inputRDD 在后面的程序中还可以继续使用,比如我们还可以从中搜索别的单词。
事实上,要再从 inputRDD 中找出所有包含单词 warning 的行。接下来,我们使用另一个转化操作 union() 来打印出包含 error 或 warning 的行数。

errorsRDD = inputRDD.filter(lambda x: "error" in x)warningsRDD = inputRDD.filter(lambda x: "warning" in x)badLinesRDD = errorsRDD.union(warningsRDD)

union() 与 filter() 的不同点在于它操作两个 RDD 而不是一个。转化操作可以操作任意数量的输入 RDD。通过转化操作,你从已有的 RDD 中派生出新的 RDD, Spark 会使用谱系图(lineage graph)来记录这些不同 RDD 之间的依赖关系。
这里写图片描述
3.2 行动操作
行动操作会把最终求得的结果返回到驱动器程序, 或者写入外部存储系统中。由于行动操作需要生成实际的输出,它们会强制执行那些求值必须用到的 RDD 的转化操作。count() 来返回计数结果,用 take() 来收集RDD 中的一些元素。
在驱动器程序中使用 take() 获取了 RDD 中的少量元素。 RDD 还有一个 collect() 函数,可以用来获取整个 RDD 中的数据。如果你的程序把 RDD 筛选到一个很小的规模,并且你想在本地处理这些数据时, 就可以使用它。记住,只有当你的整个数据集能在单台机器的内存中放得下时,才能使用 collect(),因此, collect() 不能用在大规模数据集上。
在大多数情况下, RDD 不能通过 collect() 收集到驱动器进程中,因为它们一般都很大。此时,我们通常要把数据写到诸如 HDFS 或 Amazon S3 这样的分布式的存储系统中。你可以使用 saveAsTextFile()、 saveAsSequenceFile(),或者任意的其他行动操作来把 RDD 的数据内容以各种自带的格式保存起来。
需要注意的是, 每当我们调用一个新的行动操作时,整个 RDD 都会从头开始计算。要避免这种低效的行为,用户可以将中间结果持久化。
3.3惰性求值
RDD 的转化操作都是惰性求值的。惰性求值意味着当我们对 RDD 调用转化操作(例如调用 map())时,操作不会立即执行。相反, Spark 会在内部记录下所要求执行的操作的相关信息。我们不应该把 RDD 看作存放着特定数据的数据集, 而最好把每个 RDD 当作我们通过转化操作构建出来的、记录如何计算数据的指令列表。 把数据读取到 RDD 的操作也同样是惰性的。因此,当我们调用sc.textFile() 时,数据并没有读取进来,而是在必要时才会读取。和转化操作一样的是,读取数据的操作也有可能会多次执行。
Spark 使用惰性求值,这样就可以把一些操作合并到一起来减少计算数据的步骤。在类似Hadoop MapReduce 的系统中,开发者常常花费大量时间考虑如何把操作组合到一起,以减少 MapReduce 的周期数。而在 Spark 中,写出一个非常复杂的映射并不见得能比使用很多简单的连续操作获得好很多的性能。 因此,用户可以用更小的操作来组织他们的程序,这样也使这些操作更容易管理。
4、向Spark传递函数
Spark 的大部分转化操作和一部分行动操作,都需要依赖用户传递的函数来计算。
4.1 Python
在 Python 中,我们有三种方式来把函数传递给 Spark。传递比较短的函数时,可以使用lambda 表达式来传递。除了 lambda 表达式,我们也可以传递顶层函数或是定义的局部函数。

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

传递函数时需要小心的一点是, Python 会在你不经意间把函数所在的对象也序列化传出去。当你传递的对象是某个对象的成员, 或者包含了对某个对象中一个字段的引用时(例如 self.field), Spark 就会把整个对象发到工作节点上,这可能比你想传递的东西大得多。有时,如果传递的类里面包含 Python 不知道如何序列化传输的对象,也会导致你的程序失败。
5、常见的转化操作和行动操作
包含特定数据类型的RDD还支持一些附加操作,例如,数字类型的RDD支持统计型函数操作,而键值对形式的RDD则支持诸如根据键聚合数据的键值对操作。
5.1 基本RDD
首先讲述受任意数据类型的RDD支持的转化操作和行动操作
1.针对各个元素的转化操作
map()和filter()。转化操作map()接收一个函数,把这个函数用于RDD的每个元素,将函数的返回结果作为结果RDD中对应元素的值。而转化操作filter()则接收一个函数,并将RDD中满足该函数的元素放入新的RDD中返回。
这里写图片描述
map() 的返回值类型不需要和输入类型一样。这样如果有一个字符串 RDD,并且我们的 map() 函数是用来把字符串解析并返回一个 Double 值的,那么此时我们的输入 RDD 类型就是 RDD[String],而输出类型是 RDD[Double]。用 map() 对 RDD 中的所有数求平方:

nums = sc.parallelize([1, 2, 3, 4])squared = nums.map(lambda x: x * x).collect()for num in squared:print "%i " % (num)

有时候,我们希望对每个输入元素生成多个输出元素。实现该功能的操作叫作 flatMap()。和 map() 类似,我们提供给 flatMap() 的函数被分别应用到了输入 RDD 的每个元素上。不过返回的不是一个元素, 而是一个返回值序列的迭代器。输出的 RDD 倒不是由迭代器组成的。我们得到的是一个包含各个迭代器可访问的所有元素的 RDD。 flatMap() 的一个简单用途是把输入的字符串切分为单词。

lines = sc.parallelize(["hello world", "hi"])words = lines.flatMap(lambda line: line.split(" "))words.first() # 返回"hello"

可以把 flatMap() 看作将返回的迭代器“拍扁”,这样就得到了一个由各列表中的元素组成的 RDD,而不是一个由列表组成的 RDD。
这里写图片描述
2、伪集合操作
RDD 本身不是严格意义上的集合,但它也支持许多数学上的集合操作,比如合并和相交操作。这些操作都要求操作的 RDD 是相同数据类型的。
我们的 RDD 中最常缺失的集合属性是元素的唯一性,因为常常有重复的元素。如果只要唯一的元素,我们可以使用 RDD.distinct() 转化操作来生成一个只包含不同元素的新RDD。不过distinct() 操作的开销很大,因为它需要将所有数据通过网络进行混洗(shuffle),以确保每个元素都只有一份。
这里写图片描述
也可以计算两个 RDD 的笛卡儿积。 cartesian(other) 转化操作会返回所有可能的 (a, b) 对,其中 a 是源 RDD 中的元素,而 b 则来自另一个 RDD。
3、行动操作
行动操作 reduce()接收一个函数作为参数,这个函数要操作两个 RDD 的元素类型的数据并返回一个同样类型的新元素。一个简单的例子就是函数 +,可以用它来对我们的 RDD 进行累加。使用 reduce(),可以很方便地计算出 RDD中所有元素的总和、元素的个数,以及其他类型的聚合操作。

sum = rdd.reduce(lambda x, y: x + y)

fold() 和 reduce() 类似,接收一个与 reduce() 接收的函数签名相同的函数,再加上一个“初始值”来作为每个分区第一次调用时的结果。 你所提供的初始值应当是你提供的操作的单位元素; 也就是说,使用你的函数对这个初始值进行多次计算不会改变结果(例如 +对应的 0, * 对应的 1,或拼接操作对应的空列表)。
fold() 和 reduce() 都要求函数的返回值类型需要和我们所操作的 RDD 中的元素类型相同。这很符合像 sum 这种操作的情况。但有时我们确实需要返回一个不同类型的值。例如,在计算平均值时, 需要记录遍历过程中的计数以及元素的数量,这就需要我们返回一个二元组。可以先对数据使用 map() 操作,来把元素转为该元素和 1 的二元组,也就是我们所希望的返回类型。这样 reduce() 就可以以二元组的形式进行归约了。
aggregate() 函数则把我们从返回值类型必须与所操作的 RDD 类型相同的限制中解放出来。与 fold() 类似,使用aggregate() 时,需要提供我们期待返回的类型的初始值。然后通过一个函数把 RDD 中的元素合并起来放入累加器。考虑到每个节点是在本地进行累加的,最终,还需要提供第二个函数来将累加器两两合并。
我们可以用 aggregate() 来计算 RDD 的平均值,来代替 map() 后面接 fold() 的方式。

sumCount = nums.aggregate((0, 0),(lambda acc, value: (acc[0] + value, acc[1] + 1),(lambda acc1, acc2: (acc1[0] + acc2[0], acc1[1] + acc2[1]))))return sumCount[0] / float(sumCount[1])

RDD 的一些行动操作会以普通集合或者值的形式将 RDD 的部分或全部数据返回驱动器程序中。把数据返回驱动器程序中最简单、 最常见的操作是 collect(),它会将整个 RDD 的内容返回。 collect() 通常在单元测试中使用,因为此时 RDD 的整个内容不会很大,可以放在内存中。使用 collect() 使得 RDD 的值与预期结果之间的对比变得很容易。由于需要将数据
复制到驱动器进程中, collect() 要求所有数据都必须能一同放入单台机器的内存中。
take(n) 返回 RDD 中的 n 个元素,并且尝试只访问尽量少的分区,因此该操作会得到一个不均衡的集合。需要注意的是,这些操作返回元素的顺序与你预期的可能不一样。
这些操作对于单元测试和快速调试都很有用,但是在处理大规模数据时会遇到瓶颈。如果为数据定义了顺序, 就可以使用 top() 从 RDD 中获取前几个元素。 top() 会使用数据的默认顺序,但我们也可以提供自己的比较函数,来提取前几个元素。有时需要在驱动器程序中对我们的数据进行采样。 takeSample(withReplacement, num,seed) 函数可以让我们从数据中获取一个采样,并指定是否替换。
有时我们会对 RDD 中的所有元素应用一个行动操作,但是不把任何结果返回到驱动器程序中,这也是有用的。比如可以用 JSON 格式把数据发送到一个网络服务器上,或者把数据存到数据库中。 不论哪种情况,都可以使用 foreach() 行动操作来对 RDD 中的每个元素进行操作,而不需要把 RDD 发回本地。
5.2 在不同RDD类型间转换
有些函数只能用于特定类型的 RDD,比如 mean() 和 variance() 只能用在数值 RDD 上,而 join() 只能用在键值对 RDD 上。 在 Scala 和 Java 中,这些函数都没有定义在标准的 RDD类中,所以要访问这些附加功能,必须要确保获得了正确的专用 RDD 类。在 Python 中,所有的函数都实现在基本的RDD 类中,但如果操作对应的 RDD 数据类型不正确,就会导致运行时错误。
6、持久化(缓存)
如前所述, Spark RDD 是惰性求值的,而有时我们希望能多次使用同一个 RDD。如果简单地对 RDD 调用行动操作, Spark 每次都会重算 RDD 以及它的所有依赖。这在迭代算法中消耗格外大,因为迭代算法常常会多次使用同一组数据。下面是由scala编写的先对 RDD 作一次计数、再把该 RDD 输出的一个小例子。

val result = input.map(x => x*x)println(result.count())println(result.collect().mkString(","))

为了避免多次计算同一个 RDD,可以让 Spark 对数据进行持久化。当我们让 Spark 持久化存储一个 RDD 时,计算出 RDD 的节点会分别保存它们所求出的分区数据。如果一个有持久化数据的节点发生故障, Spark 会在需要用到缓存的数据时重算丢失的数据分区。如果希望节点故障的情况不会拖累我们的执行速度,也可以把数据备份到多个节点上。
出于不同的目的,我们可以为 RDD 选择不同的持久化级别。在 Scala和 Java 中,默认情况下 persist() 会把数据以序列化的形式缓存在 JVM 的堆空间中。在 Python 中,我们会始终序列化要持久化存储的数据,所以持久化级别默认值就是
以序列化后的对象存储在 JVM 堆空间中。当我们把数据写到磁盘或者堆外存储上时,也总是使用序列化后的数据。
这里写图片描述
在 Scala 中使用 persist():

val result = input.map(x => x * x)result.persist(StorageLevel.DISK_ONLY)println(result.count())println(result.collect().mkString(","))

注意,我们在第一次对这个 RDD 调用行动操作前就调用了 persist() 方法。 persist() 调用本身不会触发强制求值。
如果要缓存的数据太多, 内存中放不下, Spark 会自动利用最近最少使用( LRU)的缓存策略把最老的分区从内存中移除。 对于仅把数据存放在内存中的缓存级别,下一次要用到已经被移除的分区时, 这些分区就需要重新计算。但是对于使用内存与磁盘的缓存级别的分区来说,被移除的分区都会写入磁盘。不论哪一种情况,都不必担心你的作业因为缓存
了太多数据而被打断。 不过,缓存不必要的数据会导致有用的数据被移出内存,带来更多重算的时间开销。
最后, RDD 还有一个方法叫作 unpersist(),调用该方法可以手动把持久化的 RDD 从缓存中移除。
7、总结
在本章中,介绍了 RDD 运行模型以及 RDD 的许多常见操作。在进行并行聚合、分组等操作时,常需要利用键值对形式的 RDD。 下一章会讲解键值对形式的 RDD 上一些相关的特殊操作。然后,讨论各种数据源的输入输出,以及一些关于使用 SparkContext 的进阶话题。

阅读全文
1 0