Spark Programming Guide (Python) Spark编程指南 (二)

来源:互联网 发布:vfp九九乘法表编程 编辑:程序博客网 时间:2024/06/07 02:24

原始版本参见:http://cholerae.com/2015/04/11/-%E7%BF%BB%E8%AF%91-Spark%E7%BC%96%E7%A8%8B%E6%8C%87%E5%8D%97-Python%E7%89%88/

对部分内容有修改,恕本人水平有限,如有错误,在所难免。


PySpark编程指南(译):

1.  概述:

a)  从高层次上来看,每一个Spark应用都包含一个驱动程序,用于执行用户的main函数以及在集群上运行各种并行操作。Spark提供的主要抽象是弹性分布式数据集(RDD),这是一个包含诸多被划分到不同节点上进行并行处理的元素集合。RDD通过打开HDFS(或其他hadoop支持的文件系统)上的一个文件或转换一个已经存在于驱动程序中的Scala集合(已存在的RDD)来得到。用户可以要求Spark将RDD持久化到内存中,这样就可以在并行操作中高效复用。另外,在节点发生错误时RDD可以自动恢复。

b)  Spark提供的另一个抽象是可以在并行操作中使用的共享变量。在默认情况下,当Spark以任务集合(分布于不同节点)的方式并行执行一个函数时,对于所有在函数中使用的变量,每一个任务都会得到一个副本。有时,某一个变量需要在任务间或任务与驱动程序之间共享。Spark支持两种共享变量:broadcast variables,将一个值缓存到所有节点的内存中;accumulators,只能用于累加的变量,比如用于计数和求和。

2.  Spark链接

a)  Spark1.6.1支持Python2.6或更高的版本(支持Python3.4+)。它使用了标准的CPython解释器,所以诸如NumPy一类的C库也是可以使用的,PyPy2.3+也是适用的。

b)  通过Spark目录下的bin/spark-submit脚本你可以在Python下运行Spark应用。这个脚本会载入Spark的Java/Scala库然后让你将应用提交到集群中。你可以执行bin/pyspark来打开Python的交互命令行。

c)  如果你希望访问HDFS上的数据,你需要为你使用的HDFS版本建立一个PySpark链接。Prebuilt Package(即spark-1.6.0-bin-without-hadoop.tgz)在Spark主页已经可以找到,适合于常见的HDFS版本。

d)  最后,你需要将一些Spark的类import到你的程序中。加入如下这行:

from pyspark import SparkContext,SparkConf

e)  PySpark需要driver与workers之间具有相同的Python版本,使用了PATH路径下的默认版本。你可以明确你想使用的Python版本通过使用PYSPARK_PYTHON:

e.g.

$PYSPARK_PYTHON=python3.4 bin/pyspark$PYSPARK_PYTHON=/opt/pypy-2.5/bin/pypy bin/spark-submitexamples/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运行程序来获得这个参数。不过,在本地测试以及单元测试时,你仍然可以通过传入”local”来运行Spark程序。

4.  使用Shell

a)  在PySpark命令行中,一个特殊的集成在解释器里的SparkContext变量已经建立好了,变量名叫做sc。创建你自己的SparkContext将会失效。你可以通过使用--master命令行参数来设置与SparkContext连接的master主机,你也可以通--py-files参数传递一个用逗号隔开的列表将Python的.zip、.egg或.py文件添加到运行时路径中。你还可以通过—package参数传递一个用逗号隔开的maven列表来给这个Shell会话添加依赖(如Spark的包)。任何可能存在依赖的仓储(比如SonaType)都可以通过传给—repositories参数来添加进去。Spark包的所有Python依赖(列在这个包的requirements.txt文件中)在必要时都必须通过pip手动安装。

比如,使用四核来运行bin/pyspark应当输入这个命令:

$./bin/pyspark --master local[4]

又比如,把code.py文件添加到搜索路径中(为了能够在程序中import code),应当使用这条命令:

$./bin/pyspark --master local[4] --py-files code.py

想要了解命令行选项的完整信息请执行pyspark --help命令。在这些场景下,pyspark会触发一个更通用的spark-submit脚本

在IPython这个加强的Python解释器中运行PySpark也是可行的。PySpark可以在1.0.0或更高版本的IPython上运行。为了使用IPython,必须在运行bin/pyspark时将PYSPARK_DRIVER_PYTHON变量设置为ipython,就像这样:

$PYSPARK_DRIVER_PYTHON=ipython ./bin/pyspark

你还可以通过设置PYSPARK_DRIVER_PYTHON_OPTS来自定义ipython。比如,在运行IPython Notebook时开启PyLab图形支持应该使用这条命令:

$ PYSPARK_DRIVER_PYTHON=ipythonPYSPARK_DRIVER_PYTHON_OPTS="notebook --pylab inline" ./bin/pyspark

