Spark的RDD编程指南

RDD编程指南

概观

在较高的层次上,每个Spark应用程序都包含一个驱动程序,该程序运行用户的main功能并在集群上执行各种并行操作。Spark提供的主要抽象是弹性分布式数据集(RDD),它是跨群集节点分区的元素集合,可以并行操作。RDD是通过从Hadoop文件系统(或任何其他Hadoop支持的文件系统)中的文件或驱动程序中的现有Scala集合开始并对其进行转换来创建的。用户还可以要求Spark 在内存中保留 RDD,允许它在并行操作中有效地重用。最后,RDD会自动从节点故障中恢复。

Spark中的第二个抽象是可以在并行操作中使用的共享变量。默认情况下,当Spark并行运行一个函数作为不同节点上的一组任务时,它会将函数中使用的每个变量的副本发送给每个任务。有时,变量需要跨任务共享,或者在任务和驱动程序之间共享。Spark支持两种类型的共享变量:广播变量,可用于缓存所有节点的内存中的值; 累加器,它们是仅“添加”到的变量,例如计数器和总和。

本指南以Spark支持的每种语言显示了这些功能。如果您启动Spark的交互式shell,最简单的方法就是 - bin/spark-shell对于Scala shell或bin/pysparkPython。

与Spark链接

Spark 2.3.1的构建和分发默认情况下与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.3.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

(在Spark 1.3.0之前,您需要显式import org.apache.spark.SparkContext._启用必要的隐式转换。)

初始化Spark

Spark程序必须做的第一件事是创建一个SparkContext对象,它告诉Spark如何访问集群。要创建SparkContext您首先需要构建一个包含有关应用程序信息的SparkConf对象。

每个JVM只能激活一个SparkContext。stop()在创建新的SparkContext之前,您必须是活动的SparkContext。

val conf = new SparkConf().setAppName(appName).setMaster(master)
new SparkContext(conf)

appName参数是应用程序在群集UI上显示的名称。 masterSpark,Mesos或YARN群集URL,或以本地模式运行的特殊“本地”字符串。实际上,当在群集上运行时,您不希望master在程序中进行硬编码,而是启动应用程序spark-submit并在那里接收它。但是,对于本地测试和单元测试,您可以传递“local”来运行Spark in-process。

使用Shell

  • Scala
  • Python

在Spark shell中,已经在名为的变量中为您创建了一个特殊的解释器感知SparkContext sc。制作自己的SparkContext将无法正常工作。您可以使用--master参数设置上下文连接到的主服务器,并且可以通过将逗号分隔的列表传递给参数来将JAR添加到类路径中--jars。您还可以通过向参数提供以逗号分隔的Maven坐标列表,将依赖项(例如Spark包)添加到shell会话中--packages。任何可能存在依赖关系的其他存储库(例如Sonatype)都可以传递给--repositories参数。例如,要bin/spark-shell在四个核心上运行,请使用:

$ ./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调用更通用的spark-submit脚本

弹性分布式数据集(RDD)

Spark围绕弹性分布式数据集(RDD)的概念展开,RDD是可以并行操作的容错的容错集合。创建RDD有两种方法:并行化 驱动程序中的现有集合,或引用外部存储系统中的数据集,例如共享文件系统,HDFS,HBase或提供Hadoop InputFormat的任何数据源。

并行化集合

并行集合通过调用创建SparkContextparallelize一个现有的收集方法,在你的驱动程序(斯卡拉Seq)。复制集合的元素以形成可以并行操作的分布式数据集。例如,以下是如何创建包含数字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会尝试根据您的群集自动设置分区数。但是,您也可以通过将其作为第二个参数传递给parallelize(例如sc.parallelize(data, 10))来手动设置。注意:代码中的某些位置使用术语切片(分区的同义词)来保持向后兼容性。

外部数据集

Spark可以从Hadoop支持的任何存储源创建分布式数据集,包括本地文件系统,HDFS,Cassandra,HBase,Amazon S3等.Spark支持文本文件,SequenceFiles和任何其他Hadoop InputFormat

