Spark中文文档翻译3.1.1-Spark RDD Guide--弹性分布式数据集(RDDs)

如果觉得内容不错,别忘记一键三连哦!!!在这里插入图片描述

弹性分布式数据集(RDDs)

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

并行集合 Parallelized Collections

并行化集合是通过调用 SparkContext 的并行化方法在您的驱动程序(Scala 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 尝试基于集群自动设置分区数。但是,您也可以通过将其作为并行化的第二个参数传递来手动设置它(例如,sc.paralize (data,10))。注意: 代码中的一些地方使用术语片(分区的同义词)来维护向下兼容。

外部数据集 External Datasets

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

可以使用 SparkContext 的 textFile 方法创建文本文件 RDDs。此方法获取文件的 URI (机器上的本地路径或 hdfs://,s3a://,etc 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 进行操作。例如,我们可以使用 map 将所有行的大小加起来并减少操作,如下所示: distFile.map(s => s.length).reduce((a, b) => a + b).

关于使用 Spark 读取文件的一些注释:

  • 如果使用本地文件系统上的路径,则还必须在辅助节点上的相同路径上访问该文件。将该文件复制到所有worker nodes,或者使用网络挂载的共享文件系统。
  • Spark 的所有基于文件的输入方法,包括 textFile,支持在目录、压缩文件和通配符上运行。例如,可以使用textFile("/my/directory"),textFile("/my/directory/.txt"), andtextFile("/my/directory/.gz")当读取多个文件时,分区的顺序取决于文件从文件系统返回的顺序。例如,它可能遵循或不遵循按路径排列的文件的字典顺序。在一个分区中,元素按照它们在基础文件中的顺序排序。
  • textFile 方法还采用一个可选的第二个参数来控制文件的分区数。默认情况下,Spark 为文件的每个块创建一个分区(在 HDFS 中默认为128 MB) ,但是您也可以通过传递更大的值来要求更高的分区数量。请注意,分区不能少于块。

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

  • SparkContext.wholeTextFiles 允许您读取包含多个小文本文件的目录,并将其中每个文件作为(文件名、内容)对返回。这与 textFile 相反,后者在每个文件中每行返回一条记录。分区是由数据局部性决定的,在某些情况下,这可能导致分区太少。对于这些情况,wholeTextFiles 提供了一个可选的第二个参数,用于控制最小分区数量。
  • 对于 SequenceFiles,使用 SparkContext 的 sequenceFile [ k,v ]方法,其中 k 和 v 是文件中键和值的类型。这些应该是 Hadoop 的 Writable 接口的子类,就像 intwrtable 和 Text 一样。此外,Spark 允许您为一些常见的 Writables 指定本机类型; 例如,sequenceFile [ Int,String ]将自动读取 IntWritables 和 Texts。
  • 对于其他 Hadoop InputFormats,您可以使用 sparkcontext.Hadoop/rdd 方法,该方法接受任意的 JobConf 和输入格式类、键类和值类。设置这些值的方式与使用输入源的 Hadoop 作业的方式相同。您还可以基于“新的”MapReduce API (org.apache.hadoop. MapReduce)对 InputFormats 使用 SparkContext.newAPIHadoopRDD。
  • RDD.Saveasobjectfile 和 SparkContext.objectFile 支持以包含序列化 Java 对象的简单格式保存 RDD。虽然这不像 Avro 这样的专用格式那样有效,但它提供了一种简单的方法来保存任何 RDD。

RDD操作(RDD Operations)

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

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

默认情况下,每次对转换后的 RDD 运行操作时,都可以重新计算它。但是,您也可以使用 persist (或 cache)方法在内存中持久化 RDD,在这种情况下,Spark 将保留集群中的元素,以便在下次查询它时更快地访问它。还支持在磁盘上持久化 rdd,或者跨多个节点复制 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,这是一个action。此时,Spark 将计算分解成任务,以便在单独的机器上运行,每台机器运行其映射部分和局部归约,只将其答案返回给驱动程序。

如果我们以后还想再次使用 lineLengths,我们可以添加:

lineLengths.persist()

在 reduce 之前,这将导致在第一次计算 lineLengths 之后将其保存在内存中。

Passing Functions to Spark 传递函数到 Spark

Spark 的 API 在很大程度上依赖于驱动程序中传递的函数在集群上运行。有两种建议的方法可以做到这一点:

  • 使用匿名函数的语法,这可以让代码更加简洁。
  • 使用全局单例对象的静态方法。比如,你可以定义函数对象objectMyFunctions,然后将该对象的MyFunction.func1方法传递给Spark,如下所示:
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方法,因此需要将整个对象发送到集群。它类似于编写 rd.map (x = > this.func1(x))。

类似地,访问外部对象的字段将引用整个对象:

class MyClass {
  val field = "Hello"
  def doStuff(rdd: RDD[String]): RDD[String] = { rdd.map(x => field + x) }
}

等价于编写 rd.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 ()增加计数器的代码,但其他操作也可能出现类似的问题。

Example 例子

考虑一下下面那个 RDD 元素 sum,它的行为可能根据是否在同一个 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 本地模式 vs 集群模式

上述代码的行为未定义,并且可能无法按预期的方式工作。为了执行作业,Spark 将 RDD 操作的处理分解为任务,每个任务都由执行者执行。在执行之前,Spark 计算任务的闭包。闭包是那些执行器在 RDD 上执行计算时必须可见的变量和方法(在本例中为 foreach ())。此闭包被序列化并发送到每个执行器。

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

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

为了确保在这些场景中定义良好的行为,应该使用 Accumulator。Spark 中的累加器专门用于提供一种机制,当执行在集群中的工作节点之间分离时,可以安全地更新变量。本指南的累加器部分将更详细地讨论这些问题。

一般来说,闭包构造(比如循环或者局部定义的方法)不应该用来改变某些全局状态。Spark 不定义或保证对闭包外引用的对象的变异行为。有些执行此操作的代码可能在本地模式下工作,但这只是偶然的,而且这些代码在分布式模式下的行为不会像预期的那样。如果需要某些全局聚合,则使用 cumulator。

打印RDD的元素(Printing elements of an RDD)

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

Working with Key-Value Pairs 使用键-值对

虽然大多数 Spark 操作都在包含任何类型对象的 rdd 上工作,但只有少数特殊操作可用于键值对的 rdd。最常见的是分布式“洗牌”操作,比如按键对元素进行分组或聚合。

在 Scala 中,这些操作在包含 tuple 2对象的 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 ()文档中概述的契约。

Transformations 转变

下表列出了 Spark 支持的一些常见转换。参考 rddapidoc (Scala,Java,Python,r)和 RDD 函数 doc (Scala,Java)获得详细信息。

TransformationMeaning
map**(func)**通过将源的每个元素传递给函数func,返回一个新的分布式数据集。
filter(func)通过选择源中func返回true的元素,返回一个新数据集。
flatMap(func)类似于map,但是每个输入项可以映射到0个或多个输出项(因此func应该返回Seq而不是单个项)。
mapPartitions(func)类似于map,但是在RDD的每个分区(块)上单独运行,所以func必须是Iterator => Iterator的类型在类型为T的RDD上运行。
mapPartitionsWithIndex(func)类似于mapPartitions,但也为func提供了一个表示分区索引的整数值,所以func在类型为T的RDD上运行时必须是类型(Int, Iterator) => Iterator
sample**(withReplacement, fraction, seed)**使用给定的随机数生成器种子,对数据的一部分进行抽样,无论是否进行替换。
union(otherDataset)返回一个包含源数据集中元素和参数的并集的新数据集。
intersection(otherDataset)返回一个新的RDD,它包含源数据集中元素和参数的交集。
distinct([numPartitions]))返回一个包含源数据集的不同元素的新数据集。
groupByKey([numPartitions])当调用一个(K, V)对的数据集时,返回一个(K, Iterable)对的数据集。注意:如果为了对每个键执行聚合(例如总和或平均值)而进行分组,那么使用reduceByKey或aggregateByKey将获得更好的性能。注意:默认情况下,输出中的并行级别取决于父RDD的分区数。您可以传递一个可选的numPartitions参数来设置不同数量的任务。
reduceByKey(func, [numPartitions])当收到(K、V)的数据集对,返回一个数据集(K、V)对每个键的值在哪里聚合使用给定减少函数func,必须类型(V, V) = >诉groupByKey,减少任务的数量通过一个可选的第二个参数是可配置的。
aggregateByKey(zeroValue)(seqOp, combOp, [numPartitions])当调用一个(K, V)对的数据集时,返回一个(K, U)对的数据集,其中每个键的值使用给定的combine函数和一个中间的“zero”值聚合。允许与输入值类型不同的聚合值类型,同时避免不必要的分配。与groupByKey一样,reduce任务的数量可以通过第二个可选参数进行配置。
sortByKey([ascending], [numPartitions])当对(K, V)对的数据集调用时,其中K实现了Ordered,返回一个(K, V)对的数据集,按键升序或降序排序,如布尔升序参数中指定的那样。
join(otherDataset, [numPartitions])当调用类型为(K, V)和(K, W)的数据集时,返回一个(K, (V, W))对的数据集,每个键包含所有对的元素。外部连接通过leftOuterJoin、righttouterjoin和fullOuterJoin支持。
cogroup(otherDataset, [numPartitions])当调用类型为(K, V)和(K, W)的数据集时,返回一个(K, (Iterable, Iterable))元组的数据集。这个操作也称为groupWith。
cartesian(otherDataset)当对类型T和U的数据集调用时,返回一个(T, U)对的数据集(所有元素对)。
pipe(command, [envVars])通过shell命令(例如Perl或bash脚本)对RDD的每个分区进行管道连接。RDD元素被写入进程的stdin,输出到它的stdout的行作为字符串的RDD返回。
coalesce(numPartitions)将RDD中的分区数量减少到numPartitions。对于在对大型数据集进行过滤后更有效地运行操作很有用。
repartition**(numPartitions)**随机重新分发RDD中的数据,以创建更多或更少的分区,并在这些分区之间进行平衡。这总是会打乱网络上的所有数据。
repartitionAndSortWithinPartitions**(partitioner)根据给定的分区程序重新分区RDD,并在每个产生的分区中,按键对记录进行排序。这比调用重分区然后在每个分区内进行排序更有效,因为它可以将排序推入洗牌机制。

Actions

下面列出了Spark支持的常用的action操作。详细请参考RDD API文档(ScalaJavaPythonR)和键值对RDD方法文档(ScalaJava)。

ActionMeaning
reduce(func)使用函数func聚合数据集的元素(它接受两个参数并返回一个)。这个函数应该是可交换的和结合律的,这样它就可以被正确地并行计算。
collect()在驱动程序中以数组形式返回数据集的所有元素。这通常在过滤器或其他操作返回足够小的数据子集之后有用。
count()返回数据集中元素的数量。
first()返回数据集的第一个元素(类似于take(1))。
take(n)返回一个包含数据集前n个元素的数组。
takeSample(withReplacement, num, [seed])返回一个包含数据集num元素的随机样本的数组,可选地预先指定一个随机数生成器种子,可替换或不替换。
takeOrdered(n, [ordering])使用RDD的自然顺序或自定义比较器返回RDD的前n个元素。
saveAsTextFile(path)将数据集的元素作为文本文件(或文本文件集)写入本地文件系统、HDFS或任何其他hadoop支持的文件系统的给定目录中。Spark将对每个元素调用toString,将其转换为文件中的一行文本。
saveAsSequenceFile(path)
(Java and Scala)
将数据集的元素作为Hadoop SequenceFile写入本地文件系统、HDFS或任何其他Hadoop支持的文件系统的给定路径中。这在实现Hadoop的可写接口的键值对的rdd上可用。在Scala中,它也适用于隐式转换为Writable的类型(Spark包括基本类型的转换,如Int、Double、String等)。
saveAsObjectFile(path)
(Java and Scala)
使用Java序列化以简单格式编写数据集的元素,然后可以使用SparkContext.objectFile()加载。
countByKey()仅在类型为(K, V)的rdd上可用。返回一个(K, Int)对的哈希映射,包含每个键的计数。
foreach(func)对数据集中每个元素使用函数func进行处理。该操作通常用于更新一个累加器(Accumulator)或与外部数据源进行交互。注意:在foreach()之外修改累加器变量可能引起不确定的后果。详细介绍请阅读Understanding closures部分。

Shuffle操作(Shuffle operations)

Spark内的一个操作将会触发shuffle事件。shuffle是Spark将多个分区的数据重新分组重新分布数据的机制。shuffle是一个复杂且代价较高的操作,它需要完成将数据在executor和机器节点之间进行复制的工作。

背景(Background)

通过reduceByKey操作的例子,来理解shuffle过程。reduceByKey操作生成了一个新的RDD,原始数据中相同key的所有记录的聚合值合并为一个元组,这个元组中的key对应的值为执行reduce函数之后的结果。这个操作的挑战是,key相同的所有记录不在同一各分区种,甚至不在同一台机器上,但是该操作必须将这些记录联合运算。

在Spark中,通常一条数据不会垮分区分布,除非为了一个特殊的操作在必要的地方才会跨分区分布。在计算过程中,一个分区由一个task进行处理。因此,为了组织所有的数据让一个reduceByKey任务执行,Spark需要进行一个all-to-all操作。all-to-all操作需要读取所有分区上的数据的所有的key,以及key对应的所有的值,然后将多个分区上的数据进行汇总,并将每个key对应的多个分区的数据进行计算得出最终的结果,这个过程称为shuffle。

虽然每个分区中新shuffle后的数据元素是确定的,分区间的顺序也是确定的,但是所有的元素是无序的。如果想在shuffle操作后将数据按指定规则进行排序,可以使用下面的方法:

  • 使用mapPartitions操作在每个分区上进行排序,排序可以使用.sorted等方法。
  • 使用repartitionAndSortWithinPartitions操作在重新分区的同时高效的对分区进行排序。
  • 使用sortBy将RDD进行排序。

会引起shuffle过程的操作有:

  • repartition操作,例如:repartitioncoalesce
  • ByKey操作(除了counting相关操作),例如:groupByKeyreduceByKey
  • join操作,例如:cogroupjoin
性能影响(Performance Impact)

shuffle是一个代价比较高的操作,它涉及磁盘IO、数据序列化、网络IO。为了准备shuffle操作的数据,Spark启动了一系列的map任务和reduce任务,map任务完成数据的处理工作,reduce完成map任务处理后的数据的收集工作。这里的map、reduce来自MapReduce,跟Spark的map操作和reduce操作没有关系。

在内部,一个map任务的所有结果数据会保存在内存,直到内存不能全部存储为止。然后,这些数据将基于目标分区进行排序并写入一个单独的文件中。在reduce时,任务将读取相关的已排序的数据块。

某些shuffle操作会大量消耗堆内存空间,因为shuffle操作在数据转换前后,需要在使用内存中的数据结构对数据进行组织。需要特别说明的是,reduceByKeyaggregateByKey在map时会创建这些数据结构,ByKey操作在reduce时创建这些数据结构。当内存满的时候,Spark会把溢出的数据存到磁盘上,这将导致额外的磁盘IO开销和垃圾回收开销的增加。

shuffle操作还会在磁盘上生成大量的中间文件。在Spark 1.3中,这些文件将会保留至对应的RDD不在使用并被垃圾回收为止。这么做的好处是,如果在Spark重新计算RDD的血统关系(lineage)时,shuffle操作产生的这些中间文件不需要重新创建。如果Spark应用长期保持对RDD的引用,或者垃圾回收不频繁,这将导致垃圾回收的周期比较长。这意味着,长期运行Spark任务可能会消耗大量的磁盘空间。临时数据存储路径可以通过SparkContext中设置参数spark.local.dir进行配置。

shuffle操作的行为可以通过调节多个参数进行设置。详细的说明请看Configuration Guide中的“Shuffle Behavior”部分。

RDD持久化(RDD Persistence)

Spark中一个很重要的能力是将数据持久化(或称为缓存),在多个操作间都可以访问这些持久化的数据。当持久化一个RDD时,每个节点会将本节点计算的数据块存储到内存,在该数据上的其他action操作将直接使用内存中的数据。这样会让以后的action操作计算速度加快(通常运行速度会加速10倍)。缓存是迭代算法和快速的交互式使用的重要工具。

RDD可以使用persist()方法或cache()方法进行持久化。数据将会在第一次action操作时进行计算,并在各个节点的内存中缓存。Spark的缓存具有容错机制,如果一个缓存的RDD的某个分区丢失了,Spark将按照原来的计算过程,自动重新计算并进行缓存。

另外,每个持久化的RDD可以使用不同的存储级别进行缓存,例如,持久化到磁盘、已序列化的Java对象形式持久化到内存(可以节省空间)、跨节点间复制、以off-heap的方式存储在 Tachyon。这些存储级别通过传递一个StorageLevel对象(ScalaJavaPython)给persist()方法进行设置。cache()方法是使用默认存储级别的快捷设置方法,默认的存储级别是StorageLevel.MEMORY_ONLY(将反序列化的对象存储到内存中)。详细的存储级别介绍如下:

  • MEMORY_ONLY:将RDD以反序列化Java对象的形式存储在JVM中。如果内存空间不够,部分数据分区将不再缓存,在每次需要用到这些数据时重新进行计算。这是默认的级别。
  • MEMORY_AND_DISK:将RDD以反序列化Java对象的形式存储在JVM中。如果内存空间不够,将未缓存的数据分区存储到磁盘,在需要使用这些分区时从磁盘读取。
  • MEMORY_ONLY_SER:将RDD以序列化的Java对象的形式进行存储(每个分区为一个byte数组)。这种方式会比反序列化对象的方式节省很多空间,尤其是在使用 fast serializer时会节省更多的空间,但是在读取时会增加CPU的计算负担。
  • MEMORY_AND_DISK_SER:类似于MEMORY_ONLY_SER,但是溢出的分区会存储到磁盘,而不是在用到它们时重新计算。
  • DISK_ONLY:只在磁盘上缓存RDD。
  • MEMORY_ONLY_2,MEMORY_AND_DISK_2,等等:与上面的级别功能相同,只不过每个分区在集群中两个节点上建立副本。
  • OFF_HEAP (实验中):以序列化的格式 (serialized format) 将 RDD存储到 Tachyon。相比于MEMORY_ONLY_SER, OFF_HEAP 降低了垃圾收集(garbage collection)的开销,使得 executors变得更小,而且共享了内存池,在使用大堆(heaps)和多应用并行的环境下有更好的表现。此外,由于 RDD存储在Tachyon中, executor的崩溃不会导致内存中缓存数据的丢失。在这种模式下, Tachyon中的内存是可丢弃的。因此,Tachyon不会尝试重建一个在内存中被清除的分块。如果你打算使用Tachyon进行off heap级别的缓存,Spark与Tachyon当前可用的版本相兼容。详细的版本配对使用建议请参考Tachyon的说明

注意,在Python中,缓存的对象总是使用Pickle进行序列化,所以在Python中不关心你选择的是哪一种序列化级别。

在shuffle操作中(例如reduceByKey),即便是用户没有调用persist方法,Spark也会自动缓存部分中间数据。这么做的目的是,在shuffle的过程中某个节点运行失败时,不需要重新计算所有的输入数据。如果用户想多次使用某个RDD,强烈推荐在该RDD上调用persist方法。

如何选择存储级别(Which Storage Level to Choose?)

Spark的存储级别的选择,核心问题是在内存使用率和CPU效率之间进行权衡。建议按下面的过程进行存储级别的选择:

  • 如果使用默认的存储级别(MEMORY_ONLY),存储在内存中的RDD没有发生溢出,那么就选择默认的存储级别。默认存储级别可以最大程度的提高CPU的效率,可以使在RDD上的操作以最快的速度运行。
  • 如果内存不能全部存储RDD,那么使用MEMORY_ONLY_SER,并挑选一个快速序列化库将对象序列化,以节省内存空间。使用这种存储级别,计算速度仍然很快。
  • 除了在计算该数据集的代价特别高,或者在需要过滤大量数据的情况下,尽量不要将溢出的数据存储到磁盘。因为,重新计算这个数据分区的耗时与从磁盘读取这些数据的耗时差不多。
  • 如果想快速还原故障,建议使用多副本存储界别(例如,使用Spark作为web应用的后台服务,在服务出故障时需要快速恢复的场景下)。所有的存储级别都通过重新计算丢失的数据的方式,提供了完全容错机制。但是多副本级别在发生数据丢失时,不需要重新计算对应的数据库,可以让任务继续运行。
  • 在高内存消耗或者多任务的环境下,还处于实验性的OFF_HEAP模式有下列几个优势:
    • 它支持多个executor使用Tachyon中的同一个内存池。
    • 它显著减少了内存回收的代价。
    • 如果个别executor崩溃掉,缓存的数据不会丢失。

移除数据(Removing Data)

Spark自动监控各个节点上的缓存使用率,并以最近最少使用的方式(LRU)将旧数据块移除内存。如果想手动移除一个RDD,而不是等待该RDD被Spark自动移除,可以使用RDD.unpersist()方法。

共享变量(Shared Variables)

通常情况下,一个传递给Spark操作(例如mapreduce)的方法是在远程集群上的节点执行的。方法在多个节点执行过程中使用的变量,是同一份变量的多个副本。这些变量的以副本的方式拷贝到每个机器上,各个远程机器上变量的更新并不会传回driver程序。然而,为了满足两种常见的使用场景,Spark提供了两种特定类型的共享变量:广播变量(broadcast variables)和累加器(accumulators)。

广播变量(broadcast variables)

广播变量允许编程者将一个只读变量缓存到每台机器上,而不是给每个任务传递一个副本。例如,广播变量可以用一种高效的方式给每个节点传递一份比较大的数据集副本。在使用广播变量时,Spark也尝试使用高效广播算法分发变量,以降低通信成本。

Spark的action操作是通过一些列的阶段(stage)进行执行的,这些阶段(stage)是通过分布式的shuffle操作进行切分的。Spark自动广播在每个阶段内任务需要的公共数据。这种情况下广播的数据使用序列化的形式进行缓存,并在每个任务在运行前进行反序列化。这明确说明了,只有在跨越多个阶段的多个任务任务会使用相同的数据,或者在使用反序列化形式的数据特别重要的情况下,使用广播变量会有比较好的效果。

广播变量通过在一个变量v上调用SparkContext.broadcast(v)方法进行创建。广播变量是v的一个封装器,可以通过value方法访问v的值。代码示例如下:

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)

