Overview
更高的层次上,每个Spark应用由运行main 方法并在集群中执行多种并行操作的driver program组成。Spark主要抽象概念是提供一个 resilient distributed dataset 弹性分布式数据集(RDD)-一个可以并行操作的跨集群节点的元素集合。RDDs从HDFS创建,或在驱动程序中存在的Scala集合,并转化它。用户也可能要求Spark持久一个内存中的RDD,在并行操作中允许有效的重复使用。最后,RDDs自动的恢复失败节点。
Spark的第二种抽象概念是并行操作中使用 shared variables 共享变量。缺省方式,当Spark在不同节点上以一系列任务的方式并行运行一个方法时,它把方法中使用的每个变量的副本传递到每个任务中。有时,一个变量需要在任务中贡献,或在任务间共享,或在驱动程序间。Spark提供两种共享变量类型:broadcast variables(广播变量)-在集群内存中缓存值时使用;accumulators(累加寄存器)-像counters,sums一样只能”added” to的变量。
这个指南展示了每一种Spark支持的语言的特点。很容易跟随Spark的交互shell进行学习-Scala的shell:bin/spark-shell,Python的shell:bin/pyspark。
Linking with Spark
Spark 1.3.0使用Scala 2.10。为了使用Scala编写applications,你需要使用兼容的Scala版本(e.g. 2.10.x)。
编写Spark应用,你需要添加Maven支持。Maven Central的Spark支持:
groupId = org.apache.spark
artifactId = spark-core_2.10
version = 1.3.0
另外,如果你想访问HDFS集群,你需要为你的HDFS版本添加一个hadoop-client依赖。一些常见的HDFS版本列表third party distributions。
groupId = org.apache.hadoop
artifactId = hadoop-client
version = <your-hdfs-version>
最后,你需要在程序中引入一些Spark classes和隐式转换。添加下面这些行:
import org.apache.spark.SparkContext
import org.apache.spark.SparkContext._
import org.apache.spark.SparkConf
Initializing Spark
Spark程序必须做的第一件事是创建SparkCOntex对象-告诉Spark怎样访问一个集群。要创建SparkContext,首先要建立一个包含你的app信息的SparkConf对象。
每个JVM只能有一个SparkContext。你必须在创建新SparkContext之前,stop()
当前活动的SparkContext。
val conf = new SparkConf().setAppName(appName).setMaster(master)
new SparkContext(conf)
参数appName
是你的app在集群UI中显示的名字。master
是Spark, Mesos or YARN cluster URL,或者一个特殊的”local“字符串以本地模式运行。
实际上,当在集群中运行时,你不想在程序中硬编码master
,宁可launch the application with spark-submit
,然后在那里使用它。然而,针对本地测试和单元测试,你可以传递”local”来在进程内运行Spark。
Using the Shell
Spark Shell中,一个特殊的解释器SparkContext已经为你创建好了,sc
变量引用。你自己的SparkContext不会工作。你可以使用--master argument
来设置context要连接的master,你也可以添加JARs到classpath,通过传递一个逗号分隔的列表给--jars argument
。你也可以添加依赖(e.g. Spark Pacjages)到你的shell session,通过提供一个逗号分隔的maven坐标列表给--packages argument
。任何依赖存在附加仓库都可以被传递给--repositories argument
。例如,以四核心运行bin/spark-shell
,使用:
$ ./bin/spark-shell --master local[4]
或者,也可以添加code.jar
到它的classpath,使用:
$ ./bin/spark-shell --master local[4] --jars code.jar
使用maven坐标引入依赖:
$ ./bin/spark-shell --master local[4] --packages "org.example:example:0.1"
运行spark-shell --help
,可以得到一个完全选项的列表。在幕后,spark-shell
调用更常规的spark-submit
script.
Resilient Distributed Datasets (RDDs)
Spark围绕着==resilient distributed dataset (RDD)==概念-一种可以并行操作的容错性的元素集合。有两种创建RDDs的方式:在你的驱动程序中parallelizing一个已存在的集合;或者引用一个外部存储系统(例如:共享文件系统,HDFS,HBase,任何提供Hadoop InputFormat的数据源)的数据集。
Parallelized Collections
并行集合通过在你的驱动程序中已存在的集合(Scala的Seq
),调用SparkContext’s parallelize
方法创建。从分布式数据集,复制集合的元素-可以并行操作。例如,下面就是怎么创建1 to 5的并行集合:
val data = Array(1, 2, 3, 4, 5)
val distData = sc.parallelize(data)
一旦创建,这个分布式的数据集(distData
)可以并行的被操作。例如,我们可以调用distData.reduce((a, b) => a + b)
合计array中的元素。我们一会儿再描述分布式数据集的上的操作。
并行集合的一个重要的参数是数据集切分成partitions的数量。Spark在集群中对每个分区运行一个任务。典型的是在集群中每个CPU你应该有2-4个分区。通常,Spark会根据你的集群自动设置分区。然而,你也可以手动的设置它-传递第二个参数给parallelize
(e.g. sc.parallelize(data, 10)
)。注意:一些代码的地方使用术语切片(分区的同义词)维持向后兼容。
External Datasets
Spark可以从任何Hadoop支持的存储源创建分布式数据集,包括你的本地文件系统,HDFS,Cassandra, HBase, Amazon S3等等。Spark支持文本文件,SequenceFiles,和其它任何Hadoop InputFormat。
文本文件RDDs可以调用SparkContext’s textFile
方法来创建。这个方法接受一个文件URI(机器上的local path,或在hdfs://,s3n://,等等其它URI),然后像lines集合读取它。下面是调用示例:
scala> val distFile = sc.textFile("data.txt")
distFile: RDD[String] = MappedRDD@1d4cee08
一旦创建,distFile
可以通过数据集操作起作用(被执行)。例如,我们可以使用map
和reduce
操作合计lines的sizes:distFile.map(s => s.length).reduce((a,b) => a + b)
。
一些使用Spark读取文件的注意事项:
- 如果使用本地文件路径,这个文件在工作节点上必须也可以以相同的路径被访问。
- 所有基于文件的输入方法,包括textFile,支持目录,压缩文件,通配符。例如,你可以使用
textFile("/my/directory")
,textFile("/my/directory/*.txt")
,textFile("/my/directory/*.gz")
。 textFile
方法也接受一个可选的第二参数来控制文件切片数。缺省的,Spark针对每个文件block(HDFS中blocks的默认大小是64M)创建一个切片,但是你也可以传递一个更大的值取得一个更高的切片值。注意,切片数不能少于blocks数。
除了文本文件,Spark的Scala API也至此一些其它数据格式:
SparkContext.wholeTextFiles
让你可以读取包含多个小文本文件的目录,然后以(filename, content)对的格式返回它们。与textFile
对比,后者会返回每个文件每行的记录。- SequenceFiles,使用SparkContext的
sequenceFile[K, V]
方法,k
和v
是文件中key和valus的类型。这些应该是Hadoop Writable接口的子类, 例如 IntWritable 和 Text。另外,Spark也允许你为一些普通Writables指定本地类型;例如,sequenceFile[Int, String]
会自动读取IntWritbles 和 Texts。 - 对于其它Hadoop InputFormats,你可以使用
SparkContext.hadoopRDD
方法-接受一个专用的JobConf
和input format class,key class,value class。使用你的输入源用相同的方法设置Hadoop Job。你也可以对Inputformats使用基于新MapReduce API(org.apache.hadoop.mapreduce
)的SparkContext.newAPIHadoopRDD
方法。 RDD.saveAsObjectFile
和SparkContext.objectFile
支持把RDD保存在由序列化Java对象组成的一个简单格式中。然而这不像专门的格式如Avro那样有效,Avro提供一个简单的方式来保存任意RDD。
RDD Operations
RDDs支持两种操作类型:transformations-从存在的数据集创建一个新数据集;actions-在数据集上运行一个计算后返回一个值到驱动程序。例如,map
是一个transformations-把数据集中每个元素传递到方法中,返回代表结果的新RDD。另一方面,reduce
是一个action-使用一些方法合计所有RDD的元素,然后返回最后的结果到驱动程序(尽管还有个并行的reduceByKey
返回一个分布式数据集)。
Spark中所有transformations都是lazy,因此它们不能立即计算出结果。它们只是记录应用在一些基本数据集(e.g. file)上的transformations。Transformations只有当action要求一个结果返回到驱动程序时才被计算。这种设计使Spark运行的更有效率-例如,我们可以这样理解:通过map
创建的一个数据集会被使用在一个reduce
中,然后只是返回reduce
的结果到驱动,好过一个巨大的映射数据集。
缺省的,每次在一个transformed RDD运行一个action时,都会重新计算一次。然而,你也可以使用persist
(或cache
)方法persist一个RDD到内存,在这种情况下Spark会把元素存储在集群上,这样下次查询就可以更快的被访问。也支持把RDDs持久到disk,或者复制到多个节点。
Basics
为了举例说明RDD基础,考虑下面这个简单程序:
val lines = sc.textFile("data.txt")
val lineLengths = lines.map(s => s.length)
val totalLength = lineLength.reduce((a, b) => a + b)
第一行从外部文件定义一个基本RDD。这个数据集不会加载到内存或者有其他行为:lines
仅仅是一个文件指针。第二行将map
转换的结果定义为linesLengths
。此外,lineLengths
不是立即计算,应该是延迟的。最后,我们运行reduce
,一个action。此时,Spark把计算分解到任务,在不同的机器运行,然后每个机器运行它的map部分和本地reduce,返回只是它的给驱动程序的答复。
如果我们一会儿还要再次使用lineLengths
,可以添加:
lineLengths.persist()
使用在reduce
之前,lineLengths
会在第一次计算后被保存在内存中。
Passing Functions to Spark
Spark的API很倚重在集群中的驱动程序中传递函数。有两种推荐方式:
Anonymous function syntax-可以被使用在短代码段中。
全局单例对象中的静态方法。例如,你可以定义
object MyFunctions
,然后传递MyFunctions.func1
,就像下面:
object MyFunctions {
def func1(s: String): String = { ... }
}
myRdd.map(MyFunctions.func1)
注意:也可以传递引用到类(对应一个单例object)实例的方法中,这需要跟随方法一起发送这个类的对象。例如,考虑下面:
class MyClass {
def func1(s: String): String = { ... }
def doStuff(rdd: RDD[String]): RDD[String] = { rdd.map(func1) }
}
这里,如果我们创建MyClass
,然后调用doStuff
,这个MyClass
实例中的map
引用了这个实例中的func1
方法,那么就需要发送整个对象到集群中。等同于rdd.map(x => this.func1(x))
。
同样的方式,访问外部对象的域,会引用到整个对象:
class MyClass {
val field = "Hello"
def doStuff(rdd: RDD[String]): RDD[String] = { rdd.map(x => field + x) }
}
等同于rdd.map(x => this.field + x)
,会引用this中的所有。为了避免这个问题,一个简单的方法是复制field
到一个本地变量,而不是外部访问:
def doStuff(rdd: RDD[String]): RDD[String] = {
val field_ = this.field
rdd.map(x => field_ + x)
}
==Important==:
1. Issue:访问外部域时,会引用到外部域所在的整个对象
2. Avoid:复制field
到本地变量
Working with Key-Value Pairs
虽然工作在RDDs上大部分Spark操作包含了任意类型的对象,但一些特殊操作只在Key-Value对格式的RDDs起作用。最常见的操作是分布式的”洗牌”操作,像通过key分组或聚集。
在Scala中,这些操作会自动在RDDs上起作用,包含Tuple2 objects(语言中内建的tuples,通过简单编写(a, b)创建),一旦你importorg.apache.spark.SparkContext._
到你的程序中,就会启用Spark的隐式转换。Key-value对儿操作起作用在http://spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.rdd.PairRDDFunctions“>PairRDDFunctions类-如果你引入这个转换将自动包装一个元组RDD。
例如,下面代码使用reduceByKey
操作key-value对儿计算文件中每行文本出现的次数:
val lines = sc.textFiel("data.txt")
val pairs = lines.map(s => (s, 1))
val counts = pair.reduceBykey((a, b) => a + b)
我们也可以使用counts.sortByKey()
,例如,按字母排序对儿,最后使用counts.collect()
把他们以数组对象形式传回驱动程序。
注意:当你在key-value对儿操作中使用自定义对象做key时,你必须保证一个自定义equals()
方法和一个相匹配的hashCode()
方法。有关详情,请见Object.hashCode() documentation。
==Important==:
1. reduceBykey()
,sortByKey()
等操作是key-value格式RDD特有的操作
2. reduceBykey()
,sortByKey()
操作是transformation,不是action
3. 需要再做collect()
之类的action操作,执行并返回真实数据集。
Transformations
下面列表列举了Spark中一些常见的transformations。参考RDD API doc(Scala, Java, Python)和PairRDDFunctions doc(Scala, Java)。
Transformation | Meaning |
---|---|
map(func) | 返回一个新的分布式数据集-由通过把源数据中每个元素都传递给func方法后组成. |
filter(func) | 返回一个新数据集-由源数据中func方法会返回true的元素组成 |
flatMap(func) | 与map相似,但是每个输入元素会被映射到0或者更多输出元素(所以func会返回一个Seq而不是一个单一的元素) |
mapPartitions(func) | 与map相似,但是独自运行在RDD的每个分区(block),所以当运行在一个T类型的RDD上时,func类型必须是Iterator<T> => Iterator<U> 类型的 |
mapPartitionsWithIndex(func) | 与mapPartitions相似,但是也会提供一个表示分区索引整数给func,所以当运行在一个T类型的RDD上时,func的类型必须是(Int, Iterator<T> => Iterator<U>) |
sample(withReplacement, fraction, seed) | 使用一个给出的随机数生成器种子,替换或者不替换的,抽样数据的一个分数fraction |
union(otherDataset) | 返回一个包含入参的元素和源数据集的元素的聚集的新数据集 |
intersection(otherDataset) | 返回一个包含入参的元素和源数据集的元素的交集的新数据集 |
distinct([numTasks]) | 返回一个包含源数据集distinct元素的新数据集 |
groupByKey([numTasks]) | 当被(K, V) 数据集调用,会返回一个(K, Iterable<V>) 数据集. 注意:如果你在每个key上分组为了执行聚合(sum或average),使用reduceByKey() 或aggregateByKey() 会有更好的性能. 注意:缺省的,输入的并行等级依赖于父RDD的分区数量。你可以传递一个可选的numTasks 参数设置一个不同的任务数量 |
reduceByKey(func, [numTasks]) | 当被一个(K, V) 数据集调用,会返回一个(K, V) -使用给出的reduce方法func(类型是(V, V) => V ), 聚集每个key对应的值。像groupByKey() 一样,通过一个可选的第二参数配置reduce的任务数量 |
aggregateByKey(zeroValue)(seqOp, combOp, [numTasks]) | 当被一个(K, V) 数据集调用,返回一个(K, U) 数据集-使用给出的合并方法和独立的’zero’值,聚集每个key对应的value。允许聚集值的类型不同于输入值的类型,为了不必要的内存分配。像groupByKey() 一样,通过一个可选的第二参数配置reduce的任务数量 |
sortByKey([ascending], [numTasks]) | 当一个K implements Ordered的(K, V) 数据集被调用,返回一个以Keys递增或递减顺序排序的(K, V) 数据集,由Boolean参数ascending 指定 |
join(otherDataset, [numTasks]) | 当(K, V) 和(K, W) 类型数据集调用时,返回一个(K, (V, W)) 类型数据集。通过leftOuterJoin() ,rightOuterJoin() ,fullOuterJoin() 支持Outer joins |
cogroup(otherDataset, [numTasks]) | 当被(K, V) 和(K, W) 类型数据集调用时,返回一个tuple(K, (Iterable<V>, Iterable<W>)) 类型的数据集。这个操作也可以是groupWith() 。 |
cartesian(otherDataset) | 当一个T或U类型的数据集调用时,返回一个(T, U) 类型的数据集 |
pipe(command, [envVars]) | 通过管道把RDD分区输送到shell控制台,例如一个Perl或bash脚本。RDD元素被写入进程的标准输入和行输出到它的标准输出,以字符串RDD形式返回。 |
coalesce(numPartitions) | 把RDD的分区数量减少到numPartitions。 对于过滤出一个巨大的数据集后,更高效的执行操作很有用。 |
repartition(numPartitions) | 随机的重新洗牌RDD中的数据,创建不多也不少的分区,在他们之间平衡。这总会重新洗牌网络上所有的数据。 |
repartitionAndSortWithinPartitions(partitioner) | 根据给出的partitioner重新分配RDD,在每个结果分区里面,用它们的keys进行排序。这比调用repartition 更高效,而且因为它可以把排序也放入每个洗牌机器,所以在每个分区中排序 |
Actions
下面是的列表列出了Spark支持的常用actions。参考了RDD API doc(Scala, Java, Python) 和 PairRDDFunctions doc(Scala, Java)
Action | Meaning |
---|---|
reduce(func) | 使用func方法(获取2个参数返回1个)聚集数据集的元素。这个方法应该是可交换的和可合并的,那么它就可以正确的并行计算 |
collect() | 在驱动程序中以数组形式返回数据集的所有元素。这通常很有用,在filter以后或者其它会返回数据的一个十分小的子集的操作以后。 |
count() | 返回数据集中元素的数量 |
first() | 返回数据集的第一个元素(类似take(1) ) |
take(n) | 以数组形式返回数据集的前n个元素 |
takeSample(withReplace, num, [seed]) | 以数组形式返回数据集num个元素的随机样本,带或者不带replacement,可选的提前指定一个随机数生成器seed |
takeOrdered(n, [ordering]) | 返回数据集前n个元素,使用自然排序或自定义comparator |
saveAsTextFile(path) | 以文本文件(或一系列文本文件)形式,把数据集中元素写入到一个给出的本地文件系统目录、HDFS或者其它任何Hadoop支持的文件系统。Spark会调用toString() 将每个元素转化成文件中的一行文本 |
saveAsSequenceFile(path)(Java and Scala) | 以Hadoop SequenceFile格式,把数据集元素写入到一个给出的本地文件系统、HDFS或其他任何Hadoop支持的文件系统。这在实现了Hadoop的Writable Interface的key-value格式RDDs有效。在Scala中,对可以隐含转化成Writable的类型(Spark对基本类型像Int, Double, String等待包含了转换),同样有效 |
savaAsObjectFile(path) | 把数据集元素写入一个使用Java序列化的简单格式-以后可以使用SparkContext.objectFile() 加载 |
countByKey() | 只对(K, V) 类型RDDs有效。返回一个(K, Int) 格式的hashmap-计算每个key的个数 |
foreach(func) | 返回针对每个数据集元素的方法func() 后的结果。这通常会有副作用-像更新一个寄存器变量(见下文)或与外部存储系统交互 |
RDD Persistence
Spark一个重要能力是persisting(或caching)一个数据集到内存,在一系列操作中使用。当你持久一个RDD,每个节点存储RDD任意一个分区,在内存中计算,然后再在这个数据集(或衍生出的数据集)的actions中重新使用。这会让后面的actions更快(一般快10倍)。Caching是迭代算法和交互使用的关键工具。
你可以使用persis()
或cache()
方法使一个RDD持久化。第一次在action中计算后,它就会被保持在节点的内存中。Spark的缓存是可容错的-如果任意一个RDD分区丢失了,它会使用当初创建它的transformations自动重新计算。
另外,允许对每个持久的RDD使用不同的storage level存储,例如:持久化数据集到磁盘,持久化它到内存-但要以序列化Java对象形式,在节点间复制它,或存储它off-heap到Tachyon。这些级别可以通过传递一个StorageLevel
(Scala, Java, Python)对象给persis()
方法来设置。cache()
方法使用默认的存储级别-StorageLevel.MEMORY_ONLY
(存储非序列化对象到内存)。完整的存储基本如下:
Storage Level | Meaning |
---|---|
MEMORY_ONLY | 以非序列化Java对象形式存储在JVM中。如果RDD不适合在内存中,一些分区将不会被缓存,从而在每次需要它们时都会被重新计算。默认级别 |
MEMORY_AND_DISK | 以非序列化Java对象形式存储在JVM中。如果RDD不适合在内存中,存储不适合分区到磁盘,当需要时从那里被读取 |
MEMORY_ONLY_SER | 以序列化Java对象形式存储在JVM中(每个分区一个byte数组)。这通常比非序列化对象更节省空间,特别是当使用fast serializer时,但是读取时会更耗CPU |
MEMORY_AND_DISK_SER | 类似于MEMORY_ONLY_SER,但是会把不适合内存的分区存储到磁盘,而不是在每次需要它们时再计算 |
DISK_ONLY | 只存储RDD分区到磁盘 |
MEMORY_ONLY_2, MEMORY_AND_DISK_2, etc. | 和上面的级别一样,但是复制每个分区到2个集群节点 |
OFF_HEAP (experimental) | 以序列化格式存储RDD到Tachyon。相对于MEMORY_ONLY_SER,OFF_HEAP减少GC的花销,并且允许executor变的更小而且共享内存池,让它在大内存或多并发应用的环境中更有吸引力。此外,因为RDDs驻留在Tachyon,executor的崩溃不会导致丢失内存中的缓存。以这种模式,Tachyon中内存是可废弃的。因此,Tachyon不会试图去重建逐出内存的block |
注意:在Python中,存储的对象都是被Pickle库序列化的,因此不需要关心你是否选择了一个序列化级别。
Spark也会自动持久化一些shuffle操作(如reduceByKey
)的中间数据,即使用户没有调用persis()
方法。这样做是为了避免,如果在shuffle的期间一个节点出错,再重新计算全部输入。我们仍然建议用户对结果RDD调用persis()
方法,如果他们准备再次使用。
Which Storage Level to Choose?
Spark存储打算提供内存利用率和CPU利用率之间不同的权衡。我们建议通过下面的过程选择一个合适的:
- 如果你的RDD很适合默认存储级别(MEMORY_ONLY
),那就选择默认。这是CPU利用率最高的,让RDDs上的操作尽可能的快。
- 如果不适合默认级别,试着使用MEMORY_ONLY_SER
,然后选择a fast serialization库提高对象的空间使用率,但是仍相当快的访问
- 除非计算数据集的函数花费很大,或者要过滤大量数据,不然不要存储到磁盘。否则,重新计算一个分区可能会和从磁盘中读取它一样慢
- 如果你需要更快的错误恢复(例如:如果使用Spark处理一个web app的请求),使用重复存储级别。所有的存储级别都可以使用重新计算丢失的数据提供完全容错,但是重复存储级别让你不必等待重新计算一个丢失的分区,就可以在RDD上继续运行任务。
- 在大内存或多应用的环境中,实验性的OFF_HEAP
模式有几个优势:
- 它允许运行多个executors来共享Tachyon中同一个内存池
- 它明显的减少GC的开销
- 如果单个executors崩溃,缓存数据不会丢失
Removing Data
Spark自动监控每个节点的使用情况,然后使用最少使用原则(least-recently-used)LRU删除老数据分区。如果你想手动删除一个RDD,来替代等待它被缓存放弃,使用RDD.unpersis()
方法。
Shared Variables
通常,当一个传递给Spark操作(例如map
或reduce
)的函数在远程集群节点上运行的时候,Spark操作工作在 函数中使用变量的独立副本。这些变量被复制到每台机器上,而且远程机器上变量的更新不会传递回驱动程序。通常,在任务间读-写分享变量是低效的。然而,Spark还是为两个常见的使用模式提供了两种有限的分享变量类型:广播变量broadcast variable 和 累加器accumulators。
Broadcast Variables
广播变量允许程序员在每个机器中缓存一个只读的变量,而不是随着任务一起传递它的拷贝。例如,利用广播变量,以一种高效的方式将一个大输入数据集的拷贝传递到每个节点。Spark也试图使用高效的广播算法来分发广播变量,为了减少通信成本。
使用SparkContext.broadcast(v)
从变量v
创建广播变量。广播变量是v
的一个包装,它的值可以通过调用value()
方法访问。下面的代码说明了这些:
scala> val broadcastVar = sc.broadcast(Array(1, 2, 3))
broadcastVar: org.apache.spark.broadcast.Broadcast[Array[Int]]
scala> broadcastVar.value
res0: Array[Int] = Array(1, 2, 3)
广播变量创建后,我们可以在集群上的函数中使用广播变量来替代v
值,不用再传递变量v到每个节点。另外,对象v在广播后不能被修改,为了确保所有节点获得相同的广播变量的值(例如,如果后来这个变量传递到一个新节点)。
Accumulators
累加器是一种只能通过关联操作进行”加”操作,因此它能高效的应用在并行操作中。它们能用来实现counters
(像在MapReduce)或sums
。Spark原生支持数值类型的累加器,而且程序员可以添加新类型的支持。如果创建有名字的累加器,它们可以显示在Spark的UI。这有利于理解运行阶段的过程(注意:Python中还没支持)。
可以通过调用SparkContext.accumulator(v)
从初始值v
创建一个累加器。运行在集群上的任务就可以添加值给它,通过使用add方法或+=
操作符(在Scala和Python中)。然而,它们不能读取它的值。只有驱动程序才能读取累加器的值,使用它的value()
方法。
下面的代码,展示了如何利用累加器合计一个数组中的元素:
scala> val accum = sc.accumulator(0, "My Accumulator")
accum: spark.Accumulator[Int] = 0
scala> sc.parallelize(Array(1, 2, 3, 4)).foreach(x => accum += x)
...
10/09/29 18:41:08 INFO SparkContext: Tasks finished in 0.317106 s
scala> accum.value
res2: Int = 10
虽然这段代码使用了Int类型累加器的内建支持,但是程序员也可以使用子类化AccumulatorParam创建他们自己的累加器类型。AccumulatorParam接口有两个方法:zero
为你的数据类型提供一个”0值”;addInPlace
方法把两个值相加。例如,假设我们有一个vector
class代表数学上的向量,我们可以这样写:
object VectorAccumulatorParam extends AccumulatorParam[Vector] {
def zero(initialValue: Vector): Vector = {
Vector.zeros(initialValue.size)
}
def addInPlace(v1: Vector, v2: Vector): Vector = {
v1 += v2
}
}
// Then, create an Accumulator of this type:
val vecAccum = sc.accumulator(new Vector(...))(VectorAccumulatorParam)
在Scala中,Spark也支持使用更通用的Accumulable接口来累积数据,结果类型不同于累加的元素类型(例如:聚集元素建立一个列表),而且SparkContext.accumulableCollection
方法累加常见的Scala集合类型。
因为累加器的更新只能在action中执行(action only),Spark保证每个任务对累加器的更新只能应用一次,即重启的任务不会更新这个值。在transformations中,用户会意识到如果tasks或jobs阶段被重新执行,每个任务的更新会应用多次。
累加器不会改变Spark的延迟计算模型。如果他们被一个在RDD上操作更新,它们的值只更新一次,作为一个action的一部分RDD被计算。因此,当使用一个延迟transformation-像map()
,累加器的更新不能保证被执行 。下面的代码片段演示了这个性质:
val acc = sc.accumulator(0)
data.map(x => acc += x; f(x))
// Here, acc is still 0 because no actions hava cause the 'map' to be computed.
==Important:==
- Accumulators的值只有在action后才会被更改,符合Spark的设计哲学-lazy evaluation
Deploying to a Cluster
Application submission guide描述了怎样提交应用到集群中。简而言之,一旦你打包你的应用成JAR(Java/Scala)或一系列.py或.zip文件,bin/spark-submit
脚本让你可以提交它到任何集群管理。
Unit Testing
Spark对使用任何流行的单元测试框架进去单元测试都是友好的。在你的测试中,简单的使用master URL设置到local
来创建一个SparkContext
,运行你的操作,然后调用SparkContext.stop()
撕掉它。为了确保你停止了context,使用finally
代码块或者测试框架的tearDown
方法,因为Spark不支持两个context在同一个程序中并行运行。
Migrating from pre-1.0 Versions of Spark
Where to Go from Here
可以阅读example Spark programs。另外,Spark包含几个样例在examples目录(Scala, Java, Python)。你可以运行Java和Scala样例,通过传递class name到bin/run-example
脚本;实例:
./bin/run-example SparkPi
Python实例,使用spark-submit
替代:
./bin/spark-submit examples/src/main/python/pi.py
为帮助程序优化,configuration和tuning指南提供了最好的实践信息。它们对确保你的数据以有效的格式存储在内存特别的重要。为了帮助部署,cluster mode overview描述分布操作相关的组件和支持的集群管理。
最后,完整的可用API文档Scala, Java, Python。