Spark (Python版) 零基础学习笔记(五)—— Spark RDDs编程

来源:互联网 发布:android获取软件版本号 编辑:程序博客网 时间:2024/06/10 14:33

RDD基础概念

创建RDD
创建RDD的方法:
1.载入外部数据集
2.分布一个对象的集合

前边几次的笔记已经提到过多次了,因此,这里只列出几个注意事项:
1.利用sc.parallelize创建RDD一般只适用于在测试的时候使用,因为这需要我们将整个数据集放入一台机器的内存中。因此,除了我们学习使或者测试时,很少使用。
2.更通用的方法是从外部存储系统上加载数据创建RDD

Spark支持两种RDDs操作:
Transformations:从已有数据集创建一个新的数据集(注意:并不是改变现有的RDD,而是返回一个新的RDD的指针。)
Actions:在数据集上进行计算后,向驱动程序返回一个值或者将数据写入外部存储系统。
注意,transformations和actions是两个完全不同的概念,因为这两种操作中,Spark对RDDs进行计算的方式不同。Transformations是惰性的,也就是说当我们使用一个transformation操作后,这个transformation的动作不会立即执行,只有在我们需要进行一个action的时候才会执行。这在大数据中十分重要,使得Spark十分的高效!
到底这样的设计有什么用呢?举个例子,比如说,我们在一个很大的数据集中进行map和reduce的操作,我们只有需要执行reduce(是一个action)在驱动程序中得到最后的结果时,map(是一个transformation)这一操作才会执行。这样最后的结果,我们只会在驱动程序中得到一个,二不需要返回中间过程中执行map后的这个巨大的数据。
再比如,我们需要定义一个文本文件并过滤其中含有“Python”字符串的行。我们利用lines = sc.textFile(…),Spark并不会马上加载和存储这些文件,因为这样会十分浪费内存,因为我们实际需要的只是一小部分含有“Python”的语句。而只有当我们需要执行filter这个action时,Spark此时才会载入文件并进行filter,最终只会返回filter的结果。更有趣的时,比如我们需要使用first()这个action,Spark在扫描文件时,只要找到了第一行符合要求的文字,他就不会再对整个文件进行读取了。这也是为什么Spark会如此高效!
默认情况下,我们每次运行一个action时,每个transformed RDD都会被重新进算一次。但是,Spark允许我们使用persist(或者cache)方法在内存中存留RDD,Spark将其中的元素保存在集群上,这样我们在下次需要访问它时,就会进一步加快速度。除了在内存中保留RDD之外,也可以将RDD暂存在硬盘上。这对于大数据来说又是一个非常关键的特性!因为我们如果不再使用RDD,Spark可以通过数据流访问数据计算出结果,没有必要浪费存储空间。

注意:当我们利用transformations命令建立了新的RDDs时,Spark会追踪保存不同RDDs之间的依赖关系,称为lineage graph普系统。Spark可以利用这些信息在需要的时候计算每个RDD,如果一部分固有RDD丢失,Spark能够利用这些信息回复丢失的数据。这也是一种故障修复的机制。
注意:在学习给过程中,经常使用action中的collect()或者take()将结果返回给驱动程序。但是,collect()在使用的时候需要考虑数据集的大小是否适合一台单机的内存。通常情况才,在大的数据集上是不会使用collect()的。普遍的做法是将数据写入分布式存储系统,例如HDFS或者Amazon S3。 可以使用saveAsFile()、saveAsSequenceFile()或者其他的一些actions保存RDD的内容,

总结以下Spark程序和shell会话的工作流程:
1.从外部数据创建RDDs
2.利用transformations将他们转移,定义新的RDDs
3.使用persist()方法使Spark将中间结果的RDDs暂存,以便再次使用
4.部署actions进行并行计算

传递函数

在Python中,有三种方式可以将函数传递给Spark。
1.使用匿名函数lambda
2.利用def局部定义的函数
3.模块中的高级函数

>>> lineRDD.filter(lambda x: 'Python' in x).collect()  # 匿名函数['high-level APIs in Scala, Java, Python, and R, and an optimized engine that', '## Interactive Python Shell', 'Alternatively, if you prefer Python, you can use the Python shell:']>>> def pythonLine(x):...     return "Python" in x... >>> lineRDD.filter(pythonLine).collect()  # 局部定义的函数['high-level APIs in Scala, Java, Python, and R, and an optimized engine that', '## Interactive Python Shell', 'Alternatively, if you prefer Python, you can use the Python shell:']

需要注意的是,当传递函数的时候,如果函数是一个对象的成员,或者包含对象中字段的引用(比如self.field),Spark会将整个对象都传递到工作节点,这要会导致传递的信息远大于我们需要的信息。有时,如果你的类中含有Python无法pickle的对象,就会导致程序无法运行。
下面给出一个错误的例子:

class SearchFunctions(object):    def __init__(self, query):        self.query = query    def isMatch(self, s):        return self.query in s    def getMatchesFunctionReference(self, rdd):        return rdd.filter(self.isMatch)    def getMatchesMemberReference(self, rdd):        return rdd.filter(lambda x: self.query in x)

