RDD编程指引(二)(部分翻译Spark官网文章RDD Programming Guide)

本人刚开始入门学习Spark,打算先将Spark文档看一遍,顺便做点笔记,就进行一些翻译和记录。由于本人只会python,所以翻译都是以python部分代码进行。以下并非完全100%官网翻译,更多是个人理解+笔记+部分个人认为重要的内容的翻译,新手作品,请各位大神多多指正。
官网原文链接:http://spark.apache.org/docs/latest/rdd-programming-guide.html

RDD Operations

RDD支持两种类型操作:

1、转换(transformations),重新生成一个新的数据集

2、处理动作(actions),返回一个或者一些值

转换总是惰性的,并不会立即计算,直到处理动作需要其计算结果的时候才会计算。这样使得spark的效率比较高,例如,map生成的大数据集,在经过reduce处理后,只返回一个或者一些值,而不会返回整个庞大的map后生成的大数据集。

转换在每次运行处理动作的时候都会进行重新计算,但是也可以通过persist或者cache办法,让spark将其保存在内存中以便下次需要的时候快速获取。也可以将结果保存到磁盘或者多个节点中。

Basics

lines = sc.textFile("data.txt")
lineLengths = lines.map(lambda s: len(s))
totalLength = lineLengths.reduce(lambda a, b: a + b)

第一行定义了从外部文件创建一个RDD.这个文件不会马上加载到内存中,lines只是data.txt的指针。第二行定义了map转换。由于转换的惰性,lineLengths不会马上计算,直到运行reduce这个处理动作,spark会将计算分割成任务在多个机器运行,每个机器运行自己负责的map和reduce任务后,将结果返回给驱动程序。

如果需要在接下来继续使用lineLengths中,可以运行

lineLengths.persist()

Passing Functions to Spark

spark重度依赖传递给驱动程序的函数,函数定义有三种推荐方式:

1、lambda表达式,适用于单行简单函数(其不支持多行函数以及无返回值函数)

2、本地def定义

3、import module

"""MyScript.py"""
if __name__ == "__main__":
    def myFunc(s):
        words = s.split(" ")
        return len(words)

    sc = SparkContext(...)
    sc.textFile("file.txt").map(myFunc)

也可以创建一个类,将map放入其中一个方法中,届时将整个实例传递给集群

class MyClass(object):
    def func(self, s):
        return s
    def doStuff(self, rdd):
        return rdd.map(self.func)

以下map对self.file的处理会影响到整个类

class MyClass(object):
    def __init__(self):
        self.field = "Hello"
    def doStuff(self, rdd):
        return rdd.map(lambda s: self.field + s)

最简单的处理办法是将self.field赋值给函数变量

def doStuff(self, rdd):
    field = self.field
    return rdd.map(lambda s: field + s)

Understanding closures

明白变量的范围和生命周期是理解spark的一大挑战。RDD操作可以修改超越其范围的变量常常引起很多人理解上的困惑。

Example
counter = 0
rdd = sc.parallelize(data)

# Wrong: Don't do this!!
def increment_counter(x):
    global counter
    counter += x
rdd.foreach(increment_counter)

print("Counter value: ", counter)

上述程序可能无法得到期望的结果(将rdd数值累加)。RDD将被分割为不同任务,每个任务由不同executor执行。而任务的闭包(closure)的计算先于实际运算执行。闭包指的是一些有自己作用域的变量和方法,它们必须被executor可见,这样才能在RDD中实现运算。而闭包都是通过序列化后传送到各个executor的,所以counter被序列化传送到executor后,被executor引用的counter并非驱动节点上的那个counter,虽然驱动节点上的counter依然在内存中,但对于其他executor并不可见。executors只能看见序列化后传输来的counter,因此,counter最终值是0,因为所有对counter的操作引用的counter都是来自于序列化的闭包。

Local vs. cluster modes

