本文翻译自Spark官方文档:Spark Programming Guide, (spark 1.3.0)去掉了其中Scala/Java的内容,只保留了Python及公共部分。
关键词:spark 编程指南 编程入门 python 中文版 spark教程 Spark官方文档
目录
概述
从概念上来说,每个spark程序都由一个驱动程序组成,驱动程序运行用户的main函数并且在集群上执行各种并行操作. Spark中的主要抽象概念是弹性分布式数据集(RDD), RDD是分布在集群各节点上的可并行操作的元素集合。 RDD可以通过hdfs中的文件创建,也可以通过驱动程序中的集合(例如Python中的List)转化而成。 用户还可以把RDD保存在内存中,供后续的并行操作高效重复使用. 并且, RDD可以从节点失效中自动恢复.
Spark的另外一个抽象概念是可以并行操作的共享变量(shared variables). By default, when Spark runs a function in parallel as a set of tasks on different nodes默认情况下,Spark以任务(task)集的方式在不同节点上并行执行函数,每个任务都会获得一份变量的拷贝。 有些情况下,变量需要在任务之间共享,或者再任务和驱动程序之间共享。为了解决这样的应用场景,Spark支持两种类型的共享变量:一种是广播变量( broadcast variables), 这种变量可以缓存在所有节点的内存中;另外一种是累加器(accumulators),种变量只能增加,例如计数器或者求和.
本文将会以Python语言为例介绍Spark的这些特性。通过启动Spark交互shell非常容易学习这些特性,Python版的交互shell是bin/pyspark 。
链接Spark
在Spark中运行Python程序,需要使用Spark安装目录中的bin/spark-submit脚本。 这个脚本会导入Spark的各种Java/Scala库,然后将程序提交到集群。 我们也可以通过bin/pyspark来启动Python版的Spark交互shell.
如果我们想访问HDFS数据,就需要使用hadoop对应的PySpark版本。在第三方发布(third party distributions) 这个网页上可以看到Cloudera Hadoop和Apache Hadoop的对应关系. 针对通用HDFS版本的预编译Spark包(Prebuilt packages) 可以在Spark官网找到。
最后,使用Python编写Spark程序,我们需要在代码中添加下面的行:
from pyspark import SparkContext, SparkConf
初始化Spark
conf = SparkConf().setAppName(appName).setMaster(master)
sc = SparkContext(conf=conf)
其中appName是程序名称,它会显示在集群状态界面上;master是要提交到的集群的地址,相关的值说明如下:
Master值 | 含义 |
---|---|
local | 使用本地worker单线程运行 |
local[K] | 使用本地worker K 线程运行 |
local[*] | 使用本地worker,根据机器配置自动选择线程数 |
spark://HOST:PORT | Spark standalone 集群. Master的域名和端口, 默认端口7077. |
mesos://HOST:PORT | Spark Mesos 集群. Master的域名和端口,默认端口5050。当Mesos使用Zookeeper, 地址为:mesos://zk://…. |
yarn-client | Spark YARN 集群, 客户端模式(Yarn Application只负责给Saprk驱动程序分配资源). 需要在spark-env.sh中配置HADOOP_CONF_DIR. |
yarn-cluster | Spark YARN 集群,集群模式(Yarn Application接管Spark驱动程序). 需要在spark-env.sh中配置HADOOP_CONF_DIR. |
In practice, when running on a cluster, you will not want to hardcodemasterin the program, but rather launch the application withspark-submit and receive it there.在实践中,当要在集群上运行程序时,我们一般不会把master写死在程序中,而是通过spark-submit脚本来提交程序,然后在程序中接收master参数值。如果只是作为测试,那么直接传入local做为master在本地运行就可以了。
使用Shell
我们可以通过下面的命令本地4线程运行pysparkShell:
$ ./bin/pyspark --master local[4]
可以通过下面的命令添加code.py文件 (从而可以在程序中import code):
$ ./bin/pyspark --master local[4] --py-files code.py
执行./bin/pyspark –help可以拿到完整的参数列表。pyspark实际上调用了更加通用的spark-submit脚本。
另外也可以通过IPython来执行PySpark Shell, 这里就不多说了。
Resilient Distributed Datasets (RDDs)
Spark是围绕弹性分布式数据集(RDD)的概念展开的,RDD是一种容错的可分布式操作的数据集合。有两中方式可以创建RDD:一种是将驱动程序中的已有集合平行化;另外一种是引用外部存储系统的数据集,例如共享文件系统,HDFS, HBase, 或者其他类似Hadoop的数据源。
并行化集合
data = [1, 2, 3, 4, 5]
distData = sc.parallelize(data)
分布式是集合(distData)一经创建,就可以并行操作了。例如,我们可以调用distData.reduce(lambda a, b: a + b) 来给这个整数链表求和。我们后续会讨论分布式集合上的相关操作。
并行化集合的一个重要参数是切分数据的分区数量,因为Spark会为集群的每个分区启动一个任务。 典型情况下,集群中的每个CPU需要2~4个分区。通常,Spark会基于集群配置自动设置分区数量。当然,我们也可以通过传给parallelize函数的第二个参数来手动设置分区数(例如:sc.parallelize(data, 10) )。注意:有些地方也会使用术语slices来表示分区数从而保证向后兼容。
外部数据集
distFile = sc.textFile("data.txt")
distFile一旦创建,就可以执行数据操作了。例如,求所有行字符串的长度和可以按下面的方式使用map和reduce: distFile.map(lambda s: len(s)).reduce(lambda a, b: a + b) 。
Spark读取文件时需要留意的问题:
- 如果使用本地文件系统地址,文件必须在各worker节点上同样的位置可读。要么将文件拷贝到所有的worker节点,要么使用网络共享文件系统。
- Spark的所有文件输入方法(包括textFile),都支持目录、压缩文件以及通配符。例如,我们可以使用textFile(“/my/directory”)、textFile(“/my/directory/*.txt”)和textFile(“/my/directory/*.gz”)。
- 可以通过textFile方法的第二个参数来控制文件的分区数量。默认情况下,Spark为每个文件块(Block)创建一个分区( HDFS中默认文件块大小是64M)。但是我们可以通过向函数传一个大的参数来让Spark创建更多的分区。注意不能将这个参数设置的比文件块数量少。
除了textFile, Spark的Python API 也支持几种另外的数据格式:
- 文本目录:使用SparkContext.wholeTextFiles我们可以读取多个小文本文件的目录,每个文件会以(文件名, 内容)对的形式返回。这跟textFile按行读取不同。
- Python序列化对象:RDD.saveAsPickleFile和SparkContext.pickleFile支持将RDD保存为简单的Python序列化对象。 序列化过程中,默认每10个元素一起批量处理.
- sequenceFile和Hadoop输入/输出格式
注意:当前上述特性是实验性的,适合高级用户。将来这些特性可能会被其他功能例如Spark SQL取代。 写支持 PySpark对sequenceFile的支持方式是:当导入Key-Value对形式的RDD,将可写类型转成JAVA基本类型,然后通过Pyrolite将JAVA基本类型序列化;当保存RDD时,做相反操作,即首先反序列化Python对象转为Java对象,然后转为可写类型。以下的可写类型会自动进行转化:
Writable Type | Python Type |
---|---|
Text | unicode str |
IntWritable | int |
FloatWritable | float |
DoubleWritable | float |
BooleanWritable | bool |
BytesWritable | bytearray |
NullWritable | None |
MapWritable | dict |
当读写数组时,不能自动处理,用户要自定义可写数组子类型(ArrayWritablesubtypes)。也就是说,写操作:用户需要指定自定义转化器将数组转为自定义的可写数组子类型 ; 读操作, 默认的转化器会将自定义的可写数组子类型 转为Java Object[], 然后序列化为Python元组。 To get Pythonarray.arrayfor arrays of primitive types, users need to specify custom converters.要使用Python数组(array.array)来容纳基本类型,用户需要指定自定义的转化器。 保存和加载 SequenceFiles 和文本文件一样,SequenceFiles也可以通过指定路径来保存和加载。特殊KEY和Value的类需要指定,但是对于标准的可写类型没有这个要求。例:
>>> 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')]
保存和加载其他 Hadoop 输入/输出 格式 PySpark可以读写任意的Hadoop输入输出格式,兼容新、旧版本的Hadoop MapReduce APIs。如果需要,Hadoop配置也可以作为词典传入读写文件的API。下面是使用Elasticsearch ESInputFormat的例子: 注:Elasticsearch是基于Lucene的分布式搜索引擎。
$ SPARK_CLASSPATH=/path/to/elasticsearch-hadoop.jar ./bin/pyspark
>>> 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})
注意,如果输入格式可以简单依赖于Hadoop配置或者输入路径,并且Key和Value 类可以按照上文表格中列出的进行转换,那么例子中的方法能很好的工作。 如果我们有自定义序列化的二进制数据(例如从Cassandra / HBase导入数据),那么我首先要在Scala/Java环境中把数据转成Pyrolite序列化能处理的类型。Spark为这种处理提供了一种转换器特性。在convert方法中,我们可以简单地扩展这个特性并且实现转化代码。要访问这种输入格式的数据,还需要确保将这个类以及需要的其他任何依赖打包到Spark job jar中,并且在PySpark classpath中指明。
使用自定义转换器读写Cassandra / HBase输入/输出格式的例子在这两个地方和可以看到:Python examples 和 Converter examples 。
RDD 操作
RDDs支持两种类型的操作:一种是转换(transformations), 该操作从已有数据集创建新的数据集;另外一种是动作(actions),该操作在数据集上执行计算之后返回一个值给驱动程序。例如, map就是一个转换,这个操作在数据集的每个元素上执行一个函数并返回一个处理之后新的RDD结果。另一方面,reduce是一个动作,这个操作按照某个函数规则聚集RDD中的所有元素并且把最终结果返回给驱动程序。
Spark中的所有转换操作都是lazy模式的,也就是说,不是立马做转换计算结果,而是将这些转换操作记录在相应的数据集上,当需要通过动作(action)把结果返回给驱动程序时才真正执行。这个设计使Spark运行起来更加高效。例如,如果通过map创建的数据集后续会被reduce用到,那么只有reduce的结果会返回给驱动程序,而不是更大的map结果。
默认情况下,RDD上的转换操作在每次做动作时,都会重新执行计算一次。然而,我们可以使用persist(或者cache)函数将RDD存放在内存中,方便后续的快速访问。另外,Spark也支持将RDD存放在磁盘上,或者在多个节点让冗余存储。
基本用法
lines = sc.textFile("data.txt")
lineLengths = lines.map(lambda s: len(s))
totalLength = lineLengths.reduce(lambda a, b: a + b)
第一行定义了一个基本的RDD结果,RDD数据源是一个外部文件。如果没有实际的操作,这个数据集不会加载到内存,也就是说变量lines仅仅是一个指针而已。第二行将map转换的结果定义为lineLengths变量。同样,由于Spark是lazy模式,lineLengths不会立即计算。最后,我们执行reduce, 由于这是一个真正的动作,Spark会将计算任务分发到各机器,每台机器上执行自有数据的map和reduce, 然后只是把最终结果返回给驱动程序。
If we also wanted to uselineLengthsagain later, we could add:
如果我们后续会再次使用变量lineLengths, 可以添加下面的命令: lineLengths.persist() 这样的话,在reduce动作之前,变量lineLengths第一次计算之后就会被保存在内存中。
Spark回调函数
- Lambda 表达式, 针对可以写成表达式的简单函数。(Lambda不支持多行函数,也不支持没有返回值的函数)
- 在代码较多的情况下,可以通过def命令定义成本地函数。
- 模块中的顶层函数。
例如,传入比lambda代码更长的函数,可以考虑用下面的方式:
"""MyScript.py"""
if __name__ == "__main__":
def myFunc(s):
words = s.split(" ")
return len(words)
sc = SparkContext(...)
sc.textFile("file.txt").map(myFunc)
我们也可以在类实例中传递方法的引用(给Spark的RDD操作),这要求将对象(包括类和方法)都发送给集群。例如,看下面的代码:
class MyClass(object):
def func(self, s):
return s
def doStuff(self, rdd):
return rdd.map(self.func)
如果我们创建一个新的MyClass对象然后调用doStuff,这时map引用的func是MyClass实例的方法,所以整个对象都要发送到集群上。
同样,访问外部对象的字段也会导致对整个对象的引用:
class MyClass(object):
def __init__(self):
self.field = "Hello"
def doStuff(self, rdd):
return rdd.map(lambda s: self.field + x)
为了避免这种情况,最简单的方法是将对象的字段拷贝到一个局部变量而不是直接外部访问(通过obj.field的方式)。
def doStuff(self, rdd):
field = self.field
return rdd.map(lambda s: field + x)
使用Key-Value对
在Python中,这些操作可以在包含Python内置元组的RDD上工作,例如(1,2)。我们可以简单的创建这样的元组然后调用想要的操作,例如:下面的代码在Key-Value对上使用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)
下表列出了Spark中一些常见的转换操作。可以参考RDD API文档(Python)查看细节。
转换(Transformation) | 含义 |
---|---|
map(func) | 对每个RDD元素应用func之后,构造成新的RDD |
filter(func) | 对每个RDD元素应用func, 将func为true的元素构造成新的RDD |
flatMap(func) | 和map类似,但是flatMap可以将一个输出元素映射成0个或多个元素。 (也就是说func返回的是元素序列而不是单个元素). |
mapPartitions(func) | 和map类似,但是在RDD的不同分区上独立执行。所以函数func的参数是一个Python迭代器,输出结果也应该是迭代器【即func作用为Iterator<T> => Iterator<U>】。 |
mapPartitionsWithIndex(func) | 和mapPartitions类似, but also provides func with an integer value representing the index of the partition, 但是还为函数func提供了一个正式参数,用来表示分区的编号。【此时func作用为(Int, Iterator<T>) => Iterator<U> 】 |
sample(withReplacement, fraction, seed) | 抽样: fraction是抽样的比例0~1之间的浮点数; withRepacement表示是否有放回抽样, True是有放回, False是无放回;seed是随机种子。 |
union(otherDataset) | 并集操作,重复元素会保留(可以通过distinct操作去重) |
intersection(otherDataset) | 交集操作,结果不会包含重复元素 |
distinct([numTasks])) | 去重操作 |
groupByKey([numTasks]) | 把Key相同的数据放到一起【(K, V) => (K, Iterable<V>)】,需要注意的问题:1. 如果分组(grouping)操作是为了后续的聚集(aggregation)操作(例如sum/average), 使用reduceByKey或者aggregateByKey更高效。2.默认情况下,并发度取决于分区数量。我们可以传入参数numTasks来调整并发任务数。 |
reduceByKey(func, [numTasks]) | 首先按Key分组,然后将相同Key对应的所有Value都执行func操作得到一个值。func必须是(V, V) => V’的计算操作。numTasks作用跟上面提到的groupByKey一样。 |
aggregateByKey(zeroValue, seqOp, combOp, [numTasks]) | 首先按Key分组,然后对同Key的Vaue做聚集操作。When called on a dataset of (K, V) pairs, returns a dataset of (K, U) pairs where the values for each key are aggregated using the given combine functions and a neutral “zero” value. Allows an aggregated value type that is different than the input value type, while avoiding unnecessary allocations. Like ingroupByKey, the number of reduce tasks is configurable through an optional second argument. |
sortByKey([ascending], [numTasks]) | 按Key排序。通过第一个参数True/False指定是升序还是降序。 |
join(otherDataset, [numTasks]) | 类似SQL中的连接(内连接),即(K, V) and (K, W) => (K, (V, W)),返回所有连接对。外连接通过:leftOUterJoin(左出现右无匹配为空)、rightOuterJoin(右全出现左无匹配为空)、fullOuterJoin实现(左右全出现无匹配为空)。 |
cogroup(otherDataset, [numTasks]) | 对两个RDD做groupBy。即(K, V) and (K, W) => (K, Iterable<V>, Iterable(W))。别名groupWith。 |
cartesian(otherDataset) | 笛卡尔积 |
pipe(command, [envVars]) | 将驱动程序中的RDD交给shell处理(外部进程),例如Perl或bash脚本。RDD元素作为标准输入传给脚本,脚本处理之后的标准输出会作为新的RDD返回给驱动程序。 |
coalesce(numPartitions) | 将RDD的分区数减小到numPartitions。当数据集通过过滤减小规模时,使用这个操作可以提升性能。 |
repartition(numPartitions) | 将数据重新随机分区为numPartitions个。这会导致整个RDD的数据在集群网络中洗牌。 |
repartitionAndSortWithinPartitions(partitioner) | 使用partitioner函数充分去,并在分区内排序。这比先repartition然后在分区内sort高效,原因是这样迫使排序操作被移到了shuffle阶段。 |
动作(Actions)
下表列出了一些Spark中常用的动作(actions)。可以参考RDD API文档(Python)查看细节。
动作(Action) | 含义 |
---|---|
reduce(func) | 使用func函数聚集RDD中的元素(func接收两个参数返回一个值)。这个函数应该满足结合律和交换律以便能够正确并行计算。 |
collect() | 将RDD转为数组返回给驱动程序。这个在执行filter等操作之后返回足够小的数据集是比较有用。 |
count() | 返回RDD中的元素数量。 |
first() | 返回RDD中的第一个元素。(通take(1)) |
take(n) | 返回由RDD的前N个元素组成的数组。 |
takeSample(withReplacement, num, [seed]) | 返回num个元素的数组,这些元素抽样自RDD,withReplacement表示是否有放回,seed是随机数生成器的种子)。 |
takeOrdered(n, [ordering]) | 返回RDD的前N个元素,使用自然顺序或者通过ordering函数对将个元素转换为新的Key. |
saveAsTextFile(path) | 将RDD元素写入文本文件。Spark自动调用元素的toString方法做字符串转换。 |
saveAsSequenceFile(path) (Java and Scala) |
将RDD保存为Hadoop SequenceFile.这个过程机制如下:1. Pyrolite用来将序列化的Python RDD转为Java对象RDD;2. Java RDD中的Key/Value被转为Writable然后写到文件。 |
countByKey() | 统计每个Key出现的次数,只对(K, V)类型的RDD有效,返回(K, int)词典。 |
foreach(func) | 在所有RDD元素上执行函数func。 |
RDD持久化
Spark中最重要的能力之一是将数据持久化到内存中方便后续操作。当持久化一个RDD的时候,一旦该RDD在内存中计算出来,每个节点保存RDD的部分分区,在其他动作中就可以重用内存中的这个RDD(以及源于它的新RDD)。这种机制使得后续的动作(actions)快很多(通常在10倍以上)。缓存是迭代算法或者快速交互的利器。
我们可以通过persist() 或者cache()两个方法将RDD标记为持久化的。该RDD第一次在动作中计算出来之后,就会被保存在各节点的内存中。Spark的缓存是容错的——任意RDD分区丢失之后,会自动使用原来的转换动作重新计算出来。
另外,每个持久化的RDD可以按照不同的存储等级来存储。例如,可以持久化到磁盘,也可持久化到内存中,还可以放到外部缓存系统(off-Heap)。这些存储等级可以通过传一个StorageLevel对象给函数persist()来设置。方法cache()是使用默认存储等级的快速写法(Python中存储等级设置为StorageLevel.MEMORY_ONLY_SER, Scala/Java中是StorageLevel.MEMORY_ONLY)。完整的存储等级说明如下:
(存储等级)Storage Level | 含义 |
---|---|
MEMORY_ONLY | 将RDD以反序列化对象保存在JVM中。如果内存容不下RDD,部分分区会再需要的过程中重新计算出来而不是缓存起来。这个是Scala/Java中cache()的默认存储方式。 |
MEMORY_AND_DISK | 将RDD以反序列化对象保存在JVM中。如果内存容不下RDD,会把容不下的分区放在磁盘上,需要的时候再从磁盘上读。 |
MEMORY_ONLY_SER | 类似MEMORY_ONLY,不同的对象以序列化JAVA对象的形式存储(每个分区是一个byte数组)。这种方式比反序列化对象更高效,特别是使用快速序列化器的时候,不过读数据会消耗更多的CPU。 |
MEMORY_AND_DISK_SER | 类似MEMORY_AND_DISK,不同的对象以序列化JAVA对象的形式存储 |
DISK_ONLY | 仅将RDD保存在磁盘上。 |
MEMORY_ONLY_2,MEMORY_AND_DISK_2, MEMORY_ONLY_SER_2,MEMORY_AND_DISK_SER_2 |
同上,但是每个分区会在两个节点上冗余。 |
OFF_HEAP (experimental) | 将RDD保存在Tachyon上(Tachyon是一个分布式内存文件系统,可以在集群里以访问内存的速度来访问存在tachyon里的文件)。和MEMORY_ONLY_SER相比,OFF_HEAP可以减少垃圾收集的开销,还允许执行器(executors)轻量化并共享内存池。这使得OFF_HEAP在大堆内存环境和高并发程序中很有竞争力。而且,由于RDD保存在Tachyon中,执行器的挂掉不会导致内存缓存数据的丢失。 |
Note: In Python, stored objects will always be serialized with the Pickle library, so it does not matter whether you choose a serialized level. Spark也会自动持久化shuffle操作中的一些即时数据(例如reduceByKey的过程中),即使用户不显示调用persist。这样做可以避免shuffle过程中节点失效时重新计算整个输入。不过我们还是建议显示调用persist,如果需要计划重用RDD的话。
选择哪个存储级别?
Spark的存储级别是为了提供在内存使用CPU效率上的平衡。我们建议通过以下过程来做出选择。
- 如果RDD能适应默认的存储等级(MEORY_ONLY),那就选这种方式。因为这是最利于CPU效率的选择,允许尽可能快的操作RDD。
- 如果不行的话就使用(MEMORY_ONLY_SER),并选择一个快速序列化库让对象有更好的空间利用率,并且有相当快的访问速度。
- 不要让RDD溢出到磁盘上,除非计算数据的代价很高,或者产出的数据特别大。
- 如果想要快速的故障恢复就使用冗余存储等级。所有的存储等级通过重新计算丢失的数据来提供完整的容错,但是冗余存储让我们可以继续操作RDD,而不用等待重新计算丢失的分区。
-
在需要大量内存或者多个应用的环境中,实验特性OFF_HEAP模式有以下几个优点:
- 多个执行器共享内存。
- 显剧减少垃圾收集的开销
- 个别执行器挂掉不会到值缓存数据丢失。
删除数据
Spark自动监视每个节点上的缓存使用,并按LRU(最近最少访问)的方式踢掉数据。如果我们想要手动删掉RDD而不是等到被踢出缓存,使用RDD.unpersist()方法。
共享变量
通常,当一个函数被传到远程集群节点执行的Spark操作中(例如map或者reduce)时,所有的函数变量都会有独立的工作副本。这些变量会被拷贝到每台机器,远程机器上变量的修改不会回传给驱动程序。在任务之间对一般读写共享变量的支持比较低效。但是,为了适应通用的引用场景,Spark还是提供了有两个有限的共享变量类型:广播变量和累加器。
广播变量
共享变量允许程序员将一个只读的变量缓存在每台机器上,而不是让每个任务随带一个变量的副本。广播变量为在每个节点上提供海量的输入数据集提供了一种高效的方式。Spark会尝试使用高效饿广播算法来减少分发广播变量的通信消耗。
广播变量通过调用SparkContext.broacase(v)创建, v是一个变量。广播变量是v的封装, v的值可以通过value方法访问。下面的代码说明了这个用法:
>>> broadcastVar = sc.broadcast([1, 2, 3])
<pyspark.broadcast.Broadcast object at 0x102789f10>
>>> broadcastVar.value
[1, 2, 3]
广播变量创建之后,应该在所有函数中替代v来使用,以免v多次被发送到集群节点。另外,对象v广播之后,不应该被修改,从而保证所有的节点看到的是相同的广播变量值。
累计器
累计器是只能通过关联操作做“加”运算的变量,从而可以高效支持并行。它可以用来实现计数器或者求和。Spark原生支持数字类型的累计器,程序员可以增加对新类型的支持。 如果累加器创建时赋给了一个名字,那么这个累加器会在Spark的UI上展现。这个有利于理解程序的执行过程(遗憾的是这个功能Python中暂不支持)。 累计器通过调用函数SparContext.accumulator(v)并赋予一个初值来创建。然后跑在集群上的任务就可以使用add方法或者+=运算符增加累计器的值。但是,任务是不能读这个累计器的值得,只有驱动程序才可以通过方法value来读。
下面的代码展示了将一个数组中的元素都添加到累计器的过程:
>>> accum = sc.accumulator(0)
Accumulator<id=0, value=0>
>>> sc.parallelize([1, 2, 3, 4]).foreach(lambda x: accum.add(x))
...
10/09/29 18:41:08 INFO SparkContext: Tasks finished in 0.317106 s
scala> accum.value
10
这段代码使用的是内置的整数类型累加器,程序员也可以通过子类AccumulatorParam创建自己的类型。AccumulatorParam接口有两个方法:zero用于提供零值, addInPlace用于将两个值求和。例如,假设我们要使用Vector类表示数学向量,可以编写下面的代码:
class VectorAccumulatorParam(AccumulatorParam):
def zero(self, initialValue):
return Vector.zeros(initialValue.size)
def addInPlace(self, v1, v2):
v1 += v2
return v1
# Then, create an Accumulator of this type:
vecAccum = sc.accumulator(Vector(...), VectorAccumulatorParam())
由于累计器的修改只能在动作(actions)内执行,Spark可以保证每个任务对累计器的修改只会执行一次,重启任务(tasks)也不会导致修改累计器的值。在转换(transformations)中,如果任务重新执行,用户需要意识到每个任务中的修改操作都会被执行多次。
累计器不会改变Spark的lazy模式。如果累计器在RDD操作中被修改了,累计器的值只会在RDD做为动作(actions)操作进行计算时才会被修改。所以,在想map()这样的lazy转换中,不能保证累计器的修改被执行完成。下面的代码片段说明了这个特性:
accum = sc.accumulator(0)
data.map(lambda x => acc.add(x); f(x))
# Here, acc is still 0 because no actions have cause the `map` to be computed.
部署到集群
程序提交指南描述了如何将程序提交到集群。总之,一旦我们把程序打包成.py文件集或者.zip文件, bin/spark-submit脚本就可以帮助我们将程序提交到任意支持的集群管理器。
单元测试
Spark可以很方便地使用任意流行的UT框架做单元测试。在测试中简单创建SparkContext并将master URL设置成local,执行操作,然后调用SparkContext.stop()停止Context。确保在结束时停止context或者在测试框架的的tearDown函数中停止context,因为Spark不支持两个context在同一个程序中运行。
Spark版本迁移(pre-1.0)
Spark 1. 0冻结了Spark 1.X系列的核心API, 也就是说所有现在可用且没有被标记为实验性(experimenttal)的或者开发中的API未来版本中也会支持。Python中唯一变化是分组(grouping)操作, 例如groupByKey/cogroup/join, 这些操不再返回(key, list of values),而是返回(key, iterable of values)。其他部分的迁移指南参见:Spark Streaming, MLlib and GraphX.
接下来做什么
我们可以在Spark网站上阅读一些Spark程序的例子。另外,Spark安装目录下的examples下面包含了几个例程,运行方法如下(Python版):
./bin/spark-submit examples/src/main/python/pi.py
为了帮助优化我们的程序,配置和优化指南提供了最佳实践信息。特别重要的是,要确保数据数据在内存中以高效的格式保存。为了帮助我们理解如果部署程序,集群模式总览介绍了跟分布式操作和集群管理器相关的组件。
Finally, full API documentation is available in Scala, Java and Python.
最后,完整的API文档参见:Python。
【转载请注明:纯净的天空https://vimsky.com出品】