文本文件RDDS可以使用创建SparkContexttextFile方法。此方法需要一个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可以通过数据集操作执行操作。例如,我们可以使用mapreduce操作添加所有行的大小,如下所示:distFile.map(s => s.length).reduce((a, b) => a + b)

有关使用Spark读取文件的一些注意事项

  • 如果在本地文件系统上使用路径,则还必须可以在工作节点上的相同路径上访问该文件。将文件复制到所有工作者或使用网络安装的共享文件系统。

  • 所有Spark的基于文件的输入方法,包括textFile支持在目录,压缩文件和通配符上运行。例如,你可以使用textFile("/my/directory")textFile("/my/directory/*.txt")textFile("/my/directory/*.gz")

  • textFile方法还采用可选的第二个参数来控制文件的分区数。默认情况下,Spark为文件的每个块创建一个分区(HDFS中默认为128MB),但您也可以通过传递更大的值来请求更多的分区。请注意,您不能拥有比块少的分区。

除文本文件外,Spark的Scala API还支持其他几种数据格式:

  • SparkContext.wholeTextFiles允许您读取包含多个小文本文件的目录,并将它们作为(文件名,内容)对返回。这与之相反textFile,每个文件中每行返回一条记录。分区由数据局部性决定,在某些情况下,可能导致分区太少。对于这些情况,wholeTextFiles提供可选的第二个参数来控制最小数量的分区。

  • 对于SequenceFiles,使用SparkContext的sequenceFile[K, V]方法,其中KV是文件中键和值的类型。这些应该是Hadoop的Writable接口的子类,如IntWritableText。此外,Spark允许您为一些常见的Writable指定本机类型; 例如,sequenceFile[Int, String]将自动读取IntWritables和文本。

  • 对于其他Hadoop InputFormats,您可以使用该SparkContext.hadoopRDD方法,该方法采用任意JobConf和输入格式类,键类和值类。设置这些与使用输入源的Hadoop作业相同。您还可以使用SparkContext.newAPIHadoopRDD基于“新”MapReduce API(org.apache.hadoop.mapreduce)的InputFormats 。

  • RDD.saveAsObjectFileSparkContext.objectFile支持以包含序列化Java对象的简单格式保存RDD。虽然这不像Avro这样的专用格式有效,但它提供了一种保存任何RDD的简便方法。

RDD操作

RDD支持两种类型的操作:转换(从现有数据集创建新数据集)和操作(在数据集上运行计算后将值返回到驱动程序)。例如,map是一个转换,它通过一个函数传递每个数据集元素,并返回一个表示结果的新RDD。另一方面,reduce是一个使用某个函数聚合RDD的所有元素并将最终结果返回给驱动程序的动作(尽管还有一个reduceByKey返回分布式数据集的并行)。

Spark中的所有转换都是懒惰的,因为它们不会立即计算结果。相反,他们只记得应用于某些基础数据集的转换(例如文件)。仅当操作需要将结果返回到驱动程序时才会计算转换。这种设计使Spark能够更有效地运行。例如,我们可以意识到通过创建的数据集map将用于a reduce并仅返回reduce驱动程序的结果,而不是更大的映射数据集。

默认情况下,每次对其执行操作时,都可以重新计算每个转换后的RDD。但是,您也可以使用(或)方法在内存中保留 RDD ,在这种情况下,Spark会在群集上保留元素,以便在下次查询时更快地访问。还支持在磁盘上保留RDD,或在多个节点之间复制。persistcache

基本

为了说明RDD基础知识,请考虑以下简单程序:

val lines = sc.textFile("data.txt")
val lineLengths = lines.map(s => s.length)
val totalLength = lineLengths.reduce((a, b) => a + b)

第一行从外部文件定义基础RDD。此数据集未加载到内存中或以其他方式执行:lines仅仅是指向文件的指针。第二行定义lineLengthsmap转换的结果。再次,lineLengths 是不是马上计算,由于懒惰。最后,我们运行reduce,这是一个动作。此时,Spark将计算分解为在不同机器上运行的任务,并且每台机器都运行其部分映射和本地缩减,仅返回其对驱动程序的答案。

