本人刚开始入门学习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_DISK | JVM中将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()方法实现。
个人原创,欢迎转载