说明
这是对Spark2.0.1的Spark Programming Guide的翻译,翻译它是想让自己静心看下去,英语水平也不高,所以有的地方难免出错,另外,翻译中的某些地方加入了自己的理解,可能就多添了一句,以便于理解。
综述
在一个高层次来说,每一个Spark应用程序都会包含driver程序(运行用户main函数的程序)和在集群上执行各种各样的并行操作。Spark提供的主要抽象是RDD(弹性的分布式数据集),RDD是可以在集群节点上可以被并行操作的分区的元素集合。RDD可以由HDFS或者其他支持hadoop文件系统的文件中的数据产生,也可以由存在于driver程序中的集合产生,并且可以被转化。用户可以要求Spark把RDD持久化到内存,这样就可以更加高效地重新利用。最后,RDD可以自动地从失败的节点中得以恢复。
Spark的第二个抽象是共享变量,它也可以被用到并行操作中。默认地,当Spark在很多节点运行一个并行函数的任务tasks时,它把这个变量的副本传到每一个任务task中去。有时候一个可能会跨task共享,或者在task和driver程序中共享。Spark支持两种类型的共享变量,广播变量broadcast variables(它可以在所有节点的内存中保持一个不变的值)和累加器accumulators(这是一种只能被加的变量,比如计数器和求和)。
指南将会讲述Spark的这些特征。边阅读指南边启动Spark的交互式脚本(bin/spark-shell)是最简单的学习方式。
连接Spark
Spark2.0.1的建立和分布式的工作默认是基于scala2.11版本的。当然spark也可以基于其他的scala版本。为了用scala写一个Spark的应用程序,应该用一个可以兼容的Scala版本,比如2.11.X。
为了写Spark的应用程序,你应该在Spark中添加Maven的依赖。Spark可以在Maven中央仓库的下面这个地方获取到:
groupId = org.apache.spark
artifactId = spark-core_2.11
version = 2.0.1
另外,如果你想连接到一个HDFS集群,你需要在hadoop-client中添加要连接的所需版本的HDFS依赖:
groupId = org.apache.hadoop
artifactId = hadoop-client
version = <your-hdfs-version>
最后,你需要引入一些Spark的类到程序中。添加下面的内容:
import org.apache.spark.SparkContext
import org.apache.spark.SparkConf
(在Spark1.3.0之前,需要明确引用import org.apache.spark.SparkContext._
来激活所有可能用到的转化。)
初始化Spark
Spark程序首先要做的事就是创建SparkContext
对象,它会告诉Spark怎样去连接到集群。在创建SparkContext
对象之前要建立一个SparkConf
对象,它包含的是你的应用程序的信息。
在每一个JVM只能有一个SparkContext
可以被激活。在创建一个新的SparkContext
对象之前必须要先用stop()
命令停止。
val conf = new SparkConf().setAppName(appName).setMaster(master)
new SparkContext(conf)
在上面出现的参数appName
是Spark UI界面上出现的应用程序的名字。参数master
写的应该是Spark,Mesos或者YARN集群的主节点的URL,或者就是一个特定的表示以本地模式运行的字符串“local”
。在实际编码中,你不会愿意master
以硬编码的方式出现在程序中,而是更愿意以spark-submit
启动应用程序且在此接受master
参数。然而,对于本地测试或者单元测试来说,是可以传入“local”
的。
利用shell
在Spark-shell中,一个内部存在的解释器SparkContext早已创建,唤作sc
。自己创建的SparkContext将不会工作。你可以使用--master
参数设置sc连接哪个master,也可以使用--jars
参数添加以逗号分隔的jar包列表。可以使用--packages
参数提供一些maven坐标的依赖包(比如Spark Packages)。有依赖存在的额外的库也可以通过--repositories
传输。比如用4个核运行bin/spark-shell
:
$ ./bin/spark-shell --master local[4]
或者,再把一个code.jar的jar包添加到它的路径上:
$ ./bin/spark-shell --master local[4] --jars code.jar
为了用maven坐标包含一个依赖:
$ ./bin/spark-shell --master local[4] --packages "org.example:example:0.1"
查看完整的选项列表可以运行run-shell --help
。其实spark-shell激活了更通用的spark-submit
脚本
弹性的可分布式数据集(RDDs)
Spark围绕弹性分布式数据集RDD的概念展开,它是一个能够并行操作的可容错的元素集合。有两个方式可以创建RDD:在驱动程序中并行化已存在的集合,或者引用一个存在的外部存储系统中的数据集,比如HDFS,HBASE或其它支持hadoop输入格式的数据源。
并行化集合
并行化集合是在驱动程序中对存在的集合调用SparkContext的parallelize
函数创建的。集合中的元素复制到可以被并行操作的分布式数据集中。比如,下面展示了如何创建1到5的并行化的数据集合:
val data = Array(1, 2, 3, 4, 5)
val distData = sc.parallelize(data)
一旦被创建,分布式数据集(distData
)就可以并行操作。比如,我们可以调用distData.reduce((a,b)=>a+b)
对数组中的所有元素求和。接下来我们将描述基于分布式数据集的操作。
对于并行集合一个重要的参数是把数据集切成几份的分区数partitions
。在集群上的每一个分区Spark都会运行一个任务task。一般来说,集群上每个CPU占有2~4个分区比较好。正常的情况下,Spark将会根据集群的情况自动地设置分区数。然而你也可以手动在parallelize第二个参数处设置分区数,sc.parallelize(data,10)
。注意:一些地方用术语slices(partitions的同义词)是为了保持向后兼容。
外部数据集
Spark可以从任何Hadoop支持的存储源创建分布式数据集,包括本地文件系统,HDFS,Cassandra,HBase,Amazon S3等等。Spark支持文本文件,序列化文件,或者任何支持Hadoop InputFormat输入格式的文件。
文本文件RDD可以使用SparkContext
的textFile
函数创建。这个函数以一个URI作为参数(机器本地路径,或者hdfs://,s3n://等等),并且把他读为行的集合。下面是一个调用的例子:
scala> val distFile = sc.textFile("data.txt")
distFile: org.apache.spark.rdd.RDD[String] = data.txt MapPartitionsRDD[10] at textFile at <console>:26
一旦被创建,distFile能够数据集操作所激活。比如,我们可以用map
和reduce
操作计算每行长度之和:distFile.map(s => s.length).reduce((a, b) => a + b).
用Spark读文件时有一些需要注意的地方:
- 如果读取的是本地文件,文件必须在worker节点同样的路径上是可以获取到的。或者复制文件到所有的节点上,或者用网络文件系统。
- 所有基于Spark文件的输入方法,包括
textFile
,都支持运行文件夹、压缩文件和通配符表示的文件。比如,可以用textFile("/my/directory")
,textFile("/my/directory/*.txt")
,textFile("/my/directory/*.gz")
. - textFile方法同样有第二个控制分区数的参数。默认情况下,Spark为文件的每一个块创建一个分区(在HDFS中块默认是64MB),但是可以传入更大的值使分区数更多。但是不能比块数blocks更少。
除了文本文件,Spark Scala的API也提供了几种其他的数据格式:
-
SparkContext.wholeTextFiles
可以让你读一个包含多个小文件的文件夹,并且以(filename,content)
对的形势返回。这和每个文件每行一条记录的textFile形成了对比。 - 对于序列化文件,用
SparkContext
的sequenceFile[K,V]
方法,其中K和V是文件中key和value的类型。K和V应该是Hadoop Writable
接口的子类,比如IntWritable
和Text
。另外,对于一小部分常见的Writables,Spark允许指定本地类型,比如,sequenceFile[Int,String]
将会自动地读取IntWritable
和Text
类型的内容。 - 对于其他的Hadoop的输入格式,你可以使用
SparkContext.hadoopRDD
方法,它接收一个任意的JobConf
参数,输入格式类,key的类,value的类。设置这些参数的方式和在Hadoop job中的设置是一样的。你也可以用基于新MapReduce API(org.apache.hadoop.mapreduce
)的SparkContext.newAPIHadoopRDD
。 -
RDD.saveAsObjectFile
和SparkContext.objectFile
支持以包含序列化java对象的简单格式保存一个RDD。尽管效率不如像Avro的特定格式的方式,但是却提供了一种保存任何RDD的简单方式。
RDD操作
RDD支持两种类型的操作:transfornations–从已存在的RDD创建新的数据集;actions–对一个数据集计算之后得到一个值并返回到驱动程序。比如,map
就是一个transformation
操作,它把数据集中的每个元素经过函数处理得到新的表示结果的RDD。另一边,reduce
就是一个action
操作,它可以用函数聚集RDD中的所有元素并且返回最终的值到驱动程序中(但是reduceByKey
返回的是一个新的分布式数据集)。
在Spark中所有的transformation
操作都是惰性lazy
的,意思就是不会立刻计算结果。相反地,它们会记住施加于原始数据集(比如一个文件)的所有transformation
操作。这些transformation
操作只有遇到action
的时候才会真正的计算。这个设计使Spark更加高效。比如,我们可以想到,经过map
和reduce
的数据集,最后只会返回结果到driver中,而不是更大的map
操作之后的数据集。
默认情况下,在每一次运行一个action
的时候,每个已经transformation
操作后的RDD都将会重新计算。然而,你可以用persist
或者cache
方法持久化到内存中,在这种情况下,当你下一次查询时Spark将会获取的更快。这也可以持久化到硬盘上,或者多节点持久化备份。
基础
为了讲述RDD的基本概念,考虑下面简单的程序:
val lines = sc.textFile("data.txt")
val lineLengths = lines.map(s => s.length)
val totalLength = lineLengths.reduce((a, b) => a + b)
第一行由外部文件定义了一个基本的RDD。这个数据集没有加载到内存,另外lines
只是一个纯粹的指向文件的指针。第二行的lineLengths
是transformation
转化操作map
的结果。再一次强调,因为惰性的特性,lineLengths
不被立即计算。最后,我们运行action
操作reduce
。这时候,把计算分派到每个任务去在每个机器上运行,并且每个机器的map
操作只操作数据集中的一部分和做本地归约reduce
操作,它的值返回到驱动程序中。
如果不久后还会用到lineLengths
,可以这样做:
lineLengths.persist()
在reduce
操作之前当它第一次被计算时,lineLengths
将会保存到内存。
为Spark传函数
Spark API严重依赖于从驱动程序到集群的传函数。有两个方式值得推荐:
- 匿名函数语法,只用简短的代码片就可以。
- 在全局单例对象中的静态方法。比如可以定义个
object MyFunctions
,然后传MyFunctions.func1
,如下所示:
object MyFunctions {
def func1(s: String): String = { ... }
}
myRdd.map(MyFunctions.func1)
注意,尽管可以传入一个类实例中的函数(这与单例对象形成了对比),但是这就会发送包含此方法的整个类对象到集群中。比如下面的例子:
class MyClass {
def func1(s: String): String = { ... }
def doStuff(rdd: RDD[String]): RDD[String] = { rdd.map(func1) }
}
在此,如果我们创建一个新的MyClass
实例,并且调用doStuff
函数,函数体中的map
操作里面就会有MyClass
类实例的方法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)
}
理解Spark闭包
当在集群上运行程序时,关于Spark的一个比较难的问题就是理解变量和方法的生命周期。RDD对它们范围之外的变量的修改操作是一个经常的混乱源头。在下面的例子中,我们将看到foreach计算累加的代码会出现这样的问题,但是其他的操作也会出现这样的问题。
例子
考虑下面RDD元素的求和,它的结果会因是不是运行在同一个JVM而不同。一个常用的例子是本地模式(–master = local[n])和集群模式(通过spark-submit提交到YARN)的对比。
var counter = 0
var rdd = sc.parallelize(data)
// Wrong: Don't do this!!
rdd.foreach(x => counter += x)
println("Counter value: " + counter)
本地模式和集群模式
上面代码运行行为是不能确定的,也许并不能得到期望的结果。为了执行作业job,Spark把RDD的计算操作分散到每个task,每个task都会有executor执行。在执行之前,Spark先计算每个task的闭包。闭包是一些变量和方法,这些变量和方法对执行RDD计算任务(代码中的foreach()
)的executor是可见的。闭包是序列化的,并且被传到每个executor中。
传到executor中的闭包内的变量都是副本,所以在foreach
函数内被引用的counter
,它再也不是驱动程序中的counter
了。在驱动程序的内存中一直有一个counter
变量,但是它不是executor看到的那个!executor只能看到这个序列化闭包的副本。因此,counter
的最终值将会是0,因为对counter
的操作都是施加在各个复制到executor的序列化闭包上。
在本地模式的一些场景中,执行计算的JVM和driver程序是一样的,并且指向的都是同一个counter
,也许就会计算出正确的结果。
为了确保各种场景中能够有一致的行为,应该选用Accumulator
。Accumulator
在Spark中被特定的用于提供安全更新变量的机制。Accumulator
的章节将会进行更详细的讲述。
一般,类似于循环或本地方法的闭包不应该用于改变的某些变量的全局状态。Spark没有定义也不能保证对于闭包外对象的改变行为。在本地模式中一些代码的确可以正常工作,但是这只是偶然,并且在分布式环境中肯定不会得到期待的结果。如果某些变量需要全局聚集,那么最好用Accumulator
。
打印RDD中的元素
另一个常见的说法是,用rdd.foreach(println)
或rdd.map(println)
打印RDD的元素。在单一机器上,这会得到期望的结果并且打印出RDD中的所有元素。然而,在集群模式中,这就会输出到executors调用的stdout
,而不是driver的stdout
,所以driver的stdout
不会显示。为了在driver中打印所有的元素,首先应该用collect
方法把RDD收集到driver节点,rdd.collect().foreach(println).
但这个可能会引起driver节点出现超出内存的错误,因为collect()
把整个RDD都弄到一台机器上了。如果只是想打印一部分元素,更安全的方法应该是take():rdd.take(100).foreach(println)
。
key-value对操作
尽管多数Spark操作都是对包含任何类型对象的RDD进行操作,但还是有一部分操作只能作用于key-value对。多数这样的操作都会shuffle,比如通过key对元素进行grouping或者aggregating。
在Scala中,对于包含Tuple2
类型(scala中的内建类型,简单地(a,b)这样写就可以被创建))对象的这些操作会自动地获取到。KV操作可以在PairRDDFunctions
类中获取,这是对tuple RDD的自动封装。
举个例子,下面的代码用reduceByKey
操作施加于kv对,去计算文件中每一行出现了多少次:
val lines = sc.textFile("data.txt")
val pairs = lines.map(s => (s, 1))
val counts = pairs.reduceByKey((a, b) => a + b)
另外,我们还可以用counts.sortByKey()
按照字母表排序,最后counts.collect()
是以对象数组的形式返回到driver程序中。
注意:当用自定义对象作为key时,必须保证自定义的equals()方法伴随着一个匹配的hashCode()
方法。全部细节查看Object.hashCode()文档。
转化操作transformation
下面的表格列出了一些常用的Spark支持的transformation
操作。更为详细的介绍参考RDD API和pair RDD函数文档。
Action操作
下面列表中是一些常用的action
操作。更为详细的介绍参考RDD API和pair RDD函数文档。
Spark RDD API 同样提供了某些action
的异步版本,比如foreach
的异步版本是foreachAsync
,foreachAsync
将立刻返回给调用者一个FutureAction
结果对象,但是foreach
会一直阻塞直到前一个action
完成。这可以用于管理或等待action
的异步执行。
shuffle操作
某些Spark的操作将会触发一个叫做shuffle
的操作。shuffle是一种重新分配数据的机制,以便于能够跨分区有效分组。一般都是在跨executor或跨机器的复制,这使shuffle成为复杂且耗时的操作。
背景
为了理解在shuffle过程中发生了什么。我们以reduceByKey
操作作为例子。reduceByKey
操作产生了一个新的RDD,在这个新RDD中每个key只会出现一次且和key对应的所有value值都经过了聚集函数的执行处理。挑战是,和一个key对应的所有的value并不一定都在同一个分区,甚至不在同一台机器,但是它们必须得一起计算这个最终结果。
在Spark中,数据一般不会为一个特定的操作跨分区地放在一个必要的位置上。在计算期间,一个任务将操作一个分区,因此为某一个reduceByKey
的任务组织所有的数据去运行时,Spark需要执行多对多的操作。Spark必须从所有分区读到所有的key对应的所有value值,然后把它们按照key聚集起来计算最终的结果,这就是shuffle过程。
尽管拥有新shuffle后数据的每个分区的元素集合是确定的,而且分区本身的顺序也是确定的,但是元素的顺序却不是确定的。如果想在shuffle之后得到有序的数据,那么可能会用到下面这些方法:
-
mapPartitions
去给每一个分区排序,比如,.sorted
。 -
repartitionAndSortWithinPartitions
去在给数据重新分区的同时也高效的为分区内的数据排序。 -
sortBy
去构建全域的有序RDD。
能够引起shuffle的操作有:repartition类的操作,比如repartition
和coalesce
;ByKey操作(除了计数),比如groupByKey
和reduceByKey
;join类的操作,比如cogroup
和join
。
性能影响
shuffle是一个非常昂贵的操作,因为它包含了硬盘IO、数据序列化和网络IO操作。为了管理shuffle的数据,Spark产生一系列的任务,map任务用来组织数据,reduce任务用来聚集数据。这个术语来自于MapReduce,但是并不直接与Spark map和reduce操作有关。
从内部上来说,各个map任务的结果将会保存在内存中,直到内存不能放下为止。然后,这些结果将会基于目标分区排序,并且写到一个文件上。在reduce端,任务将会读取相关的排序好的块。
某些shuffle操作将会消耗大量的堆内存,因为它们将会在转化操作之前使用内存中的数据结构去管理记录。特别地,reduceByKey
和aggregateByKey
在map端创建了这些数据结构,并且ByKey
操作在reduce端产生最终的数据结构。当内存中放不下这些数据时,Spark将会分隔数据到磁盘中,这回导致额外的硬盘IO开销和GC的增长。
shuffle将会在磁盘上产生大量的中间级的文件。自Spark1.3起,这些文件将会一直保存,直到相关RDD不再使用或者它们被GC回收。这样做的原因是,即使血统被重新计算,shuffle文件也不会被重新创建。如果应用程序保持了对RDD的引用或者GC经常不起作用,那么垃圾回收机制GC可能经过挺长时间才会发生。如果GC好长时间没有发生,这就意味着长时间运行的Spark job可能消耗大量的磁盘空间。当配置Spark的时候可以通过spark.local.dir
参数指定临时的存储目录。
shuffle的行为可以通过配置大量的参数来优化。详情请查看Spark Configuration Guide的Shuffle Behavior章节。
RDD持久化
在Spark中最重要的技能之一就是在内存中跨操作的持久化。当持久化一个RDD的时候,每个节点都储存了它的任何分区以便于在内存中计算并且在其他的action操作重用。这使得之后的action的计算速度可以提高10倍。持久化是一个便于迭代和更快交互使用的重要工具。
你可以使用persist()
或者cache()
方法标记想要持久化的RDD。执行action触发第一次计算的时候,它将会保持到节点的内存之中。Spark的缓存机制是容错的,如果RDD的任何分区丢失的话,就会用一开始创建它的转化操作自动重新计算丢失的RDD分区。
另外,每个持久化RDD可以用不同的存储级别,比如持久化到磁盘,以java序列化对象的方式持久化到内存,或者跨节点备份。这些级别的设置都可以通过传入StorageLevel
对象到persist()
设置。cache()是persist()默认存储级别情况下的缩写,也就是StorageLevel.MEMORY_ONLY
(在内存中存储的非序列化对象)。存储级别的全部设置如下:
在shuffle操作(比如reduceByKey
)中,即使没有用户调用persist
,Spark也可以自动持久化一些中间数据。这样做是为了,在shuffle过程中如果一个节点失败,那就会避免重新计算整个输入。如果用户重用某个结果RDD,我们建议调用persist
.
选择哪一个存储级别呢?
Spark存储级别意味着在内存占用和CPU效率之间做了不同的折中。我们推荐按照下面的程序选择:
- 如果默认存储级别下RDD可以从容存放,那就默认。这是CPU效率最高的选择,会使施加于此RDD的操作尽可能快。
- 默认情况下如果不可以从容存放,那就试一下
MEMORY_ONLY_SER
,并且选择一个快速序列化库,使得对象的存储节省许多空间,但同时,速度也还可以。(java
and scala) - 当函数计算数据集花销很大的时候,或者过滤大量的数据的时候,RDD分区数据就分割到硬盘上。否则,重新计算一个分区就会和从硬盘读它一样快。
- 如果你想快速容错恢复(比如用Spark去请求一个web应用),那就使用冗余存储级别。所有的存储级别都可以通过重新计算丢失的数据来达到容错,但是冗余存储级别将会使程序在不用等待重新计算而一直运行。
移除数据
Spark自动监测节点缓存的使用,并且以LRU原则丢弃老的数据分区。如果你想手动移动持久化的RDD而不是等待那个缓存满,就用RDD.unpersist()
方法吧。
共享变量
正常情况下,当被传到Spark操作里面(比如map
或者reduce
)的一个函数在集群节点上执行,在这个函数用到的变量都是相互独立的副本。这些变量被复制到每台机器上,并且在集群节点上返回到driver程序的变量不会被更新。一般情况下,支持跨任务对共享变量的读写将是低效的。然而,Spark为两种常用用法提供了两种类型的共享变量,广播变量brocast variables
和累加器accumalator
。
广播变量
广播变量允许编程者在每台机器上的内存中保持一个只读的变量,而不是在每个任务内都有一个副本。比如说,它们可以以有效的方式给每个节点一个大输入数据的副本。Spark也尽量用有效的广播算法分散广播变量,以期减少通信消耗。
Spark action是通过一系列的被shuffle过程分割的stages来执行的。在每个stage,Spark会自动的广播被每个任务需要的共用数据。这样被广播的数据是以序列化形式被广播的,在运行每个任务之前还要进行反序列化。这意味着明确创建广播变量只在两处是有用的,不同stage需要同一份数据,或者以非序列化的形式被cache是非常重要的。
从一个变量v
创建广播变量需要调用SparkContext.broadcast(v)
。广播变量封装了v
,它的值可以通过调用value
方法得到。就如下面代码展现的一样。
scala> val broadcastVar = sc.broadcast(Array(1, 2, 3))
broadcastVar: org.apache.spark.broadcast.Broadcast[Array[Int]] = Broadcast(0)
scala> broadcastVar.value
res0: Array[Int] = Array(1, 2, 3)
在广播变量被创建之后,它可以被运行在集群上的所有函数使用,而不是直接使用变量v
,所以变量v
传到某个节点的次数不多于1次。另外,对象v
在广播之后不应该被修改,以确保所有的节点都接收到相同的广播变量(比如这个变量之后被广播到一个新的节点)。
累加器
累加器是这样一些变量,它们只能通过可加性和可交换性的操作实现累加,因此可以有效地支持并行。它们能够用于实现计数器和求和。Spark自带地支持数字类型的累加器,并且编程者可以增加支持一些新的类型。
如果一个累加器是带着名字被创建的,那它们就会显示在Spark UI页面上。这对于理解不同阶段运行过程是有用的。
数字类型的累加器可以通过调用SparkContext.longAccumulator()
或者SparkContext().doubleAccumulator()
来创建,这样就可以累加Long或者Double类型的数值。运行在集群上的任务可以使用add
方法累加数值。然而,任务不能读值。只有driver程序才可以使用value
方法读累加器的值。
下面的代码展示了累加器是怎样累加数组中的元素。
scala> val accum = sc.longAccumulator("My Accumulator")
accum: org.apache.spark.util.LongAccumulator = LongAccumulator(id: 0, name: Some(My Accumulator), value: 0)
scala> sc.parallelize(Array(1, 2, 3, 4)).foreach(x => accum.add(x))
...
10/09/29 18:41:08 INFO SparkContext: Tasks finished in 0.317106 s
scala> accum.value
res2: Long = 10
尽管上面的代码使用的是内建的累加器类型,但编程者也可以通过继承子类AccumulatorV2来创建自己的类型。抽象类AccumulatorV2有几个方法需要去重写,reset
方法是把累加器重置为0,add
方法是把另一个值加到累加器,merge
方法是把另一个同样类型的累加器合并到这个累加器。其它需要重写的方法可以参考Scala API文档。比如说,假设我们有一个MyVector
类,代表了一个数学向量,那我们就可以这样写:
object VectorAccumulatorV2 extends AccumulatorV2[MyVector, MyVector] {
val vec_ : MyVector = MyVector.createZeroVector
def reset(): MyVector = {
vec_.reset()
}
def add(v1: MyVector, v2: MyVector): MyVector = {
vec_.add(v2)
}
...
}
// Then, create an Accumulator of this type:
val myVectorAcc = new VectorAccumulatorV2
// Then, register it into spark context:
sc.register(myVectorAcc, "MyVectorAcc1")
注意:当编程者定义自己的AccumulatorV2类型时,结果类型可以是与被添加元素的类型不一致。
对于内部的累加器更新行为只会执行一次,Spark保证每个任务对累加器的更新将只会发生一次,也就是说重启的任务不会再一次更新这个值。在transformation操作中,用户应该知道如果任务task或者job stage被重新计算了,那每个任务的更新操作就会再次发生,但是累加器的再一次更新却不会发生。
累加器不会改变Spark的lazy特性。如果累加器在一个RDD的操作中更新,那么累加器的值只有碰到action操作的时候才会被唯一的执行。因此,在转化操作中的累加器更新并不保证被执行,下面的代码片段阐述了这个属性:
val accum = sc.longAccumulator
data.map { x => accum.add(x); x }
// Here, accum is still 0 because no actions have caused the map operation to be computed.