如果我们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里面有引用的 func1方法是的MyClass实例,所以需要发送到群集的整个对象。它类似于写作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的一个难点是在跨集群执行代码时理解变量和方法的范围和生命周期。修改其范围之外的变量的RDD操作可能经常引起混淆。在下面的示例中,我们将查看foreach()用于递增计数器的代码,但其他操作也可能出现类似问题。

考虑下面的天真RDD元素总和,根据执行是否在同一JVM中发生,它可能表现不同。一个常见的例子是在local模式(--master = local[n])中运行Spark 而不是将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)

本地与群集模式

上述代码的行为未定义,可能无法按预期工作。为了执行作业,Spark将RDD操作的处理分解为任务,每个任务由执行程序执行。在执行之前,Spark计算任务的关闭。闭包是那些变量和方法,它们必须是可见的,以便执行者在RDD上执行计算(在这种情况下foreach())。该闭包被序列化并发送给每个执行者。

发送给每个执行程序的闭包内的变量现在是副本,因此,当在函数内引用计数器foreach,它不再是驱动程序节点上的计数器。驱动程序节点的内存中仍然有一个计数器,但执行程序不再可见!执行程序只能看到序列化闭包中的副本。因此,计数器的最终值仍然为零,因为计数器上的所有操作都引用了序列化闭包内的值。

在本地模式下,在某些情况下,该foreach函数实际上将在与驱动程序相同的JVM中执行,并将引用相同的原始计数器,并且可能实际更新它。

为了确保在这些场景中明确定义的行为,应该使用Accumulator。Spark中的累加器专门用于提供一种机制,用于在跨集群中的工作节点拆分执行时安全地更新变量。本指南的“累加器”部分更详细地讨论了这些内容。

通常,闭包 - 类似循环或本地定义的方法的构造不应该用于改变某些全局状态。Spark没有定义或保证从闭包外部引用的对象的突变行为。执行此操作的某些代码可能在本地模式下工作,但这只是偶然的,并且此类代码在分布式模式下不会按预期运行。如果需要某些全局聚合,请使用累加器。

打印RDD的元素

另一个常见的习惯用法是尝试使用rdd.foreach(println)或打印出RDD的元素rdd.map(println)。在一台机器上,这将生成预期的输出并打印所有RDD的元素。但是,在cluster模式下,stdout执行程序调用的输出现在写入执行stdout程序,而不是驱动程序上的输出,因此stdout驱动程序不会显示这些!要打印驱动程序上的所有元素,可以使用该collect()方法首先将RDD带到驱动程序节点:rdd.collect().foreach(println)。但是,这会导致驱动程序内存不足,因为collect()将整个RDD提取到一台机器上; 如果您只需要打印RDD的一些元素,更安全的方法是使用take()rdd.take(100).foreach(println)

使用键值对

虽然大多数Spark操作都适用于包含任何类型对象的RDD,但一些特殊操作仅适用于键值对的RDD。最常见的是分布式“随机”操作,例如通过密钥对元素进行分组或聚合。