在IPython NoteBook 服务器被启动后,你可以通过”Files”栏创建一个新的”Python 2”notebook。在你试图通过IPython notebook来尝试Spark之前,你可以通过在notebook中,键入命令

%pylabinlines

来扩充你的notebook。

5.  弹性分布式数据集(RDDs)

Spark是以RDD概念为中心运行的。RDD是一个容错的、可以被并行操作的元素集合。创建一个RDD有两个方法:在你的驱动程序中并行化一个已经存在的集合;从外部存储系统中引用一个数据集。这个存储系统可以是一个共享文件系统,比如HDFS、HBase或任意提供了Hadoop输入格式的数据来源。

6.  并行化集合

并行化集合是通过在驱动程序中一个现有的迭代器或集合上调用SparkContext的parallelize方法建立的。为了创建一个能够并行操作的分布数据集,集合中的元素都会被拷贝。举例,以下语句创建了一个包含1到5的并行化集合:

data = [1, 2, 3, 4,5]distData= sc.parallelize(data)

分布式数据集(distData)被创建后,就可以进行并行操作了。比如,我们可以调用disData.reduce(lambda a, b: a+b)来对元素进行叠加。在后文中我们会描述分布数据集上支持的操作。

并行集合的一个重要参数是将数据集划分成分片的数量。对每一个分片,Spark会在集群中运行一个对应的任务。典型情况下,集群中的每一个CPU将对应运行2-4个分片。一般情况下,Spark会根据当前集群的情况自行设定分片数量。但是,你也可以通过将数目以第二个参数形式,传递给parallelize方法(比如sc.parallelize(data, 10))来手动确定分片数量。注意:有些代码中会使用切片(slice,分片的同义词)这个术语来保持向下兼容性。

7.  外部数据集

a)  PySpark可以通过Hadoop支持的外部数据源(包括本地文件系统、HDFS、 Cassandra、HBase、亚马逊S3等等)建立分布数据集。Spark支持文本文件、序列文件以及其他任何Hadoop输入格式文件。

b)  通过文本文件创建RDD可以使用SparkContexttextFile方法。这个方法会使用一个文件的URI(或本地文件路径,hdfs://、s3n://这样的URI等等)然后以文本行的集合的形式读入。e.g.文件调取示例:

>>>distFile = sc.textFile("data.txt")

建立完成后distFile上就可以调用数据集操作了。比如,我们可以调用mapreduce操作来迭加所有文本行的长度,代码如下:

distFile.map(lambdas: len(s)).reduce(lambda a, b: a + b)

c)  在Spark中读入文件时有几点要注意:

         i.     如果使用了本地文件路径时,要保证在worker节点上这个文件也能够通过这个路径访问。将这个文件拷贝到所有worker上或者使用网络挂载的共享文件系统来解决此问题。

       ii.     包括textFile在内的所有基于文件的Spark读入方法,都支持将文件夹、压缩文件、包含通配符的路径作为参数。比如,以下代码都是合法的:

textFile("/my/directory")textFile("/my/directory/*.txt")textFile("/my/directory/*.gz")

      iii.     textFile方法也可以传入第二个可选参数来控制文件的分片数量。默认情况下,Spark会为文件的每一个块(在HDFS中块的大小默认是64MB)创建一个分片。但是你      也可以通过传入一个更大的值来要求Spark建立更多的分片。注意,分片的数量绝不能小于文件块的数量。

d)  除了文本文件之外,Spark的Python API还支持多种其他数据格式:

         i.     SparkContext.wholeTextFiles能够读入包含多个小文本文件的目录,然后为每一个文件返回一个(文件名,内容)对。这与textFile方法为每一个文本行返回一条记录相对应。

       ii.     RDD.saveAsPickleFileSparkContext.pickleFile支持将RDD以简单的串行化的Python对象格式存储起来。串行化的过程中会以默认10个一批的数量批量处理。

      iii.     序列文件和其他Hadoop输入输出格式。

e)  注意:

这个特性目前仍处于试验阶段,被标记为Experimental,目前只适用于高级用户。这个特性在未来可能会被基于Spark SQL的读写支持所取代,因为Spark SQL是更好的方式。

f)  可写类型支持

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类型来使用主要类型的数组,用户需要自行指定转换器。

g)  保存和读取序列文件

         i.     和文本文件类似,序列文件可以通过指定路径来保存与读取。键值类型都可以自行指定,但是对于标准可写类型可以不指定。

>>>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')]

h)  保存和读取其他Hadoop输入输出格式

         i.     PySpark同样支持写入和读出其他Hadoop输入输出格式,包括’new’和’old’两种HadoopMapReduce API。如果有必要,一个Hadoop配置可以使用Python字典的形式传入。以下是一个例子,使用了ElasticsearchESInputFormat

