spark之4:基础指南(源自官方文档)

@(SPARK)[spark, 大数据]


  • spark之4基础指南源自官方文档
  • 一简介
  • 二接入Spark
  • 三初始化Spark
  • 一使用Shell
  • 四弹性分布式数据集RDDs
  • 一并行集合
  • 二外部数据集
  • 三RDD操作
  • 1基础操作
  • 2向Spark传递函数
  • 3理解闭包
  • 例子
  • 本地模式VS集群模式
  • 打印RDD中的元素
  • 4键值对的使用
  • 5转换
  • 6动作
  • 7洗牌操作
  • 背景
  • 性能影响
  • 四RDD持久化
  • 1如何选择存储级别
  • 2移除数据
  • 五共享变量
  • 二累加器
  • 六把代码部署到集群上
  • 七从JavaScala中启动Spark作业
  • 八单元测试


参考:

英文:https://spark.apache.org/docs/latest/programming-guide.html

以下大部分内容来自于中文版本,便其是针对spark1.2.1的,如有疑问,请直接参考英文版本。

一、简介

总的来说,每一个Spark的应用,都是由一个驱动程序(driver program)构成,它运行用户的main函数,在一个集群上执行各种各样的并行操作。Spark提出的最主要抽象概念是弹性分布式数据集 (resilientdistributed dataset,RDD),它是一个元素集合,划分到集群的各个节点上,可以被并行操作。RDDs的创建可以从HDFS(或者任意其他支持Hadoop文件系统)上的一个文件开始,或者通过转换驱动程序(driver program)中已存在的Scala集合而来。用户也可以让Spark保留一个RDD在内存中,使其能在并行操作中被有效的重复使用。最后,RDD能自动从节点故障中恢复。

Spark的第二个抽象概念是共享变量(shared variables),可以在并行操作中使用。在默认情况下,Spark通过不同节点上的一系列任务来运行一个函数,它将每一个函数中用到的变量的拷贝传递到每一个任务中。有时候,一个变量需要在任务之间,或任务与驱动程序之间被共享。Spark支持两种类型的共享变量:广播变量,可以在内存的所有的结点上缓存变量;累加器:只能用于做加法的变量,例如计数或求和。

本指南将用每一种Spark支持的语言来展示这些特性。这都是很容易来跟着做的如果你启动了Spark的交互式Shell或者Scala的bin/spark-shell或者Python的bin/pyspark。

二、接入Spark

Spark1.5需要和Scala2.10一起使用。如果你要用Scala来编写应用,你需要用一个相应版本的Scala(例如2.10.X)。

要写一个Spark应用程序,你需要在添加Spark的Maven依赖,Spark可以通过Maven中心库来获得:

groupId = org.apache.spark  
artifactId = spark-core_2.10  
version = 1.5.1

除此之外,如果你想访问一个HDFS集群,你需要根据你的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.SparkContext._  
import org.apache.spark.SparkConf

Java

Spark需要运行在Java6及更高版本上。如果你正在使用Java8,Spark支持使用Lambda表达式简洁地编写函数,或者你可以使用在org.apache.spark.api.java.function包中的类。

要使用Java编写Spark应用程序,你需要添加一个Spark的依赖。Spark可以通过Maven中心库获得:

groupId = org.apache.spark  
artifactId = spark-core_2.10  
version = 1.5.1

此外,如果你想访问一个HDFS集群,你需要根据你的HDFS版本,添加一个hadoop-client的依赖。一些通用的HDFS版本标签在第三方发行版页面列出。

groupId = org.apache.hadoop  
artifactId = hadoop-client  
version = <your-hdfs-version>

最后,你需要将Spark的类导入到你的程序中。添加如下行:

importorg.apache.spark.api.java.JavaSparkContext  
import org.apache.spark.api.java.JavaRDD  
import org.apache.spark.SparkConf

三、初始化Spark

Spark程序需要做的第一件事情,就是创建一个SparkContext对象,它将告诉Spark如何访问一个集群。要创建一个SparkContext你首先需要建立一个SparkConf对象,这个对象包含你的程序的信息。