在Scala中,这些操作在包含Tuple2对象的RDD上自动可用 (语言中的内置元组,通过简单编写创建(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()文档中概述的合同。

转换

下表列出了Spark支持的一些常见转换。有关详细信息,请参阅RDD API文档(Scala, Java, Python, R)并配对RDD函数doc(ScalaJava)。

转型 含义
地图功能 返回通过函数func传递源的每个元素形成的新分布式数据集。
过滤器功能 返回通过选择func返回true 的源元素形成的新数据集。
flatMapfunc 与map类似,但每个输入项可以映射到0个或更多输出项(因此func应该返回Seq而不是单个项)。
mapPartitionsfunc 与map类似,但在RDD的每个分区(块)上单独运行,因此当在类型T的RDD上运行时,func必须是Iterator <T> => Iterator <U>类型。
mapPartitionsWithIndexfunc 与mapPartitions类似,但也为func提供了表示分区索引的整数值,因此当在类型T的RDD上运行时,func必须是类型(Int,Iterator <T>)=> Iterator <U>。
样本withReplacementfractionseed 使用给定的随机数生成器种子,在有或没有替换的情况下对数据的一小部分进行采样。
联盟otherDataset 返回一个新数据集,其中包含源数据集和参数中元素的并集。
交叉口(其他数据 返回包含源数据集和参数中元素交集的新RDD。
distinct([ numPartitions ])) 返回包含源数据集的不同元素的新数据集。
groupByKey([ numPartitions ]) 在(K,V)对的数据集上调用时,返回(K,Iterable <V>)对的数据集。
注意:如果要对每个键执行聚合(例如总和或平均值)进行分组,则使用reduceByKeyaggregateByKey将产生更好的性能。 
注意:默认情况下,输出中的并行级别取决于父RDD的分区数。您可以传递可选numPartitions参数来设置不同数量的任务。
reduceByKeyfunc,[ numPartitions ]) 当调用(K,V)对的数据集时,返回(K,V)对的数据集,其中使用给定的reduce函数func聚合每个键的值,该函数必须是类型(V,V)=> V.同样groupByKey,reduce任务的数量可通过可选的第二个参数进行配置。
aggregateByKeyzeroValue)(seqOpcombOp,[ numPartitions ]) 当调用(K,V)对的数据集时,返回(K,U)对的数据集,其中使用给定的组合函数和中性“零”值聚合每个键的值。允许与输入值类型不同的聚合值类型,同时避免不必要的分配。同样groupByKey,reduce任务的数量可通过可选的第二个参数进行配置。
sortByKey([ 升序 ],[ numPartitions ]) 当在K实现Ordered的(K,V)对的数据集上调用时,返回按键按升序或降序排序的(K,V)对的数据集,如布尔ascending参数中所指定的。
joinotherDataset,[ numPartitions ]) 当调用类型(K,V)和(K,W)的数据集时,返回(K,(V,W))对的数据集以及每个键的所有元素对。外连接通过支持leftOuterJoinrightOuterJoinfullOuterJoin
协同组otherDataset,[ numPartitions]) 当调用类型(K,V)和(K,W)的数据集时,返回(K,(Iterable <V>,Iterable <W>))元组的数据集。此操作也称为groupWith
笛卡尔(其他数据 当调用类型T和U的数据集时,返回(T,U)对的数据集(所有元素对)。
pipe命令[envVars] 通过shell命令(例如Perl或bash脚本)管道RDD的每个分区。RDD元素被写入进程的stdin,并且输出到其stdout的行将作为字符串的RDD返回。
合并numPartitions 将RDD中的分区数减少为numPartitions。过滤大型数据集后,可以更有效地运行操作。
重新分区numPartitions 随机重新调整RDD中的数据以创建更多或更少的分区并在它们之间进行平衡。这总是随机播放网络上的所有数据。
repartitionAndSortWithinPartitions分区程序 根据给定的分区重新分区RDD,并在每个生成的分区中按键对记录进行排序。这比repartition在每个分区中调用然后排序更有效,因为它可以将排序推送到shuffle机器中。

操作

下表列出了Spark支持的一些常见操作。请参阅RDD API文档(Scala, Java, Python, R

并配对RDD函数doc(Scala, Java)以获取详细信息。

行动 含义
减少功能 使用函数func(它接受两个参数并返回一个)来聚合数据集的元素。该函数应该是可交换的和关联的,以便可以并行正确计算。
收集() 在驱动程序中将数据集的所有元素作为数组返回。在过滤器或其他返回足够小的数据子集的操作之后,这通常很有用。
count() 返回数据集中的元素数。
第一个() 返回数据集的第一个元素(类似于take(1))。
n 返回包含数据集的前n个元素的数组。
takeSamplewithReplacementnum,[ seed ]) 返回一个数组,其中包含数据集的num个元素的随机样本,有或没有替换,可选地预先指定随机数生成器种子。
takeOrderedn[ordering] 使用自然顺序或自定义比较器返回RDD 的前n个元素。
saveAsTextFilepath 将数据集的元素作为文本文件(或文本文件集)写入本地文件系统,HDFS或任何其他Hadoop支持的文件系统的给定目录中。Spark将在每个元素上调用toString,将其转换为文件中的一行文本。
saveAsSequenceFilepath
(Java和Scala)
将数据集的元素作为Hadoop SequenceFile写入本地文件系统,HDFS或任何其他Hadoop支持的文件系统中的给定路径中。这可以在实现Hadoop的Writable接口的键值对的RDD上使用。在Scala中,它也可以在可隐式转换为Writable的类型上使用(Spark包括基本类型的转换,如Int,Double,String等)。
saveAsObjectFilepath
(Java和Scala)
使用Java序列化以简单格式编写数据集的元素,然后可以使用Java序列化加载SparkContext.objectFile()
countByKey() 仅适用于类型(K,V)的RDD。返回(K,Int)对的散列映射以及每个键的计数。
foreachfunc 在数据集的每个元素上运行函数func。这通常用于副作用,例如更新累加器或与外部存储系统交互。 
注意:修改除累加器之外的变量foreach()可能会导致未定义的行为。有关详细信息,请参阅了解闭包

Spark RDD API还公开某些操作的异步版本,例如foreachAsyncfor foreach,它会立即FutureAction向调用者返回一个而不是在操作完成时阻塞。这可用于管理或等待操作的异步执行。

随机操作

Spark中的某些操作会触发称为shuffle的事件。随机播放是Spark的重新分配数据的机制,因此它可以跨分区进行不同的分组。这通常涉及跨执行程序和机器复制数据,使得混洗成为复杂且昂贵的操作。

背景

为了理解在洗牌过程中发生的事情,我们可以考虑reduceByKey操作的例子 。该reduceByKey操作生成一个新的RDD,其中单个键的所有值都组合成一个元组 - 键和对与该键关联的所有值执行reduce函数的结果。挑战在于,并非单个密钥的所有值都必须位于同一个分区,甚至是同一个机器上,但它们必须位于同一位置才能计算结果。

在Spark中,数据通常不跨分区分布,以便在特定操作的必要位置。在计算过程中,单个任务将在单个分区上运行 - 因此,为了组织reduceByKey执行单个reduce任务的所有数据,Spark需要执行全部操作。它必须从所有分区读取以查找所有键的所有值,然后将各个值组合在一起以计算每个键的最终结果 - 这称为shuffle

尽管新洗牌数据的每个分区中的元素集将是确定性的,并且分区本身的排序也是如此,但这些元素的排序不是。如果在随机播放后需要可预测的有序数据,则可以使用:

  • mapPartitions 例如,使用以下方式对每个分区进行排序 .sorted
  • repartitionAndSortWithinPartitions 在重新分区的同时有效地对分区进行排序
  • sortBy 制作全局有序的RDD

可以导致混洗的操作包括重新分区操作,如 repartitioncoalesce'ByKey操作(除了计数)之类的groupByKeyreduceByKey,以及 连接操作,如cogroupjoin

绩效影响

所述随机播放是昂贵的操作,因为它涉及的磁盘I / O,数据序列,和网络I / O。为了组织shuffle的数据,Spark生成多组任务 - 映射任务以组织数据,以及一组reduce任务来聚合它。这个术语来自MapReduce,并不直接与Spark mapreduce操作相关。

在内部,各个地图任务的结果会保留在内存中,直到它们无法适应。然后,这些基于目标分区进行排序并写入单个文件。在reduce方面,任务读取相关的排序块。

某些shuffle操作会消耗大量的堆内存,因为它们使用内存中的数据结构来在传输记录之前或之后组织记录。具体而言, reduceByKeyaggregateByKey创建在地图上侧这样的结构,和'ByKey操作产生这些上减少侧。当数据不适合内存时,Spark会将这些表溢出到磁盘,从而导致磁盘I / O的额外开销和垃圾收集增加。

Shuffle还会在磁盘上生成大量中间文件。从Spark 1.3开始,这些文件将被保留,直到不再使用相应的RDD并进行垃圾回收。这样做是为了在重新计算谱系时不需要重新创建shuffle文件。如果应用程序保留对这些RDD的引用或GC不经常启动,则垃圾收集可能仅在很长一段时间后才会发生。这意味着长时间运行的Spark作业可能会占用大量磁盘空间。spark.local.dir配置Spark上下文时,配置参数指定临时存储目录 。

可以通过调整各种配置参数来调整随机行为。请参阅“ Spark配置指南 ”中的“随机行为”部分。

RDD持久性

Spark中最重要的功能之一是跨操作在内存中持久化(或缓存)数据集。当您持久保存RDD时,每个节点都会存储它在内存中计算的任何分区,并在该数据集(或从中派生的数据集)的其他操作中重用它们。这使得未来的行动更快(通常超过10倍)。缓存是迭代算法和快速交互使用的关键工具。

您可以使用persist()cache()方法标记要保留的RDD 。第一次在动作中计算它时,它将保留在节点的内存中。Spark的缓存是容错的 - 如果RDD的任何分区丢失,它将使用最初创建它的转换自动重新计算。

此外,每个持久化RDD可以使用不同的存储级别进行存储,例如,允许您将数据集保留在磁盘上,将其保留在内存中,但作为序列化Java对象(以节省空间),跨节点复制它。通过传递StorageLevel对象(Scala, Java, Python)来设置这些级别 persist()。该cache()方法是使用默认存储级别的简写,即StorageLevel.MEMORY_ONLY(在内存中存储反序列化的对象)。完整的存储级别是:

存储级别 含义
MEMORY_ONLY 将RDD存储为JVM中的反序列化Java对象。如果RDD不适合内存,则某些分区将不会被缓存,并且每次需要时都会重新计算。这是默认级别。
MEMORY_AND_DISK 将RDD存储为JVM中的反序列化Java对象。如果RDD不适合内存,请存储不适合磁盘的分区,并在需要时从那里读取它们。
MEMORY_ONLY_SER 
(Java和Scala)
将RDD存储为序列化 Java对象(每个分区一个字节数组)。这通常比反序列化对象更节省空间,特别是在使用快速序列化器时,但读取CPU密集程度更高。
MEMORY_AND_DISK_SER 
(Java和Scala)
与MEMORY_ONLY_SER类似,但将不适合内存的分区溢出到磁盘,而不是每次需要时即时重新计算它们。
DISK_ONLY 仅将RDD分区存储在磁盘上。
MEMORY_ONLY_2,MEMORY_AND_DISK_2等 与上面的级别相同,但复制两个群集节点上的每个分区。
OFF_HEAP(实验性) 与MEMORY_ONLY_SER类似,但将数据存储在 堆外内存中。这需要启用堆外内存。

注意: 在Python中,存储对象将始终使用Pickle库进行序列化,因此选择序列化级别无关紧要。Python中的可用存储级别包括MEMORY_ONLYMEMORY_ONLY_2, MEMORY_AND_DISKMEMORY_AND_DISK_2DISK_ONLY,和DISK_ONLY_2

reduceByKey即使没有用户调用,Spark也会在shuffle操作(例如)中自动保留一些中间数据persist。这样做是为了避免在shuffle期间节点出现故障时重新计算整个输入。我们仍然建议用户persist在计划重用RDD时调用生成的RDD。

选择哪种存储级别?

Spark的存储级别旨在提供内存使用和CPU效率之间的不同折衷。我们建议您通过以下流程选择一个:

  • 如果您的RDD与默认存储级别(MEMORY_ONLY)保持一致,请保持这种状态。这是CPU效率最高的选项,允许RDD上的操作尽可能快地运行。

  • 如果没有,请尝试使用MEMORY_ONLY_SER选择快速序列化库,以使对象更加节省空间,但仍然可以快速访问。(Java和Scala)

  • 除非计算数据集的函数很昂贵,否则它们不会溢出到磁盘,或者它们会过滤大量数据。否则,重新计算分区可能与从磁盘读取分区一样快。

  • 如果要快速故障恢复,请使用复制的存储级别(例如,如果使用Spark来处理来自Web应用程序的请求)。所有存储级别通过重新计算丢失的数据提供完全容错,但复制的存储级别允许您继续在RDD上运行任务,而无需等待重新计算丢失的分区。

删除数据

Spark会自动监视每个节点上的缓存使用情况,并以最近最少使用(LRU)的方式删除旧数据分区。如果您想手动删除RDD而不是等待它退出缓存,请使用该RDD.unpersist()方法。

共享变量

通常,当在远程集群节点上执行传递给Spark操作(例如mapor reduce)的函数时,它将在函数中使用的所有变量的单独副本上工作。这些变量将复制到每台计算机,并且远程计算机上的变量更新不会传播回驱动程序。支持跨任务的通用,读写共享变量效率低下。但是,Spark确实为两种常见的使用模式提供了两种有限类型的共享变量:广播变量和累加器。

广播变量

广播变量允许程序员在每台机器上保留一个只读变量,而不是随副本一起发送它的副本。例如,它们可用于以有效的方式为每个节点提供大输入数据集的副本。Spark还尝试使用有效的广播算法来分发广播变量,以降低通信成本。

Spark动作通过一组阶段执行,由分布式“shuffle”操作分隔。Spark自动广播每个阶段中任务所需的公共数据。以这种方式广播的数据以序列化形式缓存并在运行每个任务之前反序列化。这意味着显式创建广播变量仅在跨多个阶段的任务需要相同数据或以反序列化形式缓存数据很重要时才有用。

广播变量是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就不会多次传送到节点。此外,在v广播之后不应修改对象 ,以确保所有节点获得相同的广播变量值(例如,如果稍后将变量发送到新节点)。

蓄电池

累加器是仅通过关联和交换操作“添加”的变量,因此可以并行地有效支持。它们可用于实现计数器(如MapReduce)或总和。Spark本身支持数值类型的累加器,程序员可以添加对新类型的支持。

作为用户,您可以创建命名或未命名的累加器。如下图所示,命名累加器(在此实例中counter)将在Web UI中显示修改该累加器的阶段。Spark显示“任务”表中任务修改的每个累加器的值。

Spark UI中的累加器

跟踪UI中的累加器对于理解运行阶段的进度非常有用(注意:Python中尚不支持这一点)。

可以通过分别调用SparkContext.longAccumulator()SparkContext.doubleAccumulator() 累积Long或Double类型的值来创建数字累加器。然后,可以使用该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类型累加器的内置支持,但程序员也可以通过继承AccumulatorV2来创建自己的类型。AccumulatorV2抽象类有几个必须覆盖的方法:reset用于将累加器重置为零,add用于将另一个值添加到累加器中, merge用于将另一个相同类型的累加器合并到这个中。其他必须覆盖的方法包含在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)
  }
  ...
}

