1 概览
- 每个spark程序都有一个驱动程序运行在用户的main函数中,以及在集群中执行不同的并行操作。
- 第一个抽象概念:RDD是元素的集合。这个集合可以被分到集群中的不同机器中并行处理。
- RDD可以由hadoop支持的文件系统中的文件创建,或者是驱动程序中的scala集合。
- RDD可以被保存在内存中被并行操作有效服用。
- 第二个抽象概念:shared variables。
- 共享变量可以在task之间或者task与driver之间共享。
- 主要有两种类型,broadcast variables,缓存在所有节点的内存中。accumulators,用于累加场景,例如计算器和求和。
2 连接spark
- spark2.4.0可以在python2.7+或者python3.4+版本中运行,可以使用CPython插件,也可使用PyPy 2.3+
- python2.6在spark2.2.0不在支持
- 可以使用bin/spark-submit或者bin/pyspark在集群中提交spark应用程序。
- 如果想使用HDFS文件,需要构建pyspark和HDFS之间的连接。
- 需要在应用程序中引入spark类
from pyspark import SparkContext, SparkConf
- 在提交应用时,可以指定Python的版本
$ PYSPARK_PYTHON=python3.4 bin/pyspark
$ PYSPARK_PYTHON=/opt/pypy-2.5/bin/pypy bin/spark-submit examples/src/main/python/pi.py
3 初始化spark
- spark编程第一件事是创建SparkContext对象,用于告诉spark如何访问集群。
- 创建SparkContext对象前,需要创建SparkConf对象,该对象包含应用信息。
conf = SparkConf().setAppName(appName).setMaster(master)
sc = SparkContext(conf=conf)
- appName:在集群UI中显示的应用名称
- master:Spark/Mesos/YARN集群的URL或者local字符串
- 一般不会硬编码master参数,而是使用spark-submit方式提交应用。
4 使用Shell
- pyspark shell中,已经创建了一个特殊的SparkContext,变量名称为sc,无法再创建自己的SparkContext。
- –master:指定context的连接模式
$ ./bin/pyspark --master local[4]
- –py-files:增加python .zip .egg .py文件
$ ./bin/pyspark --master local[4] --py-files code.py
- 使用ipython
$ PYSPARK_DRIVER_PYTHON=ipython ./bin/pyspark
- 使用Jupyter notbook
$ PYSPARK_DRIVER_PYTHON=jupyter PYSPARK_DRIVER_PYTHON_OPTS=notebook ./bin/pyspark
5 RDDs
5.1 Parallelized Collections
- 创建方式
data = [1, 2, 3, 4, 5]
distData = sc.parallelize(data)
- distData是一个分布式数据集,可以被并行处理
distData.reduce(lambda a, b: a + b)
- 集群中的,每个节点都会去处理RDDs的一个分片,一般每CPU处理2-4个分片,Spark会自动划分,也可以手动配置每CPU处理的分片数量
sc.parallelize(data, 10)
5.2 外部Datasets
- Spark可以从任何Hadoop支持的外部存储源创建RDD
- 可以使用SparkContext’s textFile方法读取外部文件,该方法的参数为文件的URL
>>> distFile = sc.textFile("data.txt")
- distData是一个RDD,可以执行并行计算操作
distFile.map(lambda s: len(s)).reduce(lambda a, b: a + b)
- spark读取文件注意点:
- 如果使用本地文件,需要将本地文件放到所worker能访问的地方。例如将文件拷贝到所有worker节点,或者使用共享存储。
- 所有Spark基于文件输入的方法,包括textFile,支持运行在目录、压缩文件、通配符。
textFile("/my/directory"), textFile("/my/directory/*.txt"), ("/my/directory/*.gz")
- textFile方法也可以设置第二个参数来制定文件的分区,默认会为每个HDFS block创建1个分片,可以通过手工配置创建更多的分片,但是不能比HDFS block数量少。
- 其他从file读取文件方法
- SparkContext.wholeTextFiles: 用于从目录中获取很多小文件,然后返回(filename,content)对。
- RDD.saveAsPickleFile/SparkContext.pickleFile:使用python的pickle序列化将RDD以该格式存储。
5.2.1写文件:pyspark负责做python和Java类型转换
Writable Type | Python Type |
---|---|
Text | unicode str |
IntWritable | int |
FloatWritable | float |
DoubleWritable | float |
BooleanWritable | bool |
BytesWritable | bytearray |
NullWritable | None |
MapWritable | dict |
array类型需自定义类型转换器,读取时,默认的转换器会将用户自定义的ArrayWritable转换为Java的Object[ ],序列化为python元组。
5.2.2 存储或加载SequenceFiles
>>> rdd = sc.parallelize(range(1, 4)).map(lambda x: (x, "a" * x))
>>> rdd.saveAsSequenceFile("path/to/file")
>>> sorted(sc.sequenceFile("path/to/file").collect())
[(1, u'a'), (2, u'aa'), (3, u'aaa')]
5.2.3 存储或加载其他Hadoop I/O 格式
$ ./bin/pyspark --jars /path/to/elasticsearch-hadoop.jar
>>> conf = {"es.resource" : "index/type"} # assume Elasticsearch is running on localhost defaults
>>> rdd = sc.newAPIHadoopRDD("org.elasticsearch.hadoop.mr.EsInputFormat",
"org.apache.hadoop.io.NullWritable",
"org.elasticsearch.hadoop.mr.LinkedMapWritable",
conf=conf)
>>> rdd.first() # the result is a MapWritable that is converted to a Python dict
(u'Elasticsearch ID',
{u'field1': True,
u'field2': u'Some Text',
u'field3': 12345})
如果使用自定义序列化数据,例如从HBase加载数据,需要先在Java或者Scala侧将数据转换为 Pyrolite’的pickler可以处理的东西。
6 RDD操作
- RDD支持两类操作。transformations:从一个已经存在的dataset创建一个新的dataSet。actions:在dataset上进行运算后,返回一个值给驱动程序
- transformations操作不会立刻计算结果,只有当action操作需要返回给驱动程序一个计算结果的时候才会执行计算。
- 默认当执行action操作时,所有的transformations RDD会被重复计算。可以使用persist或者cache将RDD放在内存中。
6.1 基础使用
统计文件中字符数量
lines = sc.textFile("data.txt")
lineLengths = lines.map(lambda s: len(s))
totalLength = lineLengths.reduce(lambda a, b: a + b)
lineLengths.persist()
6.2 向Spark传递函数
- lambda 表达式
- 定义本地函数,将函数作为参数传入spark
- 在其他模块中定义函数
函数式编程
"""MyScript.py"""
if __name__ == "__main__":
def myFunc(s):
words = s.split(" ")
return len(words)
sc = SparkContext(...)
sc.textFile("file.txt").map(myFunc)
面向对象编程
class MyClass(object):
def func(self, s):
return s
def doStuff(self, rdd):
return rdd.map(self.func)
如果将变量定义在了对象之外,如下所示,会将整个对象发送给集群
class MyClass(object):
def __init__(self):
self.field = "Hello"
def doStuff(self, rdd):
return rdd.map(lambda s: self.field + s)
应该将传入spark的数值定义为本地变量,则只会讲变量的值发送给集群
def doStuff(self, rdd):
field = self.field
return rdd.map(lambda s: field + s)
6.3 理解闭包
理解变量和方法被传入到集群的里程碑和生命周期
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)
- 在集群模式下,counter在driver节点的内存中被序列化,然后发送到各工作节点上,执行spark程序时,工作节点的counter值会变化,但是不会传回到driver,最后driver中的值还是0
- 在单节点模式下,如果程序在同一个JVM中,counter会累加。
- 两种模式下表现不一致,如果有类似全局累加器的场景,需要用到Accumulator
- 想要查看RDD中的元素,应该使用rdd.collect().foreach(println)。:从集群中获取全量数据,take()从集群中获取制定数量数据rdd.take(100).foreach(println).
6.4 使用键值对
统计内同相同行的数量
lines = sc.textFile("data.txt")
pairs = lines.map(lambda s: (s, 1))
counts = pairs.reduceByKey(lambda a, b: a + b)
按字符顺序排序
lines = sc.textFile("data.txt")
pairs = lines.map(lambda s: (s, 1))
counts = pairs.sortByKey()
6.5 Transformations算子
Transformations | 含义 |
---|---|
map(func) | 将源RDD的每个元素通过func进行计算,返回一个新的RDD |
filter(func) | 将源RDD中的每个元素传入func,将func计算返回true的元素作为新的RDD |
flatMap(func) | 和Map类似,但是每个输入元素map处理后,会有0个或者多个输出,func函数的返回值是一个序列 |
mapPartitions(func) | 类似于map,但是以RDD的分片(block)为单位进行处理的,例如,处理元素为T的RDD时,func的输入输出形式为 Iterator<T> => Iterator<U> |
mapPartitionsWithIndex(func) | 和mapPartitions类似,但是可以将一个整型值作为源RDD分片的索引,例如处理元素为T的RDD时,func的输入输出形式为 (Int, Iterator<T>) => Iterator<U> |
sample(withReplacement, fraction, seed) | 使用给定的随机数发生器种子,以可选的替换值对数据的一小部分进行采样。 |
union(otherDataset) | 返回一个新的数据集,该数据集包含源RDD和参数中RDD元素的并集。 |
intersection(otherDataset) | 返回包含源RDD和参数中元素的交集的新RDD。 |
distinct([numPartitions])) | 返回一个包含源数据集的不同元素的新数据集。 |
groupByKey([numPartitions]) | 在(K , V)RDD上调用时,返回(K , Iterable< V >)对的数据集。注意:如果要在每个键上执行聚合(如求和或平均值),则使用reduceByKey或AggregateByKey将获得更好的性能。注意:默认情况下,输出的并行度取决于父RDD的分区数量。可以通过一个可选的numPartitions参数来设置不同数量的任务。 |
reduceByKey(func, [numPartitions]) | 在(K,V)RDD对上调用时,返回(K,V)对的数据集,使用给定的减少函数func聚合每个密钥的值,该函数必须是类型(V,V)= > V,通过可选的第二个参数可配置减少任务的个数。 |
aggregateByKey(zeroValue)(seqOp, combOp, [numPartitions]) | 举例 |
sortByKey([ascending], [numPartitions]) | 在一个Key可排序的的(K,V)RDD上调用时,根据传入的ascending参数(true为升序,false为降序),按K对源RDD中的参数进行排序,然后新的RDD |
join(otherDataset, [numPartitions]) | 在(K,V)和(K,W)RDD上调用时,对每个key做聚合,返回(K,(V,W))的RDD,还可以执行eftOuterJoin, rightOuterJoin, and fullOuterJoin三种join |
cogroup(otherDataset, [numPartitions]) | 在(K,V)和(K,W)RDD上调用时,返回一个元素为(K, (Iterable<V>, Iterable<W>)) 的元组的RDD |
cartesian(otherDataset) | 在类型为T和U的RDD上调用时,返回元素类型为(T,U)元组的RDD |
pipe(command, [envVars]) | 通过系统的stdin和和stdout源RDD和结果RDD输入和从目的RDD输出(以字符串的形式)可以传入bash或者perl命令 |
coalesce(numPartitions) | 将RDD的分区减少到制定数目,通常在使用filter将一个大的数据集精简之后使用 |
repartition(numPartitions) | 按指定的数量分区重新分区,可以增加也可以减少,通常所有数据都需要重新shuffle,占用较多带宽 |
repartitionAndSortWithinPartitions(partitioner) | 重新分区后,再在各分区内对各元素按键排序,由于在shuffle的时候就已经排序了,比repartition效率更高 |
6.6 Actions算子
Action | 意义 |
---|---|
reduce(func) | 使用func将源RDD中的元素聚合 |
collect() | 将RDD的所有元素都返回给驱动程序,通常用在filter或者别的返回比较小的子数据集的场景 |
count() | 返回RDD元素的个数 |
first() | 返回RDD的第一个元素 |
take(n) | 返回RDD的前n个元素 |
takeSample(withReplacement, num, [seed]) | 按随机采样的方式返回n个元素,可以设置替换值和随机数种子 |
saveAsTextFile(path) | 将RDD保存为文件,存在本地或者其他Hadoop支持的文件系统中 |
saveAsSequenceFile(path) (Java and Scala) | 将数据集的元素作为Hadoop SequenceFile写入本地文件系统,HDFS或任何其他Hadoop支持的文件系统中的给定路径中。 这可以在实现Hadoop的Writable接口的键值对的RDD上使用。 在Scala中,它也可以在可隐式转换为Writable的类型上使用(Spark包括基本类型的转换,如Int,Double,String等)。 |
saveAsObjectFile(path) (Java and Scala) | 使用Java 序列化进行格式化,可以使用SparkContext.objectFile()来加载 |
countByKey() | 在类型为(K,V)的RDD上调用,返回一个元素类型为(K,int)的hashmap,计算每个K的个数 |
foreach(func) | 在数据集的每个元素上运行函数func。 不适用于如更新累加器或与外部存储系统交互的场景。 |
RDD API中也支持部分异步场景,例如foreachAsync,不会造成进程堵塞
6.7 shuffle 操作
- 某些操作包括shuffle过程,shuffle过程是一种数据重分布机制,可以将RDD中的数据在不同分区中重新分组。
- shuffle通常会将数据在执行器和节点之间进行拷贝,比较复杂且开销很大
6.7.1 背景
- 以reduceByKey为例说明shuffle:
reduceByKey将源RDD(K,V)中的所有元素按Key进行分组,然后在组内执行func函数,将所有value合并,最后输出一个(K,V)RDD。由于源RDD中具有相同K的数据不一定在同一个分区上,需要进行shuffle操作,将分散的数据进行排序、重分布后在计算输出。 - shuffle的定义:spark在集群内执行一个all-to-all的操作,先读取到所有K的下各自的全部V,然后按K对每个K下的所有V的做计算操作。
- 由于RDD下的所有数据都拿到手了,因此会对RDD的数据做一次排序。
- 重分区: repartition、coalesce;byKey:groupByKey 、reduceByKey;join: cogroup 、join通常都会引起shuffle
6.7.2 性能
- shuffle会大量调用磁盘I/O,数据序列化,网络I/O。
- spark会生成一系列任务,map任务组织数据,reduce任务聚合数据。
- 来自单个map任务的结果数据会被包租在内存里面,在数据稳定后,他们会按目标分区进行排序,存储在单个文件里。reduce任务会读取已经排好序的块文件。
- shuffle会在磁盘上生成大量中间文件。通常由垃圾收集器在一定周期内自动清除。可以通过spark.local.dir指定零时文件的存储位置。
- shuffle性能调优参数
6.8 RDD persistence
- RDD persistence可以将RDD的数据在内存中持久化,从而做到数据复用,提升性能
- 使用persist()或者cache()函数标记RDD
- 在第一次action操作后,RDD会被缓存在内存中,这种缓存具有容错性,一旦掉电,内存中数据丢失,spark会自动按之前的transformations重新计算一遍。
- persist有不同的存储等级,persist在内存中、persist在磁盘中、创建序列化对象、创建两副本。
Storage Level | 含义 |
---|---|
MEMORY_ONLY | 将RDD以反序列化Java对象存储在JVM中。如果RDD不适合放在内存中,一些分区将不会被缓存,每次需要用的时候会被重新计算,为默认级别 |
MEMORY_AND_DISK | 将RDD以反序列化Java对象存储在JVM中,如果RDD不适合放在内存中,将这些RDD的分区放在磁盘中,当需要被用到的时候会被读取 |
MEMORY_ONLY_SER (Java and Scala) | 将RDD以序列化Java对象的方式存储在内存中,每个分区一个byte数组。比非序列化的存储方式空间利用率更高,但是会消耗更多CPU |
MEMORY_AND_DISK_SER (Java and Scala) | 和MEMORY_ONLY_SER类似,但是会将不适合放在内存中的分区存储在硬盘 |
DISK_ONLY | 将RDD分区只存储在硬盘上 |
MEMORY_ONLY_2, MEMORY_AND_DISK_2, etc | 和上面的级别一致,但是会缓存在集群中的两个节点 |
OFF_HEAP (experimental) | 类似MEMORY_ONLY_SER,但是是将数据存储在堆外内存 |
python会始终使用pickle库对RDD的分区进行序列化,所以不存选择是否序列化,只有如下级别MEMORY_ONLY, MEMORY_ONLY_2, MEMORY_AND_DISK, MEMORY_AND_DISK_2, DISK_ONLY, and DISK_ONLY_2
spark也会自动persist某些中间数据,用于避免节点shuffle失败后的重复计算,但是如果数据确实可能复用,仍然建议手工申明presist。
6.8.1 如何选择存储等级
- 如果数据适合存储在内存中,使用默认等级。
- 如果不是,使MEMORY_ONLY_SER,并使用效率最高的序列化库
- 除非类似filter一个很到数据量的数据集,尽量不要使用存储在磁盘中的模式
- 所有级别都有容错功能,但是副本模式可以让你不用重新计算,在这种场景下使用服务级别
6.8.2 删除数据
spark基于最近至少使用过的原则自动删除缓存数据,如果想要手动清除,使用RDD.unpersist()方法。
7 共享变量
- 执行spark操作时,spark会将驱动程序中涉及的变量拷贝到集群中的所有节点,但是当计算结束后,这些变量的值不会回传到驱动程序。
- spark基于两种常见场景提供共享变量:broadcast variables和accumulators
7.1 Broadcast Variables
- 广播变量会被缓存在所有节点的内存中,而不是在执行task的时候在去拷贝数据。
- spark会有一套高效的广播算法
- spark操作是由一系列的stage组成,stage被shuffle操作分开。数据在stage中的task中被使用。
- 广播变量会以序列化的格式缓存在各节点的内存中,然后在执行task前反序列化。
- 因此,广播变量的适用场景为:跨stage执行task的时候,需要用的相同的数据(广播变量),或者