一个正确的例子:

class WordFunctions(object):    ...def getMatchesNoReference(self, rdd):    query = self.query  # 使用局部变量,避免传递整个对象    return rdd.filter(lambda x: query in x) 

理解闭包

在Spark中,一个难点就是当跨越集群执行工作节点时,需要正确理解变量和方法的作用域和生命周期。RDD的一些操作可以修改作用域之外的变量,很容易让人困惑。

下面结合一个例子进行理解。下面这个例子本意是要实现sum的功能,但是当命令不再相同的JVM中执行时,就会出现不一样的结果。

#这是一个错误的例子,不要这样写!!!!>>> data = list(range(10))>>> counter = 0>>> rdd = sc.parallelize(data)>>> def increment_counter(x):...     global counter...     counter += x... >>> rdd.foreach(increment_counter)>>> print("Counter value: ", counter)                                         Counter value:  0

本地(local)模式 vs. 集群(cluster)模式:
Spark的本地模式:–master = local[n]
发布到集群上的Spark应用: 例如spark-submit to YARN
Spark会将RDD操作的过程分割成不同的tasks,每个task由一个executor执行。在执行之前,Spark会计算task的闭包。闭包就是executor在执行RDD上的计算时可见的变量和方法。闭包会被序列化并传递给每个executor。
闭包中传递给executor的每个变量会被复制,因此当上述程序中counter在每个foreach函数中被引用后,此时的counter就不再是驱动节点上的counter了。在驱动节点上仍然存在一个counter,但是它是对executors不可见的。Executors只能见到从序列化的闭包中复制的counter。因此,counter最后的值仍然为0。
在本地模式中,如果foreach函数确实是在一个和驱动节点相同的JVM中执行的,那么将会引用到同一个counter,并能够真的更新counter的值。
为了确保在上述情况下正确定义RDD的行为,可以使用一个accumulator。Spark中,accumulators能够提供一个机制,确保命令给分割在不同的工作节点上执行时,变量能够安全的更新。一般来讲,闭包的结构类似于循环(loop)和本地定义的方法,不能用于改变全局变量。

打印RDD的元素:
另一个常见的用法是希望打印RDD中的每个元素。如果使用rdd.foreach(print)或者rdd.map(print)进行打印,在一个计算机上执行时,能够产生我们希望的结果。然而,在集群模式下,被executor调用的标准输出会被executor自己的标准输出代替,和驱动节点上的标准输出不再相同。为了实现打印功能,可以使用collect()方法,现将RDD传递给驱动节点,然后才进行打印,也就是rdd.collect().foreach(print)。但这样做可能会导致内存溢出,因为需要将整个RDD放在一个机器上,如果只需要打印RDD中的部分元素,更为安全的方法是使用take()方法:rdd.take(100).foreach(print)

Transformation和Actions的详细讲解见前两次笔记,这里不再赘述:
http://blog.csdn.net/zhangyang10d/article/details/53146953
http://blog.csdn.net/zhangyang10d/article/details/53239404

Shuffle操作

Spark中的操作会触发一个事件,称为shuffle。Shuffle是spark中的一个用于数据再分布的机制,从而实现数据在不同的partitions之间重新分组。这一操作通常需要在不同的executors上复制数据,因此造成shuffle操作非常复杂和耗时。
应用背景:
为了理解shuffle的工作过程,我们可以结合reduceByKey进行学习。reduceByKey这一操作会将key值相同的值组合成一个tuple,生成一个新的RDD,一个key所有的value都会执行一个reduce函数。这个过程存在的一并不需要个难点和挑战是,对于一个key,它的所有values并不需要存放在同一个partition中,甚至不需要在同一个machines中,但是必须都能够被访问从而计算结果。
在Spark中,要进行一个具体操作时,一般要求数据不要分布在不同的partitions中。在计算过程中,一个task会在一个partition上执行,因此,为了执行reduceByKey这一单一的reduce操作,Spark需要执行一个all-to-all的操作。Spark会从所有partitions中读取所有键对应的数值,并把他们从不同的partitions中合并到一起,计算每个键最后的值,这一过程就叫做shuffle。
虽然经过shuffle后,partition中元素的集合会被唯一确定,而且partitions的顺序也会被唯一确定,但是其中的元素的顺序并不确定。如果我们希望知道经过shuffle后数据的顺序可以预知,则可以使用以下操作:
利用mapPartitions对每个partition进行排列,例如sorted
利用repartitionAndSortWithPartitions,在进行repartitioning的同时,高效的进行排列
利用sortBy,产生一个全局顺序的RDD
能够引发shuffle的操作包括repartition操作(例如repartition和coalesce),ByKey操作(例如groupByKey和reduceByKey,但是不包括counting),和join操作(例如cogroup和join)。