// Then, create an Accumulator of this type:
val myVectorAcc = new VectorAccumulatorV2
// Then, register it into spark context:
sc.register(myVectorAcc, "MyVectorAcc1")

请注意,当程序员定义自己的AccumulatorV2类型时,结果类型可能与添加的元素类型不同。

对于操作执行的累加器更新,Spark保证每个任务对累加器的更新仅应用一次,即重新启动的任务不会更新该值。在转换中,用户应该知道,如果重新执行任务或作业阶段,则可以多次应用每个任务的更新。

累加器不会改变Spark的惰性评估模型。如果在RDD上的操作中更新它们,则只有在RDD作为操作的一部分计算时才更新它们的值。因此,当在惰性变换中进行时,不保证执行累加器更新map()。以下代码片段演示了此属性:

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.

部署到群集

提交申请指南介绍了如何提交申请到集群。简而言之,一旦将应用程序打包到JAR(用于Java / Scala)或一组.py.zip文件(用于Python),该bin/spark-submit脚本允许您将其提交给任何支持的集群管理器。

从Java / Scala启动Spark作业

org.apache.spark.launcher 包提供类推出的Spark作为工作使用一个简单的Java API的子进程。

单元测试

Spark对任何流行的单元测试框架进行单元测试都很友好。只需SparkContext在主URL设置为的情况下创建测试local,运行您的操作,然后调用SparkContext.stop()将其拆除。确保在finally块或测试框架的tearDown方法中停止上下文,因为Spark不支持在同一程序中同时运行的两个上下文。

猜你喜欢

转载自blog.csdn.net/u010675669/article/details/81702478