在本地模式中,某些情况下如果foreach函数在同一个jvm中执行,引用的是同一个counter,那么上述函数有可能能够达到预期效果。(实际本地测试,结果依然为0)

上述场景中最好使用累加器(Accumulators)。累加器提供了一种在分布式worker工作中安全地修改变量的值的机制。

总的来说,不要尝试在闭包体例如循环或者本地作用域中对全局状态进行修改,spark无法保证能够正确修改闭包体以外的对象。即使本地模式下,某些代码能够达到预期效果,但那是属于“意外”事件,在集群模式下就行不通了。如果确实要做全局汇聚,建议采用累加器(Accumulators)来实现。

Printing elements of an RDD

本地模式可以用rdd.foreach(println) 或者 rdd.map(println)等方式来输出RDD所有元素,但在集群模式下,输出被写入到executor的stdout中,所以在驱动节点查看stdout是不会有任何输出的。可以用collect()让驱动节点收集所有executors的结果,例如
rdd.collect().foreach(println),但这样可能会消耗较多驱动节点的内存,因为其尝试将所有executors的结果汇集到一台机器上。所以,比较安全的办法是用take收集部分结果,例如rdd.take(100).foreach(println)

Working with Key-Value Pairs

大部分的操作对RDD均可有效,但部分操作是key-pairs类型RDD专属的。最常见的是通过key来分组,聚合元素的shuffle操作。
在Python中,包含了元组的RDD可以应用上述操作。

例子是通过reduceByKey来计算文件有多少行

lines = sc.textFile("data.txt")
pairs = lines.map(lambda s: (s, 1))
counts = pairs.reduceByKey(lambda a, b: a + b)

可以用 counts.sortByKey()来让元素按照字母进行排序,最后通过counts.collect()来取回到驱动节点来作为一组对象呈现。

Transformations

一些常用的转换函数:

转换类型用法说明
map(func)将函数func作用在每个元素上,并返回一个新的数据集
filter(func)将函数func作用在每个元素上,只返回由func判断后返回ture的那些元素组成的数据集
flatMap(func)类似于map,但每个元素可以映射到0-N个(所以函数应该尝试返回一个序列而非单个值)
mapPartitions(func)类似于map,但作用在partitions上,函数类型需要为 Iterator => Iterator
mapPartitionsWithIndex(func)类似于mapPartitions,但增加了一个int来标识partitions,函数类型为(Int, Iterator) => Iterator
sample(withReplacement, fraction, seed)通过随机数量生成器种子,取样部分数据,可以替换成withReplacement部分,也可以不替换
union(otherDataset)汇聚源数据和目标数据,返回新的数据集,公共元素不会被丢弃
intersection(otherDataset)取源数据和目标数据的交集,返回该数据集
distinct([numPartitions]))取源数据对目标数据的差集,返回该数据集
groupByKey([numPartitions])作用在(K,V)类型的RDD上,返回 (K, Iterable) 类型的数据集。注意:如果是为了求和,可以用reducebykey或者aggregationbykey,
reduceByKey(func, [numPartitions])作用在(K,V)类型的RDD上,返回(K,V)类型的数据集,其中V是各个相同K的值的汇聚,func类型为 (V,V) => V
aggregateByKey(zeroValue,seqOp, combOp, [numPartitions])作用在(K,V)类型的RDD上,通过seqOp和combOp,利用zeroValue将K的值汇集起来。seqOp用于在同一个partition合并值,而combOp则用于在不同partition合并值
sortByKey([ascending], [numPartitions])作用在(K,V)类型的RDD上,根据K的值来进行升序或者降序排列元素,得到新的数据集
join(otherDataset, [numPartitions])连接(K,V)和(K,W)数据集为(K,(V,W)),支持外连接leftOuterJoin, rightOuterJoin,和 fullOuterJoin
cogroup(otherDataset, [numPartitions])作用在(K,V)和(K,W)数据集,返回(K, (Iterable, Iterable))元组,也被称为groupwith
cartesian(otherDataset)笛卡尔连接数据集T和U,返回(T,U)数据集
pipe(command, [envVars])将每个partition送至shell 命令中处理,通过stdin输入,stdout输出
coalesce(numPartitions)减少RDD中的partitions数量
repartition(numPartitions)重新将数据在均匀分布在更多/更少的分区中
repartitionAndSortWithinPartitions(partitioner)在指定的partitioner中进行重新分区,并在相应分区中进行排序,该转换比repartition后再排序更高效,因为其排序动作能够叠加到到shuffle操作中
Actions
转换类型用法说明
reduce(func)通过一个函数(接受两个参数,返回一个值)聚合数据集,函数应该具备无序性(commutative)和相关性(associative)。
collect()返回RDD数据集所有元素到驱动节点上,经常用于查看经过filter转换或者其他操作后返回的较小的数据集合。
count()返回数据集中元素个数。
first()返回数据集第一个元素。(作用类似于take(1)).
take(n)返回数据集前N个元素。
takeSample(withReplacement, num, [seed])返回N个随机样本数据,可以指定是否替换,可选择指定随机数生成器种子。
takeOrdered(n, [ordering])返回原始顺序或者按照自定义比较器处理后的前N个元素。
saveAsTextFile(path)将数据集保存在指定目录的text文件,可以保存在本地,HDFS或者其他hadoop支持的文件系统。spark将调用toString将每个元素转换为text中的一行。
saveAsSequenceFile(path)(Java and Scala) 将数据集转换为hadoop的序列文件,并保存在指定目录,可以保存在本地,HDFS或者其他hadoop支持的文件系统。该action针对的是实现hadoop writable接口的key-value类型的RDD。在Scala,对于隐式可转换为writable的类型也适用。
saveAsObjectFile(path)(Java and Scala) 用Java序列化将数据集转换为可以用SparkContext.objectFile()加载的简单格式。
countByKey()仅仅适用于key-value类型的RDD,将(K,V)返回为hashmap(K,Int)。
foreach(func)将函数作用在数据集每个元素上。
Shuffle operations

某些在spark的操作会触发shuffle操作,shuffle是spark用来对数据进行重新排布的一种机制,因为牵涉到在executors之间拷贝数据,所以这是一种复杂和开销较大的操作。

Background

以reduceByKey为例,考虑shuffle操作的机制。reduceByKey操作将同一个key的值汇集到一个元组中并生成一个新的RDD,其操作难度在于并不是所有同一个key的元组都在同样一个分区或者同一节点上,所以必须将他们重新定位,以便可以顺利计算出最终结果。

数据一般来说并不会因为操作需要就分布在其所需要的位置。一个任务对应一个分区,为此,对于执行reduceByKey任务,spark必须从所有分区读取所有key的值,并将其汇聚到一起,计算每个key的最终结果。这就是shuffle操作。

尽管每个分区的元素在最新的shuffle操作后都会被确定下来,分区的顺序也可以被确定,但元素依然是无序的。如果需要对分区后的元素进行排序,可以用以下方法:

  • 运行mapPartitions后,通过.sorted排序
  • 通过repartitionAndSortWithinPartitions效率地进行重新分区时候排序
  • 通过sortBy生成一个排序的RDD

导致shuffle的操作包括repartition, coalesce等重分区操作,groupByKey 和reduceByKey 等’ByKey‘类相关操作(除了count以外),cogroup和join等Join类型操作。

Performance Impact

Shuffle是一个代价高昂的操作,因为其中牵涉到disk开销,数据序列化开销,网络开销。spark通过map任务来组织数据,并通过reduce任务来汇聚结果。术语shuffle来自于MapReduce,并非直接与Spark的map和reduce操作相关。

map任务的结果会保存在内存中,直到出现内存不足的情况,就会在当前partitions进行排序后写到一个单独文件中。reduce任务则会读取相关已进行排序的blocks。