累加器只允许关联操作进行"added"操作,因此在并行计算中可以支持特定的计算。累加器可以用于实现计数(类似在MapReduce中那样)或者求和。原生Spark支持数值型的累加器,编程者可以添加新的支持类型。创建累加器并命名之后,在Spark的UI界面上将会显示该累加器。这样可以帮助理解正在运行的阶段的运行情况(注意,在Python中还不支持)。

一个累加器可以通过在原始值v上调用SparkContext.accumulator(v)。然后,集群上正在运行的任务就可以使用add方法或+=操作对该累加器进行累加操作。只有driver程序可以读取累加器的值,读取累加器的值使用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

上面的代码示例使用的是Spark内置的Int类型的累加器,开发者可以通过集成AccumulatorParam类创建新的累加器类型。AccumulatorParam接口有两个方法:zero方法和addInPlace方法。zero方法给数据类型提供了一个0值,addInPlace方法能够将两个值进行累加。例如,假设我们有一个表示数学上向量的Vector类,我们可以写成:

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)

Spark也支持使用更通用的 Accumulable接口去累加数据,其结果数据的类型和累加的元素类型不同(例如,通过收集数据元素创建一个list)。在Scala中,SparkContext.accumulableCollection方法可用于累加常用的Scala集合类型。

累加器的更新只发生在action操作中,Spark保证每个任务只能更新累加器一次,例如重新启动一个任务,该重启的任务不允许更新累加器的值。在transformation用户需要注意的是,如果任务过job的阶段重新执行,每个任务的更新操作将会执行多次。