性能影响Performance Impact
Shuffle是一种非常昂贵的操作,因为它涉及到硬盘的I/O,数据序列化和network I/O。为了组织数据进行Shuffle,Spark需要产生一系列的tasks,包括map操作进行组织,一系列reduce操作进行聚集。

Shuffle操作会消耗很多堆内存空间,因为这一过程需要利用内存中的数据结果,从而在数据transfer之前或者之后进行记录。如果数据超出内存容量,Spark会将表格溢出到硬盘上,造成额外的硬盘I/O操作和垃圾收集。Shuffle还会在硬盘上生成大量的中间文件。在Spark 1.3中,这些文件会一直被保存,知道RDD不再使用并且垃圾已经被收集。垃圾收集只有在很长一段时间后才会发生,如果应用还需要访问这些RDDs,这意味着长时间运行的Spark工作需要消耗大量的磁盘空间。在配置Spark Context过程中,临时存储目录存储在spark.local.dir这一配置参数中。可以通过调整配置参数来调制shuffle行为。

RDD持久化(RDD Persistence)

Spark一个重要的功能是可以在进行不同操作过程中,在内存中持久化(persisting或者caching)数据。当持久化一个RDD后,每个节点都会将内存中计算得到的partitions进行存储,需要进行其他操作时再次使用。这样能够提升操作的速度。Caching是迭代算法的一个重要工具。
可以利用persist()或者cache()方法进行RDD的持久化操作。当RDD第一次执行action时,就会被保存在node中。Spark的cache方法可以容错,如果RDD的一个partition丢失了,它能够自动利用当初创建它的transformations重新计算。
此外,每个持久化的RDD能够被存储在不同的存储层,比如,循序我们在硬盘上暂存一个RDD,也可以在内存中暂存一个RDD,在其他节点复制RDD。这些不同的存储层次可以通过向persist()传递一个StorageLevel的对象进行设定。而cache方法只能够使用默认的存储层次,为StorageLevel.MEMORY_ONLY。存储层次分别有以下几种:

存储层次 含义 MEMORY_ONLY 将RDD以反序列化的Java对象存储在JVM中。如果RDD的大小与内存大小不匹配,一部分partitions将不会被缓存,而是在需要使用它们的时候重新进行计算。这是默认的存储层次。 MEMORY_AND_DISK 将RDD以反序列化的Java对象存储在JVM中。如果RDD的大小与内存大小不匹配,将超出内存容量的partitions存储在硬盘上,在需要使用它们时从硬盘上读取。 MEMORY_ONLY_SER(Java和Scala) 将RDD以序列化的Java对象存储(每个partition为1byte的数组)。这种形式相比于反序列化的存储方式更加节省空间,特别是在使用fast serializer时。但是需要占用更多的CPU进行读取。 MEMORY_AND_DISK_SER(Java和Scala) 和MEMORY_ONLY_SER相似,但是会将溢出内存的部分partitions存储在硬盘上,而不是在需要使用的时候重新计算。 DISK_ONLY 将RDD仅存储在硬盘上。 MEMORY_ONLY_2, MEMORY_AND_DISK_2, etc. 和上述的存储层次相同,但是会将每个partition复制在两个集群节点上。 OFF_HEAP(试验阶段) 和MEMORY_ONLY_SER相似,但是数据存储在外堆内存中。这需要系统对外堆存储的支持。

注意在python中,被存储的对象总会通过pickle library进行序列化,因此无论是否选择了序列化的存储层次都没有关系。在python中支持的存储层次包括MEMORY_ONLY, MEMORY_ONLY_2, MEMORY_AND_DISK, MEMORY_AND_DISK_2, DISK_ONLY, and DISK_ONLY_2。

及时用户没有调用persist(),Spark也会在Shuffle操作(例如reduceByKey)中自动缓存一些中间数据。这一机制可以确保shuffle过程中,在某个节点出现故障时无需对整个输入进行重新计算。

存储层次的选择依据:
Spark的不同存储层次的选择需要权衡内存占用和CPU的执行效率。可以根据以下步骤对存储层次进行选择:
1.如果RDD适合按照默认存储层次(MEMORY_ONLY)进行存储,则不做改变。这一方式的CPU执行效率最高。
2.如果不适合,尝试使用 MEMORY_ONLY_SER,并选择一个快速序列化的库(fast serialization library),从而更加节省存储空间,前提保证合理的读取速度。(适用于Java和Scala)
3.尽量不要使用硬盘,除非计算数据集的函数非常复杂,或者他们需要过滤大量的数据。否则,从新计算partitions的速度还是要比从硬盘读取RDD更快。
4.如果希望具备快速的故障恢复功能(比如需要使用Spark进行网页应用层序的请求服务),可以使用复制存储的方式。Spark的所有存储层次都具备容错功能,但是复制RDDs能够使我们无需等待重新计算丢失的partitions就可以继续在RDD上执行任务。

删除缓存出RDD
Spark能够自动监测内个节点上cache的使用状况,从而按照least-recently-used模式自动丢弃旧的数据分区。如果希望手动删除RDD,可以使用RDD.unpersist()方法。

0 0