某些shuffle操作会消耗掉大量的内存,因为它们必须将数据在转换前/后都必须保留在内存中。特别是reduceByKey和aggregateByKey会在map端创建上述数据,其他ByKey操作在reduce端生成。当内存无法满足数据创建需求的时候,spark会将数据分割一部分数据到磁盘,从而增加了额外的读取开销以及垃圾回收开销。

shuffle会在磁盘生成大量的临时文件。spark1.3版本中,这些文件会保存直至RDD不再需要,然后就会被进行垃圾回收处理。如果相关计算要重新计算,这些文件不会被重复创建。垃圾回收一般会很久才进行一次。这意味着长时间运行的spark可能会占用大量磁盘空间。可以通过spark.local.dir设定临时文件目录。

shuffle可以配置一系列参数,参考配置手册可以了解更多。

RDD Persistence

Spark可以将操作过程中在内存中的产生数据集保存或者缓存下来,这样可以被再次利用,使得接下来的处理动作更快。
可以使用persist() 或者 cache()方法实现RDD的持久化。第一次计算后,结果保留在节点内存中。任何分区的RDD丢失,都会自动调用最初的转换操作重新生成该丢失的RDD。

此外,每个持久化RDD可以设置多种保存的方式,例如可以保存在磁盘中,或者以序列化java对象保存在内存中。可以通过在persist()指定存储的级别(即存储的方式)。cache()方法是使用内存保存的快速办法,也是默认的存储级别。

存储方式说明
MEMORY_ONLY默认级别。在JVM中将RDD保存为java对象,如果保存RDD的内存不够,部分partition将无法被保存下来,需要的时候将会进行重新计算。
MEMORY_AND_DISKJVM中将RDD保存为java对象,如果保存RDD的内存不够,部分partition将会保存到磁盘中。
MEMORY_ONLY_SER (Java and Scala)在JVM中将RDD保存为序列化java对象,这会比上述的非序列化java对象更节省空间,但需要耗费额外的CPU资源。
MEMORY_AND_DISK_SER(Java and Scala) 与MEMORY_ONLY_SER类似, 如果保存RDD的内存不够,部分partition将会保存到磁盘中。
DISK_ONLY仅仅将RDD保存到磁盘中。
MEMORY_ONLY_2, MEMORY_AND_DISK_2, etc.字面意思与上述一致,但会将相同RDD保存到2个节点中。
OFF_HEAP(实验功能) 类似于MEMORY_ONLY_SER, 但将数据保存到堆外内存。

注意:在python,保存对象总是会通过pickle库进行序列化,支持的存储方式有MEMORY_ONLY, MEMORY_ONLY_2, MEMORY_AND_DISK, MEMORY_AND_DISK_2, DISK_ONLY, and DISK_ONLY_2.

Which Storage Level to Choose?

选择Spark的存储方式,需要衡量内存和CPU两类资源:
如果RDD在默认存储方式下工作良好,保持即可,这是CPU资源最高效的方式。

如果RDD较大内存资源紧张,可以尝试MEMORY_ONLY_SER并选用快速序列化库压缩保存的RDD所需的空间,如果行得通依然是一种较快的方式。(Java and Scala)

不要将RDD写入到磁盘中,除非处理RDD的函数对CPU资源消耗巨大,否则重新计算RDD可能并不会比从磁盘中重新读取这些RDD慢。

如果需要考虑快速故障恢复(例如用Spark处理一个web app的请求),可以使用提供冗余的存储方式。尽管所有的存储方式都可以通过重新计算来实现完整的故障恢复,但带有冗余的方式可以让服务不需要等待重新计算,保持连续。

Removing Data

spark会自动监控每个节点缓存的使用情况,并采用LRU算法自动清除旧的partitions。如果需要手动清理RDD,可以用RDD.unpersist()方法实现。

个人原创,欢迎转载

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值