赞
踩
Spark对数据的核心抽象——弹性分布式数据集(Resilient Distributed Dataset,简称RDD),RDD其实就是分布式的元素集合。RDD是一个不可变的分布式对象集合,所以,所有的transformation算子都是从一个RDD转换生成一个新的RDD。每个RDD都被分为多个分区,这些分区运行在集群中的不同节点上。
RDD支持两种类型的操作:转化操作(transformation) 和行动操作 (action)。转化操作会由一个RDD生成一个新的RDD,而行动操作会对RDD计算出一个结果。转化操作和行动操作的区别在于Spark计算RDD的方式不同,我们可以在任何时候定义新的RDD,但Spark只会惰性计算这些RDD。它们只有第一次在一个行动操作中用到时,才会真正计算。
默认情况下,Spark的RDD会在你每次对它们进行行动操作时重新计算。如果想在多个行动操作中重用同一个RDD,可以使用RDD.persist()让spark把这个RDD缓存下来,spark会把RDD的内容保存到内存中,这样在之后的行动操作中,就可以重用这些数据了。总体来说,每个spark程序都按如下方式工作:
spark提供了两种创建RDD的方式:读取外部数据集以及在驱动器程序中对一个集合进行并行化。创建RDD最简单的方式就是把程序中一个已有的集合传给SparkContext的parallelize()方法,如下:
lines = sc.parallelize(["pandas", "i like pandas"])
但更常用的方式是从外部存储中读取数据来创建RDD,比如将文本文件读入为一个存储字符串的RDD的方法SparkContext.textFile(),如下所示:
lines = sc.textFile("/path/to/README.md")
RDD支持两种操作:转化操作和行动操作。RDD的转化操作是返回一个新的RDD的操作,比如map()和filter(),而行动操作则是向驱动器程序返回结果或把结果写入外部系统的操作,会触发实际的计算,比如count()和first()。我们可以这么理解,spark做转化操作返回的是RDD,而行动操作返回的是其它的数据类型。
RDD的转化操作是返回新RDD的操作,转化出来的RDD是惰性求值的,只有在行动操作中用到这些RDD时才会被计算。如下展示的是对log.txt文件刷选出所有包含error和warning的行:
inputRDD = sc.textFile("log.txt")
errorRDD = inputRDD.filter(lambda x: "error" in x)
warningRDD = inputRDD.filter(lambda x: "warning" in x)
badLineRDD = errorRDD.union(warningRDD)
通过转化操作,从已有的RDD中派生出新的RDD,spark会使用谱系图(lineage graph) 来记录这些不同RDD之间的依赖关系。spark需要用这些信息来按需计算每个RDD,也可以依靠谱系图在持久化的RDD丢失部分数据时恢复所丢失的数据。如下图所示:
行动操作是第二种类型的RDD操作,它们会把最终求得的结果返回到驱动器程序或者写入外部存储系统中。需要注意的是,每当我们调用一个新的行动操作时,整个RDD都会从头开始计算。要避免这种低效的行为,用户可以将中间结果持久化。RDD常见的take(),collect()操作都属于行动操作。
RDD的转化操作都是惰性求值的。比如我们对RDD调用转化操作(例如调用map())时,操作不会立即执行。相反,spark会在内部记录下所要执行的操作的相关信息。我们不应该把RDD看作存放着特定数据的数据集,而最好把每个RDD当做我们通过转化操作构建出来的、记录如何计算数据的指令列表。把数据读取到RDD的操作也同样是惰性的,因此,当我们调用sc.textFile()时,数据并没有读取出来,而是在有action操作的时候才会读取出来。spark使用惰性求值,这样就可以把一些操作合并到一起来减少计算数据的步骤。
spark的大部分转化操作和一部分行动操作,都需要依赖用户传递的函数来计算。在python中,我们如下方式把函数传递给spark:
word = rdd.filter(lambda s: "error" in s)
def contrainError(s):
return "error" in s
word = rdd.filter(containError)
我们平时用到的最常用的转化操作是map()和filter()。转化操作map()接收一个函数,把这个函数用于RDD中的每个元素,将函数的返回结果作为结果RDD中对应元素的值。而转化操作filter()则接收一个函数,并将RDD中满足该函数的元素放入新的RDD中返回。
我们来看一个简单的例子,用map()对RDD中的所有数求平方:
nums = sc.parallelize([1,2,3,4])
squared = nums.map(lambda x: x *x).collect()
for num in squared:
print num
若我们希望对输入元素生成多个输出元素,用flatMap()函数,返回的是一个包含各个迭代器可访问的所有元素的RDD,flatMap()的一个简单用途是把输入的字符串切分为单词,如下所示:
lines = sc.parallelize(["hello word","hi"])
words = lines.flatMap(lambda line: line.split())
words.firt() # 返回“hello”
尽管RDD本身不是严格意义上的集合,但它支持许多数学上的集合操作,比如合并,相交操作等。
所以常见的转化操作如下:
函数名 | 目的 |
---|---|
map() | 将函数应用于RDD中的每个元素 |
flatMap() | 将函数应用于RDD中的每个元素,将返回的迭代器的所有内容构成新的RDD |
filter() | 过滤,组成新的RDD |
distinct() | 去重 |
sample(withReplacement, fraction, [seed]) | 对RDD采样,以及是否替换,rdd.sample(false,0.5) |
union() | 两个RDD求并 |
intersection() | 求两个RDD共同的元素 |
subtract() | 移除一个RDD中的内容 |
cartesian() | 与另外一个RDD的笛卡尔积 |
我们常见的行动操作reduce(),它接受一个函数作为参数,使用reduce(),可以很方便地计算出RDD中所有元素的总和,元素的个数,以及其他类型的聚合操作。如下计算RDD所有元素的总和:
sums = rdd.reduce(lambda x, y: x+y)
RDD的一些行动操作会以普通集合或者值的形式将RDD的部分或全部数据返回驱动器程序中。把数据返回驱动器程序中最简单,最常用的操作是collect(),它会将整个RDD的内容返回。由于需要将数据复制到驱动器进程中,collect()要求所有数据都必须能一同放入单台机器的内存中。如下总结了RDD的基本行动操作:
函数名 | 目的 | 实例 |
---|---|---|
collect() | 返回RDD的所有元素 | rdd.collect() |
count() | RDD中的元素个数 | rdd.count() |
countByValue() | 各元素在RDD中出现的次数 | rdd.contByValue() |
take(num) | 从RDD中返回num个元素 | rdd.take(2) |
top(num) | 从RDD中返回最前面的num个元素 | rdd.top(2) |
takeSample(withReplacement, num, [seed]) | 从RDD中返回任意一些元素 | rdd.takeSample(false,1) |
reduce(func) | 并行整合RDD中所有数据 | rdd.reduce((x,y)=> x+y) |
foreach(func) | 对RDD中的每个元素使用给定的函数 | rdd.foreach(func) |
Spark RDD 是惰性求值的,而有时我们希望能多次使用同一个RDD。如果简单地对RDD 调用行动操作,Spark 每次都会重算RDD 以及它的所有依赖。为了避免多次计算同一个RDD,可以让Spark 对数据进行持久化。在Python 中,我们会始终序列化要持久化存储的数据,所以持久化级别默认值就是以序列化后的对象存储在JVM 堆空间中。当我们把数据写到磁盘或者堆外存储上时,也总是使用序列化后的数据。
如果要缓存的数据太多,内存中放不下,Spark 会自动利用最近最少使用(LRU)的缓存策略把最老的分区从内存中移除。对于仅把数据存放在内存中的缓存级别,下一次要用到已经被移除的分区时,这些分区就需要重新计算。RDD 还有一个方法叫作unpersist(),调用该方法可以手动把持久化的RDD 从缓存中移除。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。