spark之11:编程指南(v1.2.1翻译)

来源:互联网 发布:中国失业率数据2016 编辑:程序博客网 时间:2024/04/29 03:05

spark之11:编程指南(v1.2.1翻译)

@(博客文章)[spark|大数据]

转载自:http://blog.csdn.net/sdujava2011/article/details/46878153?utm_source=tuicool

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

Spark编程指南V1.4.0

· 简介

· 接入Spark

· Spark初始化

    ·        使用Shell    ·        在集群上部署代码

· 弹性分布式数据集

    ·        并行集合(Parallelized Collections)    ·        其他数据集    ·        RDD的操作             ·        基础操作             ·        向Spark传递函数             ·        处理键值对             ·        转换             ·        动作    ·        RDD的持久化             ·        存储级别的选择             ·        移除数据

· 共享变量

    ·        广播变量    ·        累加器

· 部署到一个集群上

· 单元测试

· 从1.0之前版本的Spark迁移

· 下一步该怎么做

简介

总的来说,每一个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

Scala

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

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

[html] view plaincopy

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

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

[html] view plaincopy

groupId = org.apache.hadoop
artifactId = hadoop-client
version =

最后,你需要将一些Spark的类和隐式转换导入到你的程序中。通过如下语句:

[java] view plaincopy

import org.apache.spark.SparkContext
import org.apache.spark.SparkContext._
import org.apache.spark.SparkConf

Java

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

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

[html] view plaincopy

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

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

[html] view plaincopy

groupId = org.apache.hadoop
artifactId = hadoop-client
version =

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

[java] view plaincopy

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

Python

Spark1.2.1需要和Python2.6或者更高的版本(但不是Python3)一起使用。它使用标准的CPython解释器,因此像NumPy这类的C语言库可以用。要用Python的方式运行Spark应用程序,可以使用在Spark目录下的bin/spark-submit脚本。这个脚本会装载Spark的Java和Scala库并允许你将程序提交到集群。你也可以使用bin/pyspark来启动一个交互式Python Shell。

如果你想要访问HDFS数据,你需要根据你的HDFS版本使用一个PySpark的构建。一些通用的HDFS版本标签在第三方发行版页面列出。针对通用的HDFS版本的预先构建的包在Spark主页上也是可获得。

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

from pyspark import SparkContext, SparkConf

初始化Spark

Scala

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

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

[java] view plaincopy

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。

Java

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

[java] view plaincopy

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。

Python

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

[python] view plaincopy

conf =SparkConf().setAppName(appName).setMaster(master)
sc = SparkContext(conf=conf)

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

使用Shell

Scala

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

[plain] view plaincopy

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

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

[plain] view plaincopy

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

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

Python

在PySpark shell中,一个特殊的解释器感知的SparkContext已经为你创建好了,变量名叫做sc。创建自己的SparkContext将不会生效。你可以使用-master参数设置context连接到那个master,并且你可以使用—py-files参数把用逗号分隔的一个Python .zip,.egg或者.py文件列表添加到classpath中。例如,如果在四核CPU上运行bin/pyspark,使用:

[plain] view plaincopy

$ ./bin/pyspark –master local[4]

或者,同时将code.py添加到搜索路径中(为了以后使用import code),使用:

[plain] view plaincopy

$ ./bin/pyspark –master local[4] –py-filescode.py

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

也可以在IPython中启动Pyspark shell,一个增强的Python解释器。PySpark要使用IPython1.0.0及其之后的版本。要使用IPython,当运行bin/pyspark时要设置PYSPARK_DRIVER_PYTHON变量为ipython:

[plain] view plaincopy

$ PYSPARK_DRIVER_PYTHON=ipython ./bin/pyspark

你可以通过设置PYSPARK_DRIVER_PYTHON_OPTS参数来自定义ipython命令。例如,启动有PyLab支持的IPython Notebook支持:

[plain] view plaincopy

$PYSPARK_DRIVER_PYTHON=ipython PYSPARK_DRIVER_PYTHON_OPTS=”notebook–pylab inline” ./bin/pyspark

弹性分布式数据集(RDDs)

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

并行集合

Scala

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

[java] view plaincopy

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的并行集合:

[java] view plaincopy

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))。注意:代码中的一些地方使用属于“分片(分区的近义词)”来保持向后兼容。

Python

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

[python] view plaincopy

data = [1, 2, 3, 4, 5]
distData = sc.parallelize(data)

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

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

外部数据集

Scala

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

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

[java] view plaincopy

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 distFile =sc.textFile(“data.txt”);

一旦创建完成,就可以在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的Java API也支持其他几种数据格式:

  1. JavaSparkContext.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,你可以使用JavaSparkContext.hadoopRDD方法,它可以接受一个任意类型的JobConf和输入格式类,key类和value类。像Hadoop Job设置输入源那样去设置这些参数即可。对基于“新”的MapReduce API(org.apache.hadoop.mapreduce)的InputFormats,你也可以使用JavaSparkContex.newHadoopRDD。

  4. JavaRDD.saveAsObjectFile和JavaSparkContext.objectFile支持由序列化的Java对象组成的简单格式来保存RDD。虽然这不是一种像Avro那样有效的序列化格式,但是她提供了一种可以存储任何RDD的简单方式。