$ SPARK_CLASSPATH=/path/to/elasticsearch-hadoop.jar./bin/pyspark>>> conf = {"es.resource" :"index/type"}   # assumeElasticsearch 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 isconverted to a Python dict(u'Elasticsearch ID', {u'field1': True,u'field2':u'Some Text',u'field3': 12345})

       ii.     注意,如果一个输入格式仅仅依赖于一个Hadoop配置和/或输入路径,并且键值类型都可以简单地根据前面的表格直接转换,那么刚才提到的这种方法将非常适用于此输入格式。

      iii.     如果你有一些自定义的序列化二进制数据(比如从Cassandra/HBase中读取数据),那么你需要首先在Scala/Java端将这些数据转化成可以被Pyrolite的串行化器处理的数据类型。转换器就是为此准备的。简单地拓展这个转换器的特点同时在convert方法中实现你的转换代码即可。注意,要确保这个类以及访问你的输入格式所需的依赖都被加入到Spark job jar中,并且确保这个包已经包含到了PySpark的classpath中。

       iv.     这里有一些通过自定义转换器来使用Cassandra/HBase输入输出格式的Python样例和转换器样例。

https://github.com/apache/spark/tree/master/examples/src/main/python

https://github.com/apache/spark/tree/master/examples/src/main/scala/org/apache/spark/examples/pythonconverters

i)  RDD操作

         i.     RDD支持两类操作:transformations,用于从已有的数据集创建新的数据集;actions,用于在数据集上的计算结束后向驱动程序返回一个值。举个例子,map是一个transformation,可以将数据集中每一个元素传给一个函数,同时将计算结果以一个新的RDD返回。另一方面,reduce操作是一个action,能够使用某些函数来聚集RDD中所有的元素,并且向驱动程序返回最终结果(同时也有一个并行的reduceByKey操作可以返回一个分布数据集)。

       ii.     在Spark中,所有的transformations操作都是惰性求值的,就是说它们并不会立刻开始计算。相反,它们仅仅是记录下了需要执行transformations操作的操作对象(比如:一个文件)。只有当一个action操作被执行,需要向驱动程序返回一个结果时,transformations操作才会真的开始计算。这样的设计使得Spark运行更加高效。 比如,我们会发觉由map操作产生的数据集将会在reduce操作中用到,之后仅仅是返回了reduce的最终的结果而不是map产生的庞大数据集。

      iii.     在默认情况下,每一个由transformations操作得到的RDD都会在每次执行actions操作时被重新计算。但是,你也可以通过调用persist(或cache)方法来将RDD驻留在内存中,这样Spark就可以在下次使用这个数据集时快速获得它。Spark同样提供了对将RDD记录到硬盘上或在多个节点间复制的支持。

j)  基本操作

         i.     以下程序是对RDD基本用法的演示说明:

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

第一行定义了一个由外部文件产生的基本RDD。这个数据集不是从内存中载入的也不是由其他操作产生的:lines仅仅是一个指向文件的指针。第二行将lineLengths义为map这一transformation操作的结果。再强调一次,由于惰性求值的缘故,lineLengths并不会被立即计算出来。最后,我们运行了reduce操作,这是一个action操作。此时,Spark将计算过程划分成许多tasks并在集群上运行,每台机器运行自己的task的map操作和本地的reduce操作,只是将自己的task的运算结果返回给驱动程序。

       ii.     如果我们希望以后重复使用lineLengths,只需在reduce之前追加下面这行代码:

lineLengths.persist()

这条代码将使得lineLengths在第一次计算生成之后驻留在内存中。

k)  向Spark传递函数:

         i.     Spark的API严重依赖于在驱动程序中传递函数作为参数。有三种推荐的方法来传递函数作为参数:

1.  Lambda表达式,简单的函数可以直接写成一个lambda表达式(lambda表达式不支持多语句函数和无返回值的语句)。

2.  对于代码很长的函数,Spark调用在本地用def定义的函数。

3.  模块中的顶级函数。

       ii.     比如,传递一个无法转化为lambda表达式的长函数,可以像以下代码这样:

"""MyScript.py"""if__name__ == "__main__":    def myFunc(s):        words = s.split(" ")        return len(words)     sc = SparkContext(...)    sc.textFile("file.txt").map(myFunc)

      iii.     应当注意的是,也可以传递类实例中方法的引用(与单例对象相反),这种传递方法会将整个对象传递过去。比如,考虑以下代码:

classMyClass(object):    def func(self, s):        return s    def doStuff(self, rdd):        returnrdd.map(self.func)

       iv.     在这里,如果我们创建了一个新的MyClass对象,然后对它调用doStuff方法,map会用到这个对象中func方法的引用,所以整个对象都需要传递到集群中。

         v.     还有另一种相似的写法,访问外部对象的数据域需要传递整个对象的引用:

classMyClass(object):    def __init__(self):        self.field = "Hello"    def doStuff(self, rdd):        returnrdd.map(lambda s: self.field + x)

       vi.     此类问题最简单的避免方法就是,使用一个局部变量缓存一份这个数据域的拷贝,避免外部直接访问对象属性:

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


 


0 0