http://spark.apache.org/docs/latest/rdd-programming-guide.html#using-the-shell
Overview(概述)
在较高的层次上,每个Spark应用程序都包含一个驱动程序,该程序运行用户的主要功能并在集群上执行各种并行操作。 Spark提供的主要抽象是弹性分布式数据集(RDD),它是跨群集节点分区的元素集合,可以并行操作。 RDD是通过从Hadoop文件系统(或任何其他Hadoop支持的文件系统)中的文件或驱动程序中的现有Scala集合开始并对其进行转换来创建的。用户还可以要求Spark在内存中保留RDD,允许它在并行操作中有效地重用。最后,RDD会自动从节点故障中恢复。 Spark中的第二个抽象是可以在并行操作中使用的共享变量。默认情况下,当Spark并行运行一个函数作为不同节点上的一组任务时,它会将函数中使用的每个变量的副本发送给每个任务。有时,变量需要跨任务共享,或者在任务和驱动程序之间共享。 Spark支持两种类型的共享变量:广播变量(可用于在所有节点的内存中缓存值)和累加变量(仅“添加”到这些变量中),例如计数器和和。 本指南以Spark支持的每种语言显示了这些功能。如果你启动Spark的交互式shell,最简单的方法是使用它 - 用于Scala shell的bin / spark-shell或用于Python的bin / pyspark
与Spark链接
默认情况下,Spark 2.4.0被构建并分发给Scala 2.11。(Spark也可以构建到与其他版本的Scala协同工作。)要使用Scala编写应用程序,您需要使用兼容的Scala版本(例如2.11.X)。要编写Spark应用程序,需要在Spark上添加Maven依赖项。Spark可以通过Maven Central访问:
groupId = org.apache.spark
artifactId = spark-core_2.11
version = 2.4.0
此外,如果希望访问HDFS集群,需要为HDFS版本增加hadoop-client的依赖性。
groupId = org.apache.hadoop
artifactId = hadoop-client
version = <your-hdfs-version>
表示。最后,您需要将一些Spark类导入到您的程序中。添加以下代码行:
import org.apache.spark.SparkContext org.apache.spark.SparkConf
在Spark 1.3.0之前,您需要显式地导入org.apache.spark.SparkContext._启用基本隐式转换。)
Initializing Spark(spark的初始化)
Spark程序必须做的第一件事是创建一个SparkContext对象,它告诉Spark如何访问集群。要创建SparkContext,首先需要构建一个包含有关应用程序信息的SparkConf对象。 每个JVM只能激活一个SparkContext。您必须在创建新的SparkContext之前停止()活动的SparkContext。
val conf = new SparkConf().setAppName(appName).setMaster(master)
new SparkContext(conf)
appName参数是应用程序在集群UI上显示的名称。 master是Spark,Mesos或YARN群集URL,或者是以本地模式运行的特殊“本地”字符串。实际上,当在群集上运行时,您不希望在程序中对master进行硬编码,而是使用spark-submit启动应用程序并在那里接收它。但是,对于本地测试和单元测试,您可以传递“local”来运行Spark in-process。
Spark围绕弹性分布式数据集(RDD)的概念展开,RDD是可并行操作的容错元素集合。创建RDDs有两种方法:在驱动程序中并行化现有集合,或在外部存储系统中引用数据集,例如共享文件系统、HDFS、HBase或任何提供Hadoop InputFormat的数据源。
scala> val distFile = sc.textFile("data.txt")
distFile: org.apache.spark.rdd.RDD[String] = data.txt MapPartitionsRDD[10] at textFile at <console>:26
val data = Array(1, 2, 3, 4, 5)
val distData = sc.parallelize(data)
Using the Shell(使用shell)
在Spark shell中,已经在名为sc的变量中为您创建了一个特殊的解释器感知SparkContext。制作自己的SparkContext将无法正常工作。您可以使用--master参数设置上下文连接到哪个主服务器,并且可以通过将逗号分隔的列表传递给--jars参数来将JAR添加到类路径中。您还可以通过向--packages参数提供以逗号分隔的Maven坐标列表,将依赖项(例如Spark包)添加到shell会话中。可能存在依赖关系的任何其他存储库(例如Sonatype)可以传递给--repositories参数。例如,要在四个核心上
$ ./bin/spark-shell --master local[4]
或者,要将code.jar添加到其类路径,请使用:
$ ./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调用更普遍
弹性分布式数据集(RDD)Resilient Distributed Datasets
Spark围绕弹性分布式数据集(RDD)的概念展开,RDD是可并行操作的容错元素集合。创建RDDs有两种方法:在驱动程序中并行化现有集合,或在外部存储系统中引用数据集,例如共享文件系统、HDFS、HBase或任何提供Hadoop InputFormat的数据源。
Parallelized Collections(并行化集合)
通过在驱动程序(Scala Seq)中的现有集合上调用SparkContext的parallelize方法来创建并行化集合。复制集合的元素以形成可以并行操作的分布式数据集。例如,以下是如何创建包含数字1到5的并行化集合:
val data = Array(1, 2, 3, 4, 5)
val distData = sc.parallelize(data)
一旦创建,分布式数据集(distData)可以并行操作。例如,我们可以调用distData。reduce((a, b) => a + b)用于将数组的元素相加。稍后我们将描述对分布式数据集的操作。
并行集合的一个重要参数是要将数据集切割成的分区数。Spark将为集群的每个分区运行一个任务。通常,您需要为集群中的每个CPU分配2-4个分区。通常,Spark尝试根据集群自动设置分区数量。但是,您也可以通过passi手动设置它(例如sc.parallelize(data,10))。注意:代码中的某些位置使用术语切片(分区的同义词)来保持向后兼容性。
External Datasets(外部数据集合)
Spark可以从Hadoop支持的任何存储源创建分布式数据集,包括本地文件系统,HDFS,Cassandra,HBase,Amazon S3等.Spark支持文本文件,SequenceFiles和任何其他Hadoop InputFormat。 可以使用SparkContext的textFile方法创建文本文件RDD。此方法获取文件的URI(计算机上的本地路径,或hdfs://,s3a://等URI)并将其作为行集合读取。这是一个示例调用:
scala> val distFile = sc.textFile("data.txt")
distFile: org.apache.spark.rdd.RDD[String] = data.txt MapPartitionsRDD[10] at textFile at <console>:26
一旦创建,数据集操作就可以对distFile进行操作。例如,我们可以使用映射将所有行的大小相加,并按如下方式减少操作:distFile.map(s = > s.length)。
关于使用Spark读取文件的一些注意事项:
如果使用本地文件系统上的路径,文件也必须在工作节点上的同一路径上可访问。要么将文件复制到所有工作者,要么使用一个挂载网络的共享文件系统。
所有Spark基于文件的输入方法,包括文本文件,支持在目录、压缩文件和通配符上运行。例如,可以使用textFile(“/my/directory”)、textFile(“/my/directory/*.txt”)和textFile(“/my/directory/*.gz”)。
textFile方法还接受一个可选的第二个参数,用于控制文件的分区数量。默认情况下,Spark为文件的每个块创建一个分区(HDFS中的块默认为128MB),但是您也可以通过传递一个较大的值来请求更多的分区。注意,分区不能少于块。
除了文本文件,Spark s Scala API还支持其他几种数据格式:
SparkContext。wholeTextFiles允许您读取包含多个小文本文件的目录,并将它们作为(文件名、内容)对返回。这与textFile相反,textFile将在每个文件中每行返回一条记录。分区由数据局部性决定,在某些情况下,数据局部性可能导致分区太少。对于这些情况,wholetextfile提供了控制最小分区数量的第二个可选参数。
对于SequenceFiles,使用SparkContext的sequenceFile[K, V]方法,其中K和V是文件中的键和值的类型。这些应该是Hadoop可写接口的子类,比如IntWritable和Text。
此外,Spark还允许为一些常见的可写程序指定本机类型;例如,sequenceFile[Int, String]将自动读取IntWritables和text。对于其他Hadoop inputformat,您可以使用SparkContext。hadoopRDD方法,它接受任意的JobConf和输入格式类、键类和值类。将这些设置为与使用输入源的Hadoop作业相同的方式。您还可以使用SparkContext。基于"new"MapReduce API (org.apache.hadoop.mapreduce)的inputformat的newAPIHadoopRDD。
RDD.saveAsObjectFile 和SparkContext.objectFile支持以由序列化的Java对象组成的简单格式保存RDD。虽然这不如Avro这样的专用格式高效,但它提供了一种简单的方式来保存任何RDD。
RDD操作(RDD Operations)
RDD支持两种类型的操作:转换(从现有数据集创建新数据集)和操作(在数据集上运行计算后将值返回到驱动程序)。例如,map是一个转换,它通过一个函数传递每个数据集元素,并返回一个表示结果的新RDD。另一方面,reduce是一个使用某个函数聚合RDD的所有元素的操作,并将最终结果返回给驱动程序(尽管还有一个返回分布式数据集的并行reduceByKey)。
Spark中的所有转换都是懒惰的,因为它们不会立即计算结果。相反,他们只记得应用于某些基础数据集(例如文件)的转换。仅当操作需要将结果返回到驱动程序时才会计算转换。这种设计使Spark能够更有效地运行。例如,我们可以意识到通过map创建的数据集将用于reduce,并仅将reduce的结果返回给驱动程序,而不是更大的映射数据集。
默认情况下,每次对其执行操作时,都可以重新计算每个转换后的RDD。但是,您也可以使用持久化(persist)(或缓存(chche))方法在内存中保留RDD,在这种情况下,Spark会在群集上保留元素,以便在下次查询时更快地访问。还支持在磁盘上保留RDD,或在多个节点之间复制。
Basics(基础)
为了说明RDD的基础知识,请考虑下面的简单程序:
val lines = sc.textFile("data.txt")
val lineLengths = lines.map(s => s.length)
val totalLength = lineLengths.reduce((a, b) => a + b)
第一行从外部文件定义基础RDD。此数据集未加载到内存中或以其他方式操作:行仅仅是指向文件的指针。第二行将lineLengths定义为地图转换的结果。同样,由于懒惰,lineLengths不会立即计算。最后,我们运行reduce,这是一个动作。此时,Spark将计算分解为在不同机器上运行的任务,并且每台机器都运行其部分映射和本地缩减,仅返回其对驱动程序的答案。
如果我们以后还想使用linelength,我们可以添加:
在reduce之前,这将导致lineLengths在第一次计算之后保存在内存中。
向Spark传递函数(Passing Functions to Spark)
Spark的API在很大程度上依赖于在驱动程序中传递函数以在集群上运行。有两种建议的方法可以做到这一点:
匿名函数语法,可用于短片代码。
全局单例对象中的静态方法。例如,您可以定义对象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),它引用了所有这些。要避免此问题,最简单的方法是将字段复制到本地变量而不是从外部访问它:
def doStuff(rdd: RDD[String]): RDD[String] = {
val field_ = this.field
rdd.map(x => field_ + x) }
Understanding closures(了解闭包)
Spark的一个难点是在跨集群执行代码时理解变量和方法的范围和生命周期。修改其范围之外的变量的RDD操作可能经常引起混淆。在下面的示例中,我们将查看使用foreach()递增计数器的代码,但其他操作也可能出现类似问题。
例
考虑下面的native RDD元素总和,根据执行是否在同一JVM中发生,它可能表现不同。一个常见的例子是在本地模式下运行Spark(--master = local [n])而不是将Spark应用程序部署到集群(例如通过spark-submit to YARN):
var counter = 0
var rdd = sc.parallelize(data)
// Wrong: Don't do this!! 错误示例:不要这样做!
rdd.foreach(x => counter += x)
println("Counter value: " + counter)
本地与群集模式(Local vs. cluster modes)
上述代码的行为是未定义的,并且可能不按预期工作。为了执行jobs,Spark将RDD操作的处理分解为tasks,每个task由executor执行。在执行之前,Spark会计算 任务的闭合。闭包是执行器在RDD上执行计算时必须可见的那些变量和方法(在本例中是foreach())。这个闭包被序列化并发送给每个执行程序。
发送到每个执行程序的闭包中的变量现在都是副本,因此,在foreach函数中引用计数器时,它不再是驱动节点上的计数器。驱动节点的内存中仍然有一个计数器,但是执行程序不再可见!执行器只看到序列化闭包中的副本。因此,计数器的最终值仍然为零,因为计数器上的所有操作都引用了序列化闭包中的值。
在本地模式中,在某些情况下,foreach函数实际上将在与驱动程序相同的JVM中执行,并引用相同的原始计数器,并可能实际更新它。
为了确保这些场景中定义良好的行为,应该使用累加器。Spark中的累加器专门用于在集群中的工作节点之间分割执行时安全地更新变量。本指南的累加器部分更详细地讨论了这些问题。
一般来说,闭包——像循环或局部定义的方法这样的结构,不应该用来改变某些全局状态。Spark不定义或保证从闭包外部引用对象的突变行为。一些这样做的代码可能在本地模式下工作,但这只是偶然的,这样的代码在分布式模式下不会像预期的那样工作。如果需要一些全局聚合,可以使用累加器。
打印RDD的元素
另一个常见的习惯用法是尝试使用RDD .foreach(println)或RDD .map(println)打印RDD的元素。在一台机器上,这将生成预期的输出并打印所有RDD s元素。然而,在集群模式下,执行程序调用的stdout的输出现在写到了执行程序的stdout,而不是驱动程序上的,所以驱动程序上的stdout不会显示这些!要打印驱动程序上的所有元素,可以使用collect()方法首先将RDD带到驱动节点,例如:RDD .collect().foreach(println)。但是,这可能导致驱动程序内存不足,因为collect()将整个RDD提取到一台机器上;如果只需要打印RDD的一些元素,更安全的方法是使用take(): RDD .take(100).foreach(println)。
使用键值对
虽然大多数Spark操作都是在包含任何类型对象的RDDs上进行的,但是有一些特殊操作只在键-值对的RDDs上可用。最常见的是分布式的“shuffle”操作,例如通过键来对元素进行grouping(分组)或聚合(aggregate)。
在Scala中,这些操作在包含Tuple2对象的RDDs上自动可用(语言中的内置元组,通过简单的编写(a, b))。键-值对操作在PairRDDFunctions类中是可用的,它会自动包装元组的RDD。
例如,下面的代码使用键-值对上的reduceByKey操作来计算文件中每行文本发生多少次:
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()将它们作为对象数组返回到驱动程序。
在键值对操作中使用自定义对象作为键时,必须确保自定义equals()方法伴随着匹配的hashCode()方法。有关完整的详细信息,请参阅Object.hashCode()文档中概述的协议
Shuffle操作
Spark中的某些操作会触发一个称为shuffle的事件。shuffle是Spark的一种重新分配数据的机制,这样数据就可以在不同分区之间进行分组。这通常涉及到在执行程序和机器之间复制数据,使shuffle成为一个复杂且昂贵的操作。
为了理解在shuffle期间发生的事情,我们可以考虑reduceByKey操作的示例。 reduceByKey操作生成一个新的RDD,其中单个键的所有值都组合成一个元组 - 键和对与该键关联的所有值执行reduce函数的结果。挑战在于,并非单个密钥的所有值都必须位于同一个分区,甚至是同一个机器上,但它们必须位于同一位置才能计算结果。
在Spark中,数据通常不会跨分区分布,以便在特定操作的必要位置分布。在计算过程中,单个任务将在单个分区上操作——因此,为了组织要执行的单个reduceByKey reduce任务的所有数据,Spark需要执行all-to-all操作。它必须从所有分区中读取,以找到所有键的所有值,然后将跨分区的值组合在一起,以计算每个键的最终结果——这称为shuffle。
虽然新打乱的数据的每个分区中的元素集合是确定的,分区本身的排序也是确定的,但是这些元素的排序是不确定的。如果你想在shuffle之后得到可预测的有序数据,那么就可以使用:
例如,使用mapPartitions对每个分区进行排序, .sorted
repartitionAndsSortWithinPartition用于高效地对分区进行排序,同时重新分区
sortBy来创建一个全局排序的RDD
可能导致shuffle的操作包括重分区操作(比如重分区(repartition)和合并(coalesce))、ByKey操作(除了计数)(比如groupByKey和reduceByKey)以及join操作(比如cogroup和join)。
性能的影响
Shuffle是一种昂贵的操作,因为它涉及到磁盘I/O、数据序列化和网络I/O。要组织shuffle的数据,Spark生成一组任务——映射任务来组织数据,以及一组reduce任务来聚合数据。这个术语来自MapReduce,与Spark的map和reduce操作没有直接关系。
在内部,单个map任务的结果会保存在内存中,直到无法匹配为止。然后,根据目标分区对它们进行排序,并将它们写入单个文件。在reduce方面,任务读取相关的排序块
某些shuffle操作可能会消耗大量的堆内存,因为它们使用内存中的数据结构来组织在传输记录之前或之后的记录。具体来说,reduceByKey和aggregateByKey在map端创建这些结构,ByKey操作在reduce端生成这些结构。当数据不适合内存时,Spark会将这些表泄漏到磁盘上,从而增加磁盘I/O的额外开销,并增加垃圾收集。
Shuffle还会在磁盘上生成大量的中间文件。在Spark 1.3中,这些文件被保存,直到相应的RDDs不再使用并被垃圾收集。这样做是为了在重新计算沿袭时不需要重新创建shuffle文件。如果应用程序保留对这些RDDs的引用,或者GC不频繁启动,那么垃圾收集可能在很长一段时间之后才会发生。这意味着长时间运行的Spark作业可能会消耗大量磁盘空间。临时存储目录由spark.local指定。配置Spark上下文时的dir配置参数。
可以通过调整各种配置参数来调整Shuffle行为。参见Spark配置指南中的Shuffle行为部分。
RDD的持久性
Spark最重要的功能之一是跨操作在内存中持久化(或缓存)数据集。当您持久化一个RDD时,每个节点将它在内存中计算的任何分区存储起来,并在该数据集(或从该数据集派生的数据集)上的其他操作中重用它们。这使得未来的动作更快(通常超过10倍)。缓存是迭代算法和快速交互使用的关键工具。
您可以使用persist()或cache()方法将RDD标记为持久化。第一次在操作中计算它时,它将保存在节点上的内存中。Spark缓存是容错的,如果RDD的任何分区丢失,它将使用最初创建它的转换自动重新计算。
此外,每个持久化的RDD都可以使用不同的存储级别进行存储,例如,允许您在磁盘上持久化数据集,在内存中持久化数据集,但是作为序列化的Java对象(以节省空间),跨节点复制数据集。这些级别是通过传递一个Storage Level对象(Scala、Java、Python)来持久()来设置的。cache()方法是使用默认存储级别StorageLevel的简写方式。MEMORY_ONLY(在内存中存储反序列化的对象)。完整的存储级别是
MEMORY_ONLY
将RDD存储为JVM中的反序列化Java对象。如果RDD不适合内存,一些分区将不会被缓存,并且将在每次需要时动态地重新计算。这是默认级别。
MEMORY_AND_DISK
将RDD存储为JVM中的反序列化Java对象。如果RDD不适合内存,则将不适合的分区存储在磁盘上,并在需要时从那里读取它们。
MEMORY_ONLY_SER (Java和Scala)
将RDD存储为序列化的Java对象(每个分区一个字节数组)。这通常比反序列化的对象更节省空间,特别是在使用快速序列化器时,但是读起来更密集。
MEMORY_AND_DISK_SER (Java和Scala)
与MEMORY_ONLY_SER类似,但是将不适合内存的分区溢出到磁盘,而不是在每次需要它们时动态地重新计算它们。
DISK_ONLY
MEMORY_ONLY_2, MEMORY_AND_DISK_2等等。
与上面的级别相同,但是在两个集群节点上复制每个分区。
OFF_HEAP(实验性的)
类似于MEMORY_ONLY_SER,但将数据存储在堆外内存中。这需要启用堆外内存。
注意:在Python中,存储的对象总是使用Pickle库进行序列化,因此选择序列化级别并不重要。Python中可用的存储级别包括MEMORY_ONLY、MEMORY_ONLY_2、MEMORY_AND_DISK、MEMORY_AND_DISK_2、DISK_ONLY和DISK_ONLY_2。
Spark还会在shuffle操作中自动持久化一些中间数据(例如reduceByKey),甚至在没有用户调用persist的情况下也是如此。这样做是为了避免在转移期间节点失败时重新计算整个输入。如果用户打算重用生成的RDD,我们仍然建议他们调用持久化。
选择哪个存储级别?
Spark存储级别旨在提供内存使用和CPU效率之间的不同权衡。我们建议通过下面的过程来选择一个:
如果您的RDDs与默认存储级别(MEMORY_ONLY)非常匹配,那么就让它这样吧。这是最有效的cpu选项,允许RDDs上的操作以尽可能快的速度运行。
如果没有,请尝试使用MEMORY_ONLY_SER并选择一个快速序列化库,以使对象更节省空间,但访问速度仍然相当快。(Java和Scala)
不要泄漏到磁盘上,除非计算数据集的函数很昂贵,或者它们过滤了大量数据。否则,重新计算分区可能与从磁盘读取分区一样快。
如果需要快速的故障恢复(例如,如果使用Spark服务于web应用程序的请求),则使用复制存储级别。通过重新计算丢失的数据,所有存储级别都提供了完全的容错能力,但是复制的存储级别允许您在RDD上继续运行任务,而无需等待重新计算丢失的分区。
删除数据
Spark自动监视每个节点上的缓存使用情况,并以最近最少使用的方式(LRU)删除旧数据分区。如果您想手动删除一个RDD,而不是等待它从缓存中退出,请使用RDD.unpersist()方法。
共享变量
通常,当传递给Spark操作(如map或reduce)的函数在远程集群节点上执行时,它在函数中使用的所有变量的单独副本上工作。这些变量被复制到每台机器上,对远程机器上的变量的更新不会传播回驱动程序。跨任务支持通用的读写共享变量将是低效的。但是,Spark确实为两种常见的使用模式提供了两种有限类型的共享变量:广播变量和累加器。
Broadcast (广播)变量
允许程序员将只读变量缓存在每台机器上,而不是将它的副本与任务一起发送出去。例如,可以使用它们以高效的方式为每个节点提供一个大型输入数据集的副本。
Spark还尝试使用高效的广播算法来分配广播变量,以降低通信成本。
Spark操作通过一组 stages执行,这些 stages由分布式shuffle操作分开。Spark自动广播tasks在每个 stages所需的公共数据。以这种方式广播的数据在运行每个task之前以序列化的形式缓存和反序列化。这意味着,只有当跨多个stages的task需要相同的数据或以反序列化的形式缓存数据时,显式地创建广播变量才有用。Broadcast变量是通过调用SparkContext.broadcast(v)从变量v中创建的。broadcast变量是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不会多次传送到节点。另外,在广播之后不应修改对象v,以确保所有节点获得广播变量的相同值(例如,如果稍后将变量发送到新节点)。
Accumulators累加器
累加器是仅通过关联和交换操作“添加”的变量,因此可以并行地有效支持。它们可用于实现计数器(如MapReduce)或总和。 Spark本身支持数值类型的累加器,程序员可以添加对新类型的支持。
作为用户,您可以创建命名或未命名的累加器。如下图所示,命名累加器(在此实例计数器中)将在Web UI中显示修改该累加器的stage。 Spark显示“tasks”表中任务修改的每个累加器的值。
在UI中跟踪累加器对于理解运行stage的进展很有用(注意:Python中还不支持这种方法)。
可以通过调用SparkContext.longAccumulator()或SparkContext.doubleAccumulator()来分别累积Long或Double类型的值来创建一个数值累加器。然后,在集群上运行tasks可以使用add方法添加到集群中。然而,他们无法读懂它的值。只有驱动程序可以使用累加器的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
虽然这段代码使用了内置的Long类型累加器支持,但程序员也可以通过子类化累加v2来创建自己的类型。AccumulatorV2抽象类有几个方法,您必须重写它们:重置累加器为零,添加为向累加器添加另一个值,合并为将另一个相同类型的累加器合并到这个累加器。必须重写的其他方法包含在API文档中。例如,假设我们有一个表示数学向量的MyVector类,我们可以这样写:
class VectorAccumulatorV2 extends AccumulatorV2[MyVector, MyVector] {
private val myVector: MyVector = MyVector.createZeroVector
def reset(): Unit = {
myVector.reset()
}
def add(v: MyVector): Unit = {
myVector.add(v) }
...
}
//然后,创建这种类型的累加器:
val myVectorAcc = new VectorAccumulatorV2
//然后,将其注册到spark context:
sc.register(myVectorAcc, "MyVectorAcc1")
注意,当程序员定义自己的 累加器v2类型时,得到的类型可能与添加的元素不同。
对于只在内部操作中执行的累加器更新,Spark保证每个task's对累加器的更新只应用一次,即重新启动的tasks不会更新值。在转换中,用户应该意识到,如果tasks或stages被重新执行,每个task的更新可能会被多次应用。
累加器不会改变Spark的惰性评估模型。如果在RDD上的操作中更新它们,则只有在RDD作为操作的一部分计算时才更新它们的值。因此,在像map()这样的惰性转换中进行累积器更新时,不能保证执行累加器更新。以下代码片段演示了此属性:
val accum = sc.longAccumulator
data.map { x => accum.add(x); x }
// 在这里,accum仍然是0,因为没有操作导致map操作被计算。
单元测试
Spark对任何流行的单元测试框架进行单元测试都很友好。只需在测试中创建一个SparkContext,将主URL设置为local,运行您的操作,然后调用SparkContext.stop()将其拆除。确保在finally块或测试框架的tearDown方法中停止上下文(context),因为Spark不支持在同一程序中同时运行的两个上下文(context)。
部署到集群
应用程序提交指南描述了如何向集群提交应用程序。简而言之,一旦您将应用程序打包到JAR(对于Java/Scala)或.py或.zip文件(对于Python)中,bin/spark-submit脚本允许您将其提交给任何受支持的集群管理器。
Tuning Spark调优指南
Apache Spark示例
集群模式
部署到群集
从Java / Scala启动Spark作业
posted @
2018-11-29 17:14
风雨辰
阅读(
385 )
评论()
编辑
收藏
举报