每个JVM只能有一个活动的SparkContext。在创建一个新的SparkContext之前你必须stop()活动的SparkContext。

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

appName是你的应用的名称,将会在集群的Web监控UI中显示。master参数,是一个用于指定所连接的Spark,Mesos or Mesos 集群URL的字符串,也可以是一个如下面所描述的用于在local模式运行的特殊字符串“local”。在实践中,当运行在一个集群上时,你不会想把master硬编码到程序中,而是启动spark-submit来接收它。然而,对于本地测试和单元测试,你可以通过“local”模式运行Spark。

Spark程序需要做的第一件事情,就是创建一个JavaSparkContext对象,它将告诉Spark如何访问一个集群。要创建一个SparkContext你首先需要建立一个SparkConf对象,这个对象包含你的程序的信息。

SparkConf conf = new SparkConf().setAppName(appName).setMaster(master);  
JavaSparkContext sc = newJavaSparkContext(conf);

appName是你的应用的名称,将会在集群的Web监控UI中显示。master参数,是一个用于指定所连接的Spark,Mesos or Mesos 集群URL的字符串,也可以是一个如下面所描述的用于在local模式运行的特殊字符串“local”。在实践中,当运行在一个集群上时,你不会想把master硬编码到程序中,而是启动spark-submit来接收它。然而,对于本地测试和单元测试,你可以通过“local”模式运行Spark。

(一)使用Shell

在Spark shell中,一个特殊的解释器感知的SparkContext已经为你创建好了,变量名叫做sc。创建自己的SparkContext将不会生效。你可以使用-master参数设置context连接到那个master,并且你可以使用-jars参数把用逗号分隔的一个jar包列表添加到classpath中。例如,如果在四核CPU上运行spark-shell,使用:

$ ./bin/spark-shell --master local[4]

或者,同时在classpath中加入code.jar,使用:

$ ./bin/spark-shell --master local[4] --jarscode.jar

想要获得完整的选项列表,运行spark-shell –help。在背后,spark-shell调用更一般的spark-submit脚本。

四、弹性分布式数据集(RDDs)

Spark围绕的概念是弹性分布式数据集(RDD),是一个有容错机制并可以被并行操作的元素集合。目前有两种创建RDDs的方法:并行化一个在你的驱动程序中已经存在的集合,或者引用在外部存储系统上的数据集,例如共享文件系统,HDFS,HBase,或者任何以Hadoop输入格式提供的数据源。

(一)并行集合

并行集合是通过调用SparkContext的parallelize方法,在一个已经存在的集合上创建的(一个Scala Seq对象)。集合的对象将会被拷贝,创建出一个可以被并行操作的分布式数据集。例如,下面展示了怎样创建一个含有数字1到5的并行集合:

val data = Array(1, 2, 3, 4, 5)  
val distData = sc.parallelize(data)

一旦创建了分布式数据集(distData),就可以对其执行并行操作。例如,我们可以调用distData.reduce((a,b)=>a+b)来累加数组的元素。后续我们会进一步地描述对分布式数据集的操作。

并行集合的一个重要参数是分区数(the number of partitions),表示数据集切分的份数。Spark将在集群上为每个分区数据起一个任务。典型情况下,你希望集群的每个CPU分布2-4个分区(partitions)。通常,Spark会尝试基于集群状况自动设置分区数。然而,你也可以进行手动设置,通过将分区数作为第二个参数传递给parallelize方法来实现。(例如:sc.parallelize(data,10))。注意:代码中的一些地方使用属于“分片(分区的近义词)”来保持向后兼容。

Java

并行集合是通过对存在于驱动程序中的集合调用JavaSparkContext的parallelize方法来构建的。构建时会拷贝集合中的元素,创建一个可以被并行操作的分布式数据集。例如,这里演示了如何创建一个包含数字1到5的并行集合:

List data = Arrays.asList(1, 2,3, 4, 5);
JavaRDD distData =sc.parallelize(data);

一旦创建了分布式数据集(distData),就可以对其执行并行操作。例如,我们可以调用distData.reduce((a,b)=>a+b)来累加数组的元素。后续我们会进一步地描述对分布式数据集的操作。