累加器没有改变Spark懒执行的模式。如果累加器在RDD中的一个操作中进行更新,该累加器的值只在该RDD进行action操作时进行更新。因此,在一个像map()这样的转换操作中,累加器的更新并没有执行。下面的代码片段证明了这个特性

val accum = sc.accumulator(0)
data.map { x => accum += x; f(x) }
// Here, accum is still 0 because no actions have caused the <code>map</code> to be computed.

将应用提交到集群(Deploying to a Cluster)

应用提交手册描述了如何将应用提交到集群。简单的说,当你将你的应用打包成一个JAR(Java/Scala)或者一组.py.zip文件(Python)后,就可以通过bin/spark-submit脚本将脚本提交到集群支持的管理器中。

Java/Scala中启动Spark作业(Launching Spark jobs from Java / Scala)

使用org.apache.spark.launcher包提供的简单的Java API,可以将Spark作业以该包中提供的类的子类的形式启动。

单元测试(Unit Testing)

Spark可以友好的使用流行的单元测试框架进行单元测试。在test中简单的创建一个SparkContext,master的URL设置为local,运行几个操作,然后调用SparkContext.stop()将该作业停止。因为Spark不支持在同一个程序中运行两个context,所以需要请确保使用finally块或者测试框架的tearDown方法将context停止。