Python

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

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

distFile =sc.textFile(“data.txt”)

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

distFile.map(lambda s: len(s)).reduce(lambda 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的Python API也支持其他几种数据格式:

  1. JavaSparkContext.wholeTextFiles可以让你读取包含多个小text文件的目录,并且每个文件对应返回一个(filename,content)对。而对应的textFile方法,文件的每一行对应返回一条记录(record)。

  2. RDD.saveAsPickleFile和SparkContext.pickleFile支持由pickled Python对象组成的简单格式保存RDD。使用批量的方式处理pickle模块的对象序列化,默认批处理大小为10.

  3. SequenceFile和Hadoop输入/输出格式

注意,此功能当前标识为试验性的,是为高级用户而提供的。在将来的版本中,可能会因为支持基于SparkSQL的读写而被取代,在这种情况下,SparkSQL是首选的方法。

Writable支持

PySpark的SequenceFile支持加载Java中的键值(key-value)对RDD,可以将Writable转换为基本的Java类型,并且通过Pyrolite,在结果Java对象上执行pickles序列化操作。当将一个键值对的RDD保存为SequenceFIle时,PySpark会对其进行反操作。它会unpickles Python的对象为Java对象,然后再将它们转换为Writables。下表中的Writables会被自动地转换:

Writable Type

Python Type

Text

unicode str

IntWritable

int

FloatWritable

float

DoubleWritable

float

BooleanWritable

bool

BytesWritable

bytearray

NullWritable

None

MapWritable

dict

数组不支持开箱(out-of-the-box)处理。当读或写数组时,用户需要指定自定义的ArrayWritable子类。当写数组时,用户也需要指定自定义的转换器(converters),将数组转换为自定义的ArrayWritable子类。当读数组时,默认的转换器会将自定义的ArrayWritable子类转换为Java的Object[],然后被pickled成Python的元组。如果要获取包含基本数据类型的数组,Python的array.array的话,用户需要为该数组指定自定义的转换器。

保存和加载SequenFiles

类似于text files,SequenceFiles可以被保存和加载到指定的路径下。可以指定key和value的类型,但对标准的Writables类型则不需要指定。

[java] view plaincopy

rdd = sc.parallelize(range(1,4)).map(lambda x: (x, “a” * x ))
rdd.saveAsSequenceFile(“path/to/file”)
sorted(sc.sequenceFile(“path/to/file”).collect())
[(1, u’a’), (2, u’aa’), (3, u’aaa’)]

保存和加载其他的Hadoop输入/输出格式

PySpark也可以读任何Hadoop InputFormat或者写任何Hadoop OutputFormat,包括“新”和“旧”两个Hadoop MapReduce APIs。如果需要的话,可以将传递进来的一个Hadoop配置当成一个Python字典。这里有一个使用了Elasticsearch ESInputFormat的样例:

[plain] view plaincopy

SPARK_CLASSPATH=/path/to/elasticsearch-hadoop.jar./bin/pyspark

conf = {“es.resource” :”index/type”} # assumeElasticsearch is running on localhost defaults
rdd =sc.newAPIHadoopRDD(“org.elasticsearch.hadoop.mr.EsInputFormat”,\
“org.apache.hadoop.io.NullWritable”,”org.elasticsearch.hadoop.mr.LinkedMapWritable”, conf=conf)
rdd.first() # the result is a MapWritable that isconverted to a Python dict
(u’Elasticsearch ID’,
{u’field1’: True,
u’field2’: u’Some Text’,
u’field3’: 12345})

注意,如果这个InputFormat只是简单地依赖于Hadoop配置和/或输入路径,以及key和value的类型,它就可以很容易地根据上面的表格进行转换,那么这种方法应该可以很好地处理这些情况。

如果你有一个定制序列化的二进制数据(比如加载自Cassandra/HBase的数据),那么你首先要做的,是在Scala/Java侧将数据转换为可以用Pyrolite的pickler处理的东西。Converter特质提供了这一转换功能。简单地extend该特质,然后在convert方法中实现你自己的转换代码。记住要确保该类,以及访问你的InputFormat所需的依赖,都需要被打包到你的Spark作业的jar包,并且包含在PySpark的类路径中。

在Python样例和Converter样例上给出了带自定义转换器的Cassandra/HBase的InputFormat和OutputFormat的使用样例。

RDD操作

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

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

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

基础操作

Scala

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

[java] view plaincopy

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的基础操作,可以考虑下面的简单程序:

[java] view plaincopy

JavaRDD lines =sc.textFile(“data.txt”);
JavaRDD 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在第一次被计算之后,被保存在内存中。

Python

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

[python] view plaincopy

lines = sc.textFile(“data.txt”)
lineLengths = lines.map(lambda s: len(s))
totalLength = lineLengths.reduce(lambda a, b: a+ b)

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

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

lineLengths.persist()

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

把函数传递到Spark

Scala

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

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

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

[java] view plaincopy

object MyFunctions {
deffunc1(s: String): String = { … }
}

myRdd.map(MyFunctions.func1)

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

[java] view plaincopy

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

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

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

[java] view plaincopy

class MyClass {
valfield = “Hello”
defdoStuff(rdd: RDD[String]): RDD[String] = { rdd.map(x => field + x) }
}

等同于写成rdd.map(x=>this.field+x),引用了整个this。为了避免这种问题,最简单的方式是把field拷贝到本地变量,而不是去外部访问它:

[java] view plaincopy

def doStuff(rdd: RDD[String]): RDD[String] = {
valfield_ = this.field
rdd.map(x => field_ + x)
}

Java

Spark的API,在很大程度上依赖于把驱动程序中的函数传递到集群上运行。在Java中,函数由那些实现了org.apache.spark.api.java.function包中的接口的类表示。有两种创建这样的函数的方式:

●在你自己的类中实现Function接口,可以是匿名内部类,或者命名类,并且传递类的一个实例到Spark。

●在Java8中,使用lambda表达式来简明地定义函数的实现。

为了保持简洁性,本指南中大量使用了lambda语法,这在长格式中很容易使用所有相同的APIs。比如,我们可以把上面的代码写成:

[java] view plaincopy

JavaRDD lines =sc.textFile(“data.txt”);
JavaRDD lineLengths =lines.map(new Function

Wrong: Don’t do this!!

rdd.foreach(lambda x: counter += x)

print(“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)。

键值对的使用

Scala

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

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

比如,下列代码在键值对上使用了reduceByKey操作,来计算在一个文件中每行文本出现的总次数:

[java] view plaincopy

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()文档大纲中列出的规定。

Java

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

在java中,可以使用Scala标准库中的scala.Tuple2类来表示键值对,你可以简单地调用new Tuple2(a,b)来创建一个元组,然后使用tuple._1()和tuple._2()方法来访问元组的字段。

使用JavaPairRDD来表示键值对RDDs。你可以使用指定版本的map操作,从JavaRDDs构建JavaPairRDDs,比如mapToPair和flatMapToPair。JavaPairRDD支持标准的RDD函数,也支持特殊的键值函数。

例如,下面的代码在键值(key-value)对上使用 reduceByKey操作来计算在一个文件中每行文本出现的总次数:

[java] view plaincopy

JavaRDD lines =sc.textFile(“data.txt”);
JavaPairRDD

Then, create an Accumulator of this type:

vecAccum = sc.accumulator(Vector(…),VectorAccumulatorParam())

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

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

Scala

[java] view plaincopy

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.

Java

[java] view plaincopy

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

Python

[python] view plaincopy

accum = sc.accumulator(0)
def g(x):
accum.add(x)
returnf(x)
data.map(g)

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。

Spark1.0之前版本的迁移

Scala

Spark 1.0 冻结了 1.X系列的 Spark核心(Core) API,现在,其中的 API,除了标识为“试验性(experimental)”或“开发者的(developer) API”的,在将来的版本中都会被支持。对 Scala用户而言,唯一的改变在于组操作(grouping operations),比如, groupByKey, cogroup和 join,其返回值已经从 (Key, Seq[Value])对修改为 (Key,Iterable[Value])。

迁移指南也可以从 Spark Streaming, MLlib和 GraphX获取。

Java

Spark 1.0 冻结了 1.X系列的 Spark核心(Core) API,现在,其中的 API,只要不是标识为“试验性(experimental)”或“开发者的(developer) API”的,在将来的版本中都会被支持。其中对 Java API做了一些修改:

•对于 org.apache.spark.api.java.function中的类函数(Function classes),在 1.0版本中变成了接口,这意味着旧的代码中 extends Function应该需要为 implement Function。

•增加了 map转换(transformations)的新变体,如 mapToPair和 mapToDouble,用于创建指定数据类型的 RDDs。

•组操作(grouping operations),如 groupByKey, cogroup 和 join的返回值也被修改了,从原先返回 (Key, List)对改为(Key,Iterable)。

迁移指南也可以从 Spark Streaming, MLlib和 GraphX获取。

Python

Spark 1.0 冻结了 1.X系列的 Spark核心(Core) API,现在,其中的 API,只要不是

标识为“试验性(experimental)”或“开发者的(developer) API”的,在将来的版本中

都会被支持。对 Python用户而言,唯一的修改在于分组操作(grouping operations),比

如groupByKey,cogroup和join,其返回值从 (key, list of values)对修改为 (key,

iterableof values)。

迁移指南也可以从 Spark Streaming, MLlib和 GraphX获取。

下一步

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

[plain] view plaincopy

./bin/run-example SparkPi

对于 Python样例,要使用 spark-submit:

[plain] view plaincopy

./bin/spark-submitexamples/src/main/python/pi.py

对于R样例,使用spark-submit:

[plain] view plaincopy

./bin/spark-submitexamples/src/main/r/dataframe.R

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

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

0 0