注意:在本指南中,我们会经常使用简洁地Java8的lambda语法来指明Java函数,而在Java的旧版本中,你可以实现org.apache.spark.api.java.function包中的接口。下面我们将在把函数传递到Spark中描述更多的细节。

并行集合的一个重要参数是分区数(the number of partitions),表示数据集切分的份数。Spark将在集群上为每个分区数据起一个任务。典型情况下,你希望集群的每个CPU分布2-4个分区(partitions)。通常,Spark会尝试基于集群状况自动设置分区数。然而,你也可以进行手动设置,通过将分区数作为第二个参数传递给parallelize方法来实现。(例如:sc.parallelize(data,10))。注意:代码中的一些地方使用属于“分片(分区的近义词)”来保持向后兼容。

(二)外部数据集

Spark可以从Hadoop支持的任何存储源中构建出分布式数据集,包括你的本地文件系统,HDFS,Cassandre,HBase,Amazon S3等。Spark支持text files,Sequence files,以及其他任何一种Hadoop InputFormat。

Text file RDDs的创建可以使用SparkContext的textFile方法。该方法接受一个文件的URI地址(或者是机器上的一个本地路径,或者是一个hdfs://,s3n://等URI)作为参数,并读取文件的每一行数据,放入集合中,下面是一个调用例子:

scala> val distFile =sc.textFile("data.txt")  
distFile: RDD[String] = MappedRDD@1d4cee08

一旦创建完成,就可以在distFile上执行数据集操作。例如,要相对所有行的长度进行求和,我们可以通过如下的map和reduce操作来完成:

distFile.map(s => s.length).reduce((a, b)=> a + b)

Spark读文件时的一些注意事项:

  1. 如果文件使用本地文件系统上的路径,那么该文件必须在工作节点的相同路径下也可以访问。可以将文件拷贝到所有的worker节点上,或者使用network-mounted共享文件系统。
  2. Spark的所有基于文件的输入方法,包括textFile,支持在目录上运行,压缩文件和通配符。例如,你可以使用textFile(”/my/directory”),textFile(“/my/directory/.txt”),和textFile(“/my/directory/.gz”)。
  3. textFile方法也带有可选的第二个参数,用于控制文件的分区数。默认情况下,Spark会为文件的每一个block创建一个分区,但是你也可以通过传入更大的值,来设置更高的分区数。注意,你设置的分区数不能比文件的块数小。

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

  1. SparkContext.wholeTextFiles可以让你读取包含多个小text文件的目录,并且每个文件对应返回一个(filename,content)对。而对应的textFile方法,文件的每一行对应返回一条记录(record)。
  2. 对于Sequence文件,使用SparkContext的sequenceFile[K,V]方法,其中K和V分别对应文件中key和values的类型。这些类型必须是Hadoop的Writable接口的子类,如IntWritable和Text。另外,Spark允许你使用一些常见的Writables的原生类型;例如,sequenceFile[Int,String]会自动的转换为类型IntWritables和Texts。
  3. 对于其他的Hadoop InputFormats,你可以使用SparkContext.hadoopRDD方法,它可以接受一个任意类型的JobConf和输入格式类,key类和value类。像Hadoop Job设置输入源那样去设置这些参数即可。对基于“新”的MapReduce API(org.apache.hadoop.mapreduce)的InputFormats,你也可以使用SparkContex.newHadoopRDD。
  4. RDD.saveAsObjectFile和SparkContext.objectFile支持由序列化的Java对象组成的简单格式来保存RDD。虽然这不是一种像Avro那样有效的序列化格式,但是她提供了一种可以存储任何RDD的简单方式。

Java

Spark可以从Hadoop支持的任何存储源中构建出分布式数据集,包括你的本地文件系统,HDFS,Cassandre,HBase,Amazon S3等。Spark支持text files,Sequence files,以及其他任何一种Hadoop InputFormat。

Text file RDDs的创建可以使用SparkContext的textFile方法。该方法接受一个文件的URI地址(或者是机器上的一个本地路径,或者是一个hdfs://,s3n://等URI)作为参数,并读取文件的每一行数据,放入集合中,下面是一个调用例子:

JavaRDD<String> distFile =sc.textFile("data.txt");

一旦创建完成,就可以在distFile上执行数据集操作。例如,要相对所有行的长度进行求和,我们可以通过如下的map和reduce操作来完成:

distFile.map(s -> s.length()).reduce((a, b)-> a + b)

(三)RDD操作

RDDs支持两种操作:转换(transformations),可以从已有的数据集创建一个新的数据集;而动作(actions),在数据集上运行计算后,会向驱动程序返回一个值。例如,map就是一种转换,它将数据集每一个元素都传递给函数,并返回一个新的分布数据集来表示结果。另一方面,reduce是一种动作,通过一些函数将所有的元素聚合起来,并将最终结果返回给驱动程序(不过还有一个并行的reduceByKey,能返回一个分布式数据集)。

Spark中的所有转换都是惰性的,也就是说,它们并不会马上计算结果。相反的,它们只是记住应用到基础数据集(例如一个文件)上的这些转换动作。只有当发生一个要求返回结果给驱动程序的动作时,这些转换才会真正运行。这种设计让Spark更加有效率地运行。例如,我们对map操作创建的数据集进行reduce操作时,只会向驱动返回reduce操作的结果,而不是返回更大的map操作创建的数据集。

默认情况下,每一个转换过的RDD都会在你对它执行一个动作时被重新计算。不过,你也可以使用持久化或者缓存方法,把一个RDD持久化到内存中。在这种情况下,Spark会在集群中保存相关元素,以便你下次查询这个RDD时,能更快速地访问。对于把RDDs持久化到磁盘上,或在集群中复制到多个节点也是支持的。

1、基础操作

为了描述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作为map转换结果。此外,由于惰性,不会立即计算lineLengths。最后,我们运行reduce,这是一个动作。这时候,Spark才会将这个计算拆分成不同的task,并运行在独立的机器上,并且每台机器运行它自己的map部分和本地的reducatin,仅仅返回它的结果给驱动程序。

如果我们希望以后可以复用lineLengths,可以添加:

lineLengths.persist()

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

Java

为了描述RDD的基础操作,可以考虑下面的简单程序:

JavaRDD<String> lines =sc.textFile("data.txt");  
JavaRDD<Integer> lineLengths =lines.map(s -> s.length());  
int totalLength = lineLengths.reduce((a, b)-> a + b);

第一行通过一个外部文件定义了一个基本的RDD。这个数据集未被加载到内存,也未在上面执行操作:lines仅仅指向这个文件。第二行定义了lineLengths作为map转换结果。此外,由于惰性,不会立即计算lineLengths。最后,我们运行reduce,这是一个动作。这时候,Spark才会将这个计算拆分成不同的task,并运行在独立的机器上,并且每台机器运行它自己的map部分和本地的reducatin,仅仅返回它的结果给驱动程序。

如果我们希望以后可以复用lineLengths,可以添加:

lineLengths.persist();

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

2、向Spark传递函数

Scala

Spark的API,在很大程度上依赖于把驱动程序中的函数传递到集群上运行。这有两种推荐的实现方式:

●使用匿名函数的语法,这可以用来替换简短的代码。

●使用全局单例对象的静态方法。比如,你可以定义函数对象objectMyFunctions,然后传递该对象的方法MyFunction.func1,如下所示:

object MyFunctions {  
  def func1(s: String): String = { ... }  
}  

myRdd.map(MyFunctions.func1)

注意:由于可能传递的是一个类实例方法的引用(而不是一个单例对象),在传递方法的时候,应该同时传递包含该方法的对象。比如,考虑:

class MyClass {  
  def func1(s: String): String = { ... }  
  defdoStuff(rdd: RDD[String]): RDD[String] = { rdd.map(func1) }  
}

这里,如果我们创建了一个类实例new MyClass,并且调用了实例的doStuff方法,该方法中的map处调用了这个MyClass实例的func1方法,所以需要将整个对象传递到集群中。类似于写成:

rdd.map(x=>this.func1(x))。

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

class MyClass {  
  valfield = "Hello"  
  defdoStuff(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] = {  
  valfield_ = this.field  
 rdd.map(x => field_ + x)  
}

3、理解闭包

关于Spark的一个更困难的问题是理解当在一个集群上执行代码的时候,变量和方法的范围以及生命周期。修改范围之外变量的RDD操作经常是造成混乱的源头。在下面的例子中我们看一下使用foreach()来增加一个计数器的代码,不过同样的问题也可能有其他的操作引起。

例子

考虑下面的单纯的RDD元素求和,根据是否运行在一个虚拟机上,它们的行为完全不同。一个平常的例子是在local模式(–master=local[n])下运行Spark对比将Spark程序部署到一个集群上(例如通过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)
本地模式VS集群模式

主要的挑战是,上述代码的行为是未定义的。在使用单个JVM的本地模式中,上面的代码会在RDD中计算值的总和并把它存储到计数器中。这是因为RDD和计数器变量在驱动节点的同一个内存空间中。

然而,在集群模式下,发生的事情更为复杂,上面的代码可能不会按照目的工作。要执行作业,Spark将RDD操作分成任务——每个任务由一个执行器操作。在执行前,Spark计算闭包。闭包是指执行器要在RDD上进行计算时必须对执行节点可见的那些变量和方法(在这里是foreach())。这个闭包被序列化并发送到每一个执行器。在local模式下,只有一个执行器因此所有东西都分享同一个闭包。然而在其他的模式中,就不是这个情况了,运行在不同工作节点上的执行器有它们自己的闭包的一份拷贝。

这里发生的事情是闭包中的变量被发送到每个执行器都是被拷贝的,因此,当计数器在foreach函数中引用时,它不再是驱动节点上的那个计数器了。在驱动节点的内存中仍然有一个计数器,但它对执行器来说不再是可见的了!执行器只能看到序列化闭包中的拷贝。因此,计数器最终的值仍然是0,因为所有在计数器上的操作都是引用的序列化闭包中的值。

在这种情况下要确保一个良好定义的行为,应该使用累加器。Spark中的累加器是一个专门用来在执行被分散到一个集群中的各个工作节点上的情况下安全更新变量的机制。本指南中的累加器部分会做详细讨论。

一般来说,闭包-构造像循环或者本地定义的方法,不应该用来改变一些全局状态。Spark没有定义或者是保证改变在闭包之外引用的对象的行为。一些这样做的代码可能会在local模式下起作用,但那仅仅是个偶然,这样的代码在分布式模式下是不会按照期望工作的。如果需要一些全局的参数,可以使用累加器。

打印RDD中的元素

另一个常见的用法是使用rdd.foreach(println)方法或者rdd.map(println)方法试图打印出RDD中的元素。在一台单一的机器上,这样会产生期望的输出并打印出RDD中的元素。然而,在集群模式中,被执行器调用输出到stdout的输出现在被写到了执行器的stdout,并不是在驱动上的这一个,因此驱动上的stdout不会显示这些信息!要在驱动上打印所有的元素,可以使用collect()方法首先把RDD取回到驱动节点如:rdd.collect().foreach(println)。然而,这可能导致驱动内存溢出,因为collect()将整个RDD拿到了单台机器上;如果你只需要打印很少几个RDD的元素,一个更安全的方法是使用take()方法:rdd.take(100).foreach(println)。

4、键值对的使用

虽然,在包含任意类型的对象的RDDs中,可以使用大部分的Spark操作,但也有一些特殊的操作只能在键值对的RDDs上使用。最常见的一个就是分布式的洗牌(shuffle)操作,诸如基于key值对元素进行分组或聚合的操作。

在Scala中,包含二元组(Tuple2)对象(可以通过简单地(a,b)代码,来构建内置于语言中的元组的RDDs支持这些操作),只要你在程序中导入了org.apache.spark.SparkContext._,就能进行隐式转换。PairRDDFunction类支持键值对的操作,如果你导入了隐式转换,该类型就能自动地对元组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()文档大纲中列出的规定。

5、转换

下表中列出了 Spark支持的一些常见的转换 (Transformations)。详情请参考 RDDAPI文档 (Scala, Java, Python)和 pair RDD函数文档 (Scala, Java)。

https://spark.apache.org/docs/latest/programming-guide.html#transformations

以后再补充中文,详细请见常用api demo

6、动作

下表中列出了 Spark支持的一些常见的动作 (actions)。详情请参考 RDD API文档

https://spark.apache.org/docs/latest/programming-guide.html#transformations

以后再补充中文,详细请见常用api demo

7、洗牌操作

Spark触发一个事件后进行的一些操作成为洗牌。洗牌是Spark重新分配数据的机制,这样它就可以跨分区分组。这通常涉及在执行器和机器之间复制数据,这就使得洗牌是一个复杂和高代价的操作。

背景

为了理解在洗牌的时候发生了什么,我们可以考虑reduceByKey操作的例子。reduceByKey操作产生了一个新的RDD,在这个RDD中,所有的单个的值被组合成了一个元组,key和执行一个reduce函数后的结果中与这个key有关的所有值。面临的挑战是一个key的所有的值并不都是在同一个分区上的,甚至不是一台机器上的,但是他们必须是可连接的以计算结果。

在Spark中,数据一般是不会跨分区分布的,除非是在一个特殊的地方为了某种特定的目的。在计算过程中,单个任务将在单个分区上操作——因此,为了组织所有数据执行单个reduceByKey中的reduce任务,Spark需要执行一个all-to-all操作。它必须读取所有分区,找到所有key的值,并跨分区把这些值放到一起来计算每个key的最终结果——这就叫做洗牌。

尽管在每个分区中新洗牌的元素集合是确定性的,分区本身的顺序也同样如此,这些元素的顺序就不一定是了。如果期望在洗牌后获得可预测的有序的数据,可以使用:

mapPartitions 来排序每个分区,例如使用.sorted

repartitionAndSortWithinPartitions 在重新分区的同时有效地将分区排序

sortBy来创建一个全局排序的RDD

可以引起洗牌的操作有重分区例如repartition和coalesce,‘ByKey操作(除了计数)像groupByKey和reduceByKey,还有join操作例如cogroup和join。

性能影响

Shuffle是一个代价高昂的操作,因为它调用磁盘I/O,数据序列化和网络I/O。要组织shuffle的数据,Spark生成一个任务集合——map任务来组织数据,并使用一组reduce任务集合来聚合它。它的命名来自与MapReduce,但并不直接和Spark的map和reduce操作相关。

在内部,单个的map任务的结果被保存在内存中,直到他们在内存中存不下为止。然后,他们基于目标分区进行排序,并写入到一个单个的文件中。在reduce这边,任务读取相关的已经排序的块。

某些shuffle操作会消耗大量的堆内存,因为他们用在内存中的数据结构在转换操作之前和之后都要对数据进行组织。特别的,reduceByKey和aggregateByKey在map侧创建这些结构,‘ByKey操作在reduce侧生成这些结构。当数据在内存中存不下时,Spark会将他们存储到磁盘,造成额外的磁盘开销和增加垃圾收集。

Shuffle也会在磁盘上产生大量的中间文件。在Spark1.3中,这些文件直到Spark停止运行时才会从Spark的临时存储中清理掉,这意味着长时间运行Spark作业会消耗可观的磁盘空间。这些做了之后如果lineage重新计算了,那shuffle不需要重新计算了。在配置Spark上下文时,临时存储目录由spark.local.dir配置参数指定。

Shuffle的行为可以通过调整各种配置参数来调整。请看Spark配置指南中的Shuffle Behavior部分。

(四)RDD持久化

Spark最重要的一个功能,就是在不同操作间,将一个数据集持久化(persisting) (或缓存(caching))到内存中。当你持久化(persist)一个 RDD,每一个节点都会把它计算的所有分区(partitions)存储在内存中,并在对数据集 (或者衍生出的数据集)执行其他动作(actioins)时重用。这将使得后续动作(actions)的执行变得更加迅速(通常快 10 倍)。缓存(Caching)是用 Spark 构建迭代算法和快速地交互使用的关键。

你可以使用 persist()或 cache()方法来持久化一个 RDD。在首次被一个动作(action)触发计算后,它将会被保存到节点的内存中。 Spark 的缓存是带有容错机制的,如果 RDD丢失任何一个分区的话,会自动地用原先构建它的转换(transformations)操作来重新进行计算。

此外,每一个被持久化的 RDD都可以用不同的存储级别(storage level)进行存储,比如,允许你持久化数据集到硬盘,以序列化的 Java对象(节省空间)存储到内存,跨节点复制,或者以off-heap的方式存储在 Tachyon。这些级别的选择,是通过将一个 StorageLevel对象 (Scala Java, Python)传递到 persist()方法中进行设置的。 cache()方法是使用默认存储级别的快捷方法,也就是 StorageLevel.MEMORY_ONLY (将反序列化 (deserialized)的对象存入内存)。完整的可选存储级别如下:

https://spark.apache.org/docs/latest/programming-guide.html#rdd-persistence
以后再补充中文

1、如何选择存储级别?

Spark 的存储级别旨在满足内存使用和CPU效率权衡上的不同需求。我们建议通过以下方法进行选择:

●如果你的 RDDs可以很好的与默认的存储级别(MEMORY_ONLY)契合,就不需要做任何修改了。这已经是 CPU使用效率最高的选项,它使得RDDs的操作尽可能的快。

●如果不行,试着使用 MEMORY_ONLY_SER,并且选择一个快速序列化库使对象在有比较高的空间使用率(space-efficient)的情况下,依然可以较快被访问。

●尽可能不要存储到硬盘上,除非计算数据集的函数的计算量特别大,或者它们过滤了大量的数据。否则,重新计算一个分区的速度,可能和从硬盘中读取差不多快。

●如果你想有快速的故障恢复能力,使用复制存储级别(例如:用 Spark来响应 web应用的请求)。所有的存储级别都有通过重新计算丢失的数据来恢复错误的容错机制,但是复制的存储级别可以让你在 RDD 上持续地运行任务,而不需要等待丢失的分区被重新计算。

●在大量的内存或多个应用程序的环境下,试验性的 OFF_HEAP模式具有以下几个优点:

o 允许多个 executors共享 Tachyon中相同的内存池。

o 极大地降低了垃圾收集器(garbage collection)的开销。

o 即使个别的 executors崩溃了,缓存的数据也不会丢失。

2、移除数据

Spark 会自动监控各个节点上的缓存使用情况,并使用最近最少使用算法(least-recently-used (LRU))删除老的数据分区。如果你想手动移除一个 RDD,而不是等它自动从缓存中清除,可以使用 RDD.unpersist()方法。

五、共享变量

一般来说,当一个函数被传递给一个在远程集群节点上运行的 Spark操作(例如 map或 reduce) 时,它操作的是这个函数用到的所有变量的独立拷贝。这些变量会被拷贝到每一台机器,而且在远程机器上对变量的所有更新都不会被传播回驱动程序。通常看来,读-写任务间的共享变量显然不够高效。然而,Spark还是为两种常见的使用模式,提供了两种有限的共享变量:广播变量(broadcast variables)和累加器(accumulators)。

(一)广播变量

广播变量允许程序员保留一个只读的变量,缓存在每一台机器上,而不是每个任务保存一份拷贝。它们可以这样被使用,例如,以一种高效的方式给每个节点一个大的输入数据集。Spark会尝试使用一种高效的广播算法来传播广播变量,从而减少通信的代价。

Spark动作的执行是通过一个阶段的集合,通过分布式的Shuffle操作分离。Spark自动广播在每个阶段里任务需要的共同数据。以这种方式广播的数据以序列化的形式缓存并在运行每个任务之前进行反序列化。这意味着显式地创建广播变量只在当多个阶段之间需要相同的数据或者是当用反序列化的形式缓存数据特别重要的时候。

广播变量是通过调用 SparkContext.broadcast(v)方法从变量 v创建的。广播变量是一个 v的封装器,它的值可以通过调用 value方法获得。如下代码展示了这个:

Scala

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不能在广播后修改,这样可以保证所有节点具有相同的广播变量的值(比如,后续如果变量被传递到一个新的节点)。

(二)累加器

累加器是一种只能通过具有结合性的操作(associative operation)进行“加(added)”的变量,因此可以高效地支持并行。它们可以用来实现计数器(如 MapReduce 中)和求和器。 Spark原生就支持数值类型的累加器,开发者可以自己添加新的支持类型。如果创建了一个命名的累加器(accumulators),这些累加器将会显示在 Spark UI 界面上。这对于了解当前运行阶段(stages)的进展情况是非常有用的(注意:这在 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: Tasksfinished in 0.317106 s  

scala> accum.value  
res2: Int = 10

虽然代码可以使用内置支持的 Int类型的累加器,但程序员也可以通过子类化(subclassing) AccumulatorParam来创建自己的类型。AccumulatorParam接口有两个方法: zero,为你的数据类型提供了一个“零值(zero value)”,以及 addInPlace提供了两个值相加的方法。比如,假设我们有一个表示数学上向量的 Vector类,我们可以这么写:

object VectorAccumulatorParam extendsAccumulatorParam[Vector] {  
  defzero(initialValue: Vector): Vector = {  
   Vector.zeros(initialValue.size)  
  }  
  defaddInPlace(v1: Vector, v2: Vector): Vector = {  
    v1 +=v2  
  }  
}  

// Then, create an Accumulator of this type:  
val vecAccum = sc.accumulator(newVector(...))(VectorAccumulatorParam)

在 Scala中, Spark也支持更通用的 Accumulable接口去累加数据,其结果类型和累加的元素不同(比如,构建一个包含所有元素的列表),并且SparkContext.accumulableCollection方法可以累加普通的 Scala集合(collection)类型。

因为累加器的更新只在action中执行,Spark确保每个任务对累加器的更新都只会被应用一次,例如,重启任务将不会更新这个值。在转换中,用户应该清楚如果任务或者作业阶段是重复运行的,每个任务的更新可能会应用不止一次。

累加器不会改变Spark的懒惰评价模型。如果它们在一个RDD的操作中正在被更新,他们的值只会被更新一次,RDD作为动作的一部分被计算。因此,累加器更新当在执行一个懒惰转换,例如map()时,并不保证被执行。下面的代码段演示了这个属性:

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

六、把代码部署到集群上

应用程序提交指南(application submission guide)描述了如何将应用程序提交到一个集群,简单地说,一旦你将你的应用程序打包成一个JAR(对于 Java/Scala)或者一组的 .py或 .zip文件 (对于 Python), bin/spark-submit 脚本可以让你将它提交到支持的任何集群管理器中。

七、从Java/Scala中启动Spark作业

Org.apache.spark.launcher包中提供了相关类来启动Spark作业作为子线程的简单的Java API。

八、单元测试

Spark 对单元测试非常友好,可以使用任何流行的单元测试框架。在你的测试中简单地创建一个 SparkContext,并将 master URL设置成local,运行你的各种操作,然后调用 SparkContext.stop()结束测试。确保在 finally块或测试框架的 tearDown方法中调用 context的 stop方法,因为 Spark不支持在一个程序中同时运行两个contexts。

九、下一步

你可以在 Spark的网站上看到 spark程序的样例。另外,Spark在 examples目录 (Scala, Java, Python,R)中也包含了一些样例。你可以通过将类名传递给 spark的 bin/run-example脚本来运行 Java和 Scala的样例,例如:

/bin/run-example SparkPi

为了帮助优化你的程序,在配置(configuration)和调优(tuning)的指南上提供了最佳实践信息。它们在确保将你的数据用一个有效的格式存储在内存上,是非常重要的。对于部署的帮助信息,可以查看集群模式概述(cluster mode overview),描述了分布式操作以及支持集群管理器所涉及的组件。

最后,完整的 API文档可以查看 Scala, Java,Python和R。