从Spark1.0之前的版本迁移(Migrating from pre­1.0 Versions of Spark)

Spark 1.0冻结了1.X系列的Spark核的API,因此,当前没有标记为"experimental"或者“developer API”的API都将在未来的版本中进行支持。

  • Scala的变化

对于Scala的变化是,分组操作(例如groupByKeycogroupjoin)的返回类型由(Key,Seq[Value])变为(Key,Iterable[Value])

  • Java API的变化
    • 1.0中org.apache.spark.api.java.function类中的Function类变成了接口,这意味着旧的代码中extends Function应该改为implement Function
    • 增加了新的map型操作,例如mapToPairmapToDouble,增加的这些操作可用于创建特殊类型的RDD。
    • 分组操作(例如groupByKeycogroupjoin)的返回类型由(Key,Seq[Value])变为(Key,Iterable[Value])

这些迁移指导对Spark Streaming、MLlib和GraphX同样有效。

下一步(Where to Go from Here)

你可以在Spark网站看一些Spark编程示例。另外,Spark在examples目录下包含了许多例子(ScalaJavaPythonR)。运行Java和Scala例子,可以通过将例子的类名传给Spark的bin/run-example脚本进行启动。例如:

./bin/run-example SparkPi

Python示例,使用spark-submit命令提交:

./bin/spark-submit examples/src/main/python/pi.py

R示例,使用spark-submit命令提交:

./bin/spark-submit examples/src/main/r/dataframe.R

configurationtuning手册中,有许多优化程序的实践。这些优化建议,能够确保你的数据以高效的格式存储在内存中。对于部署的帮助信息,请阅读cluster mode overview ,该文档描述了分布式操作和支持集群管理器的组件。

最后,完整的API文档请查阅ScalaJavaPythonR

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值