自开源之日至今,Spark已经5岁了。从最初不到4000行代码发展到当下通用大数据处理引擎的有力竞争者,Spark一直保持着小而紧凑,使许多开发人员更容易理解,也让升级起来更加方便。快、通用让Spark如鱼得水,然而对于1个年仅5岁的开源项目来说,其远谈不上尽善尽美,就比如文档相关。近日 @Cholerae从官网翻译了Spark编程指南Python版,并发布于其 个人博客。
以下为原文
译者说在前面:最近在学习Spark相关的知识,在网上没有找到比较详细的中文教程,只找到了官网的教程。出于自己学习同时也造福其他初学者的目的,把这篇指南翻译成了中文,笔者水平有限,文章中难免有许多谬误,请高手不吝赐教。
本文翻译自 Spark Programming Guide,由于笔者比较喜欢Python,在日常中使用也比较多,所以只翻译了Python部分,不过Java和Scala大同小异。
从高层次上来看,每一个Spark应用都包含一个驱动程序,用于执行用户的main函数以及在集群上运行各种并行操作。Spark提供的主要抽象是弹性分布式数据集(RDD),这是一个包含诸多元素、被划分到不同节点上进行并行处理的数据集合。RDD通过打开HDFS(或其他hadoop支持的文件系统)上的一个文件、在驱动程序中打开一个已有的Scala集合或由其他RDD转换操作得到。用户可以要求Spark将RDD持久化到内存中,这样就可以有效地在并行操作中复用。另外,在节点发生错误时RDD可以自动恢复。
Spark提供的另一个抽象是可以在并行操作中使用的共享变量。在默认情况下,当Spark将一个函数转化成许多任务在不同的节点上运行的时候,对于所有在函数中使用的变量,每一个任务都会得到一个副本。有时,某一个变量需要在任务之间或任务与驱动程序之间共享。Spark支持两种共享变量:广播变量,用来将一个值缓存到所有节点的内存中;累加器,只能用于累加,比如计数器和求和。
这篇指南将展示这些特性在Spark支持的语言中是如何使用的(本文只翻译了Python部分)。如果你打开了Spark的交互命令行——bin/spark-shell的Scala命令行或bin/pyspark的Python命令行都可以——那么这篇文章你学习起来将是很容易的。
Spark1.3.0只支持Python2.6或更高的版本(但不支持Python3)。它使用了标准的CPython解释器,所以诸如NumPy一类的C库也是可以使用的。
通过Spark目录下的bin/spark-submit脚本你可以在Python中运行Spark应用。这个脚本会载入Spark的Java/Scala库然后让你将应用提交到集群中。你可以执行bin/pyspark来打开Python的交互命令行。
如果你希望访问HDFS上的数据,你需要为你使用的HDFS版本建立一个PySpark连接。常见的HDFS版本标签都已经列在了这个第三方发行版页面。
最后,你需要将一些Spark的类import到你的程序中。加入如下这行:
在一个Spark程序中要做的第一件事就是创建一个SparkContext对象来告诉Spark如何连接一个集群。为了创建SparkContext,你首先需要创建一个SparkConf对象,这个对象会包含你的应用的一些相关信息。
appName参数是在集群UI上显示的你的应用的名称。master是一个Spark、Mesos或YARN集群的URL,如果你在本地运行那么这个参数应该是特殊的”local”字符串。在实际使用中,当你在集群中运行你的程序,你一般不会把master参数写死在代码中,而是通过用spark-submit运行程序来获得这个参数。但是,在本地测试以及单元测试时,你仍需要自行传入”local”来运行Spark程序。
在PySpark命令行中,一个特殊的集成在解释器里的SparkContext变量已经建立好了,变量名叫做sc。创建你自己的SparkContext不会起作用。你可以通过使用—master命令行参数来设置这个上下文连接的master主机,你也可以通过—py-files参数传递一个用逗号隔开的列表来将Python的.zip、.egg或.py文件添加到运行时路径中。你还可以通过—package参数传递一个用逗号隔开的maven列表来给这个命令行会话添加依赖(比如Spark的包)。任何额外的包含依赖包的仓库(比如SonaType)都可以通过传给—repositorys参数来添加进去。Spark包的所有Python依赖(列在这个包的requirements.txt文件中)在必要时都必须通过pip手动安装。
比如,使用四核来运行bin/pyspark应当输入这个命令:
想要了解命令行选项的完整信息请执行pyspark --help命令。在这些场景下,pyspark会触发一个更通用的spark-submit脚本
在IPython这个加强的Python解释器中运行PySpark也是可行的。PySpark可以在1.0.0或更高版本的IPython上运行。为了使用IPython,必须在运行bin/pyspark时将PYSPARK_DRIVER_PYTHON变量设置为ipython,就像这样:
你还可以通过设置PYSPARK_DRIVER_PYTHON_OPTS来自省定制ipython。比如,在运行IPython Notebook时开启PyLab图形支持应该使用这条命令:
Spark是以RDD概念为中心运行的。RDD是一个容错的、可以被并行操作的元素集合。创建一个RDD有两个方法:在你的驱动程序中并行化一个已经存在的集合;从外部存储系统中引用一个数据集,这个存储系统可以是一个共享文件系统,比如HDFS、HBase或任意提供了Hadoop输入格式的数据来源。
并行化集合是通过在驱动程序中一个现有的迭代器或集合上调用SparkContext的parallelize方法建立的。为了创建一个能够并行操作的分布数据集,集合中的元素都会被拷贝。比如,以下语句创建了一个包含1到5的并行化集合:
分布数据集(distData)被建立起来之后,就可以进行并行操作了。比如,我们可以调用disData.reduce(lambda a, b: a+b)
来对元素进行叠加。在后文中我们会描述分布数据集上支持的操作。
并行集合的一个重要参数是将数据集划分成分片的数量。对每一个分片,Spark会在集群中运行一个对应的任务。 典型情况下,集群中的每一个CPU将对应运行2-4个分片。一般情况下,Spark会根据当前集群的情况自行设定分片数量。但是,你也可以通过将第二个参 数传递给parallelize方法(比如sc.parallelize(data, 10))来手动确定分片数量。注意:有些代码中会使用切片(slice,分片的同义词)这个术语来保持向下兼容性。
PySpark可以通过Hadoop支持的外部数据源(包括本地文件系统、HDFS、 Cassandra、HBase、亚马逊S3等等)建立分布数据集。Spark支持文本文件、 序列文件以及其他任何 Hadoop输入格式文件。
通过文本文件创建RDD要使用SparkContext的textFile方法。这个方法会使用一个文件的URI(或本地文件路径,hdfs://、s3n://这样的URI等等)然后读入这个文件建立一个文本行的集合。以下是一个例子:
建立完成后distFile上就可以调用数据集操作了。比如,我们可以调用map和reduce操作来叠加所有文本行的长度,代码如下:
在Spark中读入文件时有几点要注意:
除了文本文件之外,Spark的Python API还支持多种其他数据格式:
可写类型支持
PySpark序列文件支持利用Java作为中介载入一个键值对RDD,将可写类型转化成Java的基本类型,然后使用 Pyrolite将java结果对象串行化。当将一个键值对RDD储存到一个序列文件中时PySpark将会运行上述过程的相反过程。首先将Python对象反串行化成Java对象,然后转化成可写类型。以下可写类型会自动转换:
| 可写类型 | Python类型 |
| ———————- | ————- |
| Text | unicode str|
| IntWritable | int |
| FloatWritable | float |
| DoubleWritable | float |
| BooleanWritable | bool |
| BytesWritable | bytearray |
| NullWritable | None |
| MapWritable | dict |
数组是不能自动转换的。用户需要在读写时指定ArrayWritable的子类型.在读入的时候,默认的转换器会把自定义的ArrayWritable子 类型转化成Java的Object[],之后串行化成Python的元组。为了获得Python的array.array类型来使用主要类型的数组,用户 需要自行指定转换器。
注意,如果这个读入格式仅仅依赖于一个Hadoop配置和/或输入路径,而且键值类型都可以根据前面的表格直接转换,那么刚才提到的这种方法非常合适。
如果你有一些自定义的序列化二进制数据(比如从Cassandra/HBase中读取数据),那么你需要首先在Scala/Java端将这些数据转化成可以被Pyrolite的串行化器处理的数据类型。一个转换器特质已经提供好了。简单地拓展这个特质同时在convert方法中实现你自己的转换代码即可。记住,要确保这个类以及访问你的输入格式所需的依赖都被打到了Spark作业包中,并且确保这个包已经包含到了PySpark的classpath中。
这里有一些通过自定义转换器来使用Cassandra/HBase输入输出格式的Python样例和转换器样例。
map
是一个转化操作,可以将数据集中每一个元素传给一个函数,同时将计算结果作为一个新的RDD返回。另一方面,
reduce
操作是一个启动操作,能够使用某些函数来聚集计算RDD中所有的元素,并且向驱动程序返回最终结果(同时还有一个并行的
reduceByKey
操作可以返回一个分布数据集)。
在Spark所有的转化操作都是惰性求值的,就是说它们并不会立刻真的计算出结果。相反,它们仅仅是记录下了转换操作的操作对象(比如:一个文件)。只有当一个启动操作被执行,要向驱动程序返回结果时,转化操作才会真的开始计算。这样的设计使得Spark运行更加高效——比如,我们会发觉由map
操作产生的数据集将会在reduce
操作中用到,之后仅仅是返回了reduce
的最终的结果而不是map
产生的庞大数据集。
在默认情况下,每一个由转化操作得到的RDD都会在每次执行启动操作时重新计算生成。但是,你也可以通过调用persist
(或cache
)方法来将RDD持久化到内存中,这样Spark就可以在下次使用这个数据集时快速获得。Spark同样提供了对将RDD持久化到硬盘上或在多个节点间复制的支持。
lines
仅仅是一个指向文件的指针。第二行将
lineLengths
定义为
map
操作的结果。再强调一次,由于惰性求值的缘故,
lineLengths
并
不会
被立即计算得到。最后,我们运行了
reduce
操作,这是一个启动操作。从这个操作开始,Spark将计算过程划分成许多任务并在多机上运行,每台机器运行自己部分的map操作和reduce操作,最终将自己部分的运算结果返回给驱动程序。
如果我们希望以后重复使用lineLengths
,只需在reduce
前加入下面这行代码:
lineLengths
在第一次计算生成之后保存在内存中。
def
定义。
值得指出的是,也可以传递类实例中方法的引用(与单例对象相反),这种传递方法会将整个对象传递过去。比如,考虑以下代码:
MyClass
对象,然后对它调用
doStuff
方法,
map
会用到这个对象中
func
方法的引用,所以整个对象都需要传递到集群中。
还有另一种相似的写法,访问外层对象的数据域会传递整个对象的引用:
shuffle
操作,比如将元素通过键来分组或聚集计算。
在Python中,这类操作一般都会使用Python内建的元组类型,比如(1, 2)。它们会先简单地创建类似这样的元组,然后调用你想要的操作。
比如,一下代码对键值对调用了reduceByKey
操作,来统计每一文本行在文本文件中出现的次数:
counts.sortByKey()
,比如,当我们想将这些键值对按照字母表顺序排序,然后调用
counts.collect()
方法来将结果以对象列表的形式返回。
(译者注:这部分翻译比较简略,仅供简单参考,具体细节请看文档)
转化操作 | 作用
————| ——
map(func) | 返回一个新的分布数据集,由原数据集元素经func处理后的结果组成
filter(func) | 返回一个新的数据集,由传给func返回True的原数据集元素组成
flatMap(func) | 与map类似,但是每个传入元素可能有0或多个返回值,func可以返回一个序列而不是一个值
mapParitions(func) | 类似map,但是RDD的每个分片都会分开独立运行,所以func的参数和返回值必须都是迭代器
mapParitionsWithIndex(func) | 类似mapParitions,但是func有两个参数,第一个是分片的序号,第二个是迭代器。返回值还是迭代器
sample(withReplacement, fraction, seed) | 使用提供的随机数种子取样,然后替换或不替换
union(otherDataset) | 返回新的数据集,包括原数据集和参数数据集的所有元素
intersection(otherDataset) | 返回新数据集,是两个集的交集
distinct([numTasks]) | 返回新的集,包括原集中的不重复元素
groupByKey([numTasks]) | 当用于键值对RDD时返回(键,值迭代器)对的数据集
aggregateByKey(zeroValue)(seqOp, combOp, [numTasks]) | 用于键值对RDD时返回(K,U)对集,对每一个Key的value进行聚集计算
sortByKey([ascending], [numTasks])用于键值对RDD时会返回RDD按键的顺序排序,升降序由第一个参数决定
join(otherDataset, [numTasks]) | 用于键值对(K, V)和(K, W)RDD时返回(K, (V, W))对RDD
cogroup(otherDataset, [numTasks]) | 用于两个键值对RDD时返回
(K, (V迭代器, W迭代器))RDD
cartesian(otherDataset) | 用于T和U类型RDD时返回(T, U)对类型键值对RDD
pipe(command, [envVars]) | 通过shell命令管道处理每个RDD分片
coalesce(numPartitions) | 把RDD的分片数量降低到参数大小
repartition(numPartitions) | 重新打乱RDD中元素顺序并重新分片,数量由参数决定
repartitionAndSortWithinPartitions(partitioner) | 按照参数给定的分片器重新分片,同时每个分片内部按照键排序
你可以通过调用persist
或cache
方法来标记一个想要持久化的RDD。在第一次被计算产生之后,它就会始终停留在节点的内存中。Spark的缓存是具有容错性的——如果RDD的任意一个分片丢失了,Spark就会依照这个RDD产生的转化过程自动重算一遍。
另外,每一个持久化的RDD都有一个可变的存储级别,这个级别使得用户可以改变RDD持久化的储存位置。比如,你可以将数据集持久化到硬盘上,也可以将它以序列化的Java对象形式(节省空间)持久化到内存中,还可以将这个数据集在节点之间复制,或者使用Tachyon将它储存到堆外。这些存储级别都是通过向persist()
传递一个StorageLevel
对象(Scala, Java, Python)来设置的。存储级别的所有种类请见下表:
注意:在Python中,储存的对象永远是通过Pickle库序列化过的,所以设不设置序列化级别不会产生影响。
Spark还会在shuffle操作(比如reduceByKey
)中自动储存中间数据,即使用户没有调用persist
。这是为了防止在shuffle过程中某个节点出错而导致的全盘重算。不过如果用户打算复用某些结果RDD,我们仍然建议用户对结果RDD手动调用persist
,而不是依赖自动持久化机制。
OFF_HEAP
模式有诸多优点:
RDD.unpersist()
方法。
map
和
reduce
)时,Spark会对涉及到的变量的所有副本执行这个函数。这些变量会被复制到每个机器上,而且这个过程不会被反馈给驱动程序。通常情况下,在任务之间读写共享变量是很低效的。但是,Spark仍然提供了有限的两种共享变量类型用于常见的使用场景:广播变量和累加器。
可以通过SparkContext.broadcast(v)
来从变量v创建一个广播变量。这个广播变量是v的一个包装,同时它的值可以功过调用value
方法来获得。以下的代码展示了这一点:
可以通过SparkContext.accumulator(v)
来从变量v创建一个累加器。在集群中运行的任务随后可以使用add
方法或+=操作符(在Scala和Python中)来向这个累加器中累加值。但是,他们不能读取累加器中的值。只有驱动程序可以读取累加器中的值,通过累加器的value
方法。
以下的代码展示了向一个累加器中累加数组元素的过程:
zero'用于为你的数据类型提供零值;'addInPlace'用于计算两个值得和。比如,假设我们有一个
Vector`类表示数学中的向量,我们可以这样写:
累加器不会该别Spark的惰性求值模型。如果累加器在对RDD的操作中被更新了,它们的值只会在启动操作中作为RDD计算过程中的一部分被更新。所以,在一个懒惰的转化操作中调用累加器的更新,并没法保证会被及时运行。下面的代码段展示了这一点:
SparkContext
,并如前文所述将master的URL设为local,执行你的程序,最后调用
SparkContext.stop()
来终止运行。请确保你在
finally
块或测试框架的
tearDown
方法中终止了上下文,因为Spark不支持两个上下文在一个程序中同时运行。
groupByKey
,
cogroup
,
join
, 它们的返回值都从(键,值列表)对变成了(键, 值迭代器)对。
你还可以阅读Spark Streaming, MLlib和GraphX的迁移指南。
你可以在Spark的网站上看到更多的Spark样例程序。另外,在examples
目录下还有许多样例代码(Scala, Java, Python)。你可以通过将类名称传给Spark的bin/run-example 脚本来运行Java和Scala语言样例,举例说明:
最后,完整的API文档在这里。Scala版本 Java版本 Python版本