Apache Spark 3.0 RDD编程指南

 RDD 是Apache Spark编程非常重要的一个特性。Spark使用Scala语言编写并支持Java和Python。

 

目录

总览

与Spark链接

Scala语言

Java语言

Python语言

 初始化Spark

Scala语言

Java语言

Python语言

使用Shell

Scala语言

Python语言

弹性分布式数据集(RDD)

并行集合

Scala语言

Java语言

Python语言

外部数据集

Scala语言

Java语言

Python语言

RDD操作

基本

Scala语言

Java语言

Python语言

将函数传递给Spark

Scala语言

Java语言

Python语言

了解闭包 

Scala语言

Java语言

Python语言

本地与集群模式

RDD的打印元素

使用键值对

Scala语言

Java语言

Python语言

转变

动作

随机操作

背景

绩效影响

RDD持久性

选择哪个存储级别?

删除资料

共享变量

广播变量

Scala语言

Java语言

Python语言

蓄能器

Scala语言

Java语言

Python语言

部署到集群

从Java / Scala启动Spark作业

单元测试

从这可去


总览

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

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

本指南以Spark的每种受支持语言显示了所有这些功能。如果启动Spark的交互式shell,无论bin/spark-shell是Scala Shell还是bin/pyspark Python Shell, 最简单的方法就是跟随它。

与Spark链接

Scala语言

默认情况下,Spark 3.0.0已构建并分发为与Scala 2.12一起使用。(可以将Spark构建为与其他版本的Scala一起使用。)要在Scala中编写应用程序,您将需要使用兼容的Scala版本(例如2.12.X)。

要编写Spark应用程序,您需要在Spark上添加Maven依赖项。可通过Maven Central在以下位置获得Spark:

groupId = org.apache.spark
artifactId = spark-core_2.12
version = 3.0.0

另外,如果您想访问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._启用必要的隐式转换。)

Java语言

Spark 3.0.0支持 lambda表达式 以简洁地编写函数,否则,您可以使用org.apache.spark.api.java.function包中的类 。

请注意,在Spark 2.2.0中已删除了对Java 7的支持。

要使用Java编写Spark应用程序,您需要添加对Spark的依赖。可通过Maven Central在以下位置获得Spark:

groupId = org.apache.spark
artifactId = spark-core_2.12
version = 3.0.0

另外,如果您想访问HDFS群集,则需要hadoop-client为您的HDFS版本添加依赖项 。

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

最后,您需要将一些Spark类导入程序。添加以下行:

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

Python语言

Spark 3.0.0适用于Python 2.7+或Python 3.4+。它可以使用标准的CPython解释器,因此可以使用像NumPy这样的C库。它还适用于PyPy 2.3+。

请注意,自Spark 3.0.0起不推荐使用Python 2。

Python中的Spark应用程序既可以在运行时使用bin/spark-submit包含Spark 的脚本运行,也可以通过在setup.py中包含以下脚本来运行:

    install_requires=[
        'pyspark=={site.SPARK_VERSION}'
    ]

要在Python中运行Spark应用程序而无需pip安装PySpark,请使用bin/spark-submitSpark目录中的脚本。该脚本将加载Spark的Java / Scala库,并允许您将应用程序提交到集群。您还可以bin/pyspark用来启动交互式Python Shell。

如果您想访问HDFS数据,则需要使用PySpark的构建链接到您的HDFS版本。 对于常用的HDFS版本,Spark主页上也提供了预构建的软件包

最后,您需要将一些Spark类导入程序。添加以下行:

from pyspark import SparkContext, SparkConf

PySpark在驱动程序和工作程序中都需要相同的次要版本的Python。它在PATH中使用默认的python版本,您可以通过指定要使用的Python版本PYSPARK_PYTHON,例如:

$ PYSPARK_PYTHON=python3.4 bin/pyspark
$ PYSPARK_PYTHON=/opt/pypy-2.5/bin/pypy bin/spark-submit examples/src/main/python/pi.py

 初始化Spark

Scala语言

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

每个JVM仅应激活一个SparkContext。stop()在创建新的SparkContext之前,您必须先激活它。

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

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

Java语言

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

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

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

Python语言

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

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

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

使用Shell

Scala语言

在Spark Shell中,已经在名为的变量中为您创建了一个特殊的可识别解释器的SparkContext sc。制作自己的SparkContext无效。您可以使用--master参数设置上下文连接到哪个主机,还可以通过将逗号分隔的列表传递给参数来将JAR添加到类路径--jars。您还可以通过在--packages参数中提供逗号分隔的Maven坐标列表,从而将依赖项(例如Spark Packages)添加到Shell会话中。可能存在依赖关系的任何其他存储库(例如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脚本

Python语言

在PySpark shell中,已经在名为的变量中为您创建了一个特殊的可识别解释器的SparkContext sc。制作自己的SparkContext无效。您可以使用--master参数设置上下文连接的主机,也可以通过将逗号分隔的列表传递到来将Python .zip,.egg或.py文件添加到运行时路径--py-files。您还可以通过在--packages参数中提供逗号分隔的Maven坐标列表,从而将依赖项(例如Spark Packages)添加到Shell会话中。可能存在依赖关系的任何其他存储库(例如Sonatype)都可以传递给--repositories参数。Spark软件包具有的所有Python依赖项(在该软件包的requirements.txt中列出)都必须pip在必要时使用手动安装。例如运行bin/pyspark 在四个核心上使用:

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

或者,也可以添加code.py到搜索路径中(以便以后import code使用),请使用:

$ ./bin/pyspark --master local[4] --py-files code.py

有关选项的完整列表,请运行pyspark --help。在幕后, pyspark调用更通用的spark-submit脚本

也可以在增强的Python解释器IPython中启动PySpark shell 。PySpark可与IPython 1.0.0及更高版本一起使用。要使用IPython,请在运行时将PYSPARK_DRIVER_PYTHON变量设置为:ipythonbin/pyspark

$ PYSPARK_DRIVER_PYTHON=ipython ./bin/pyspark

要使用Jupyter笔记本(以前称为IPython笔记本),

$ PYSPARK_DRIVER_PYTHON=jupyter PYSPARK_DRIVER_PYTHON_OPTS=notebook ./bin/pyspark

您可以通过设置自定义ipythonjupyter命令PYSPARK_DRIVER_PYTHON_OPTS

启动Jupyter Notebook服务器后,您可以从“文件”选项卡创建一个新的“ Python 2”笔记本。在笔记本内部,您可以在%pylab inline从Jupyter笔记本开始尝试Spark之前输入命令作为笔记本的一部分。

弹性分布式数据集(RDD)

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

并行集合

Scala语言

通过在驱动程序(Scala )中的现有集合上调用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))来手动设置它。注意:代码中的某些位置使用术语片(分区的同义词)来保持向后兼容性。

Java语言

通过在驱动程序中现有的上调用JavaSparkContextparallelize方法来创建并行集合Collection。复制集合的元素以形成可以并行操作的分布式数据集。例如,以下是创建包含数字1到5的并行化集合的方法:

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

创建后,分布式数据集(distData)可以并行操作。例如,我们可能会调用distData.reduce((a, b) -> a + b)以添加列表中的元素。我们稍后将描述对分布式数据集的操作。

并行集合的一个重要参数是将数据集切入的分区数。Spark将为集群的每个分区运行一个任务。通常,群集中的每个CPU都需要2-4个分区。通常,Spark会尝试根据您的集群自动设置分区数。但是,您也可以通过将其作为第二个参数传递给parallelize(例如sc.parallelize(data, 10))来手动设置它。注意:代码中的某些位置使用术语片(分区的同义词)来保持向后兼容性。

Python语言

通过在驱动程序中现有的可迭代对象或集合上调用SparkContextparallelize方法来创建并行集合。复制集合的元素以形成可以并行操作的分布式数据集。例如,以下是创建包含数字1到5的并行化集合的方法:

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

创建后,分布式数据集(distData)可以并行操作。例如,我们可以调用distData.reduce(lambda a, b: a + b)添加列表中的元素。我们稍后将描述对分布式数据集的操作。

并行集合的一个重要参数是将数据集切入的分区数。Spark将为集群的每个分区运行一个任务。通常,群集中的每个CPU都需要2-4个分区。通常,Spark会尝试根据您的集群自动设置分区数。但是,您也可以通过将其作为第二个参数传递给parallelize(例如sc.parallelize(data, 10))来手动设置它。注意:代码中的某些位置使用术语片(分区的同义词)来保持向后兼容性。

外部数据集

Scala语言

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

可以使用SparkContexttextFile方法创建文本文件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就可以通过数据集操作对其进行操作。例如,我们可以使用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允许您为一些常见的可写对象指定本机类型。例如,sequenceFile[Int, String]将自动读取IntWritables和Texts。

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

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

Java语言

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

可以使用SparkContexttextFile方法创建文本文件RDD 。此方法需要一个URI的文件(本地路径的机器上,或一个hdfs://s3a://等URI),并读取其作为行的集合。这是一个示例调用:

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

一旦创建,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的Java API还支持其他几种数据格式:

  • JavaSparkContext.wholeTextFiles使您可以读取包含多个小文本文件的目录,并将每个小文本文件作为(文件名,内容)对返回。与相比textFile,会在每个文件的每一行返回一条记录。

  • 对于SequenceFiles,请使用SparkContext的sequenceFile[K, V]方法,其中KV是文件中键和值的类型。这些应该是Hadoop的Writable接口的子类,例如IntWritableText

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

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

Python语言

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

可以使用SparkContexttextFile方法创建文本文件RDD 。此方法需要一个URI的文件(本地路径的机器上,或一个hdfs://s3a://等URI),并读取其作为行的集合。这是一个示例调用:

>>> distFile = sc.textFile("data.txt")

一旦创建,distFile就可以通过数据集操作对其进行操作。例如,我们可以使用mapreduce操作将所有行的大小相加,如下所示:distFile.map(lambda s: len(s)).reduce(lambda a, b: a + b)

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

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

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

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

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

  • SparkContext.wholeTextFiles使您可以读取包含多个小文本文件的目录,并将每个小文本文件作为(文件名,内容)对返回。与相比textFile,会在每个文件的每一行返回一条记录。

  • RDD.saveAsPickleFileSparkContext.pickleFile支持将RDD保存为由腌制的Python对象组成的简单格式。批处理用于咸菜序列化,默认批处理大小为10。

  • SequenceFile和Hadoop输入/输出格式

请注意,此功能当前已标记Experimental,仅供高级用户使用。将来可能会替换为基于Spark SQL的读/写支持,在这种情况下,Spark SQL是首选方法。

可写支持

PySpark SequenceFile支持在Java内加载键-值对的RDD,将Writables转换为基本Java类型,并使用Pyrolite腌制所得的Java对象。将键/值对的RDD保存到SequenceFile时,PySpark会执行相反的操作。它将Python对象分解为Java对象,然后将它们转换为Writables。以下可写对象将自动转换:

可写类型Python类型
文本unicode str
可写整型
浮动可写浮动
双写浮动
布尔可写布尔
字节可写字节数组
空可写没有
MapWritable字典

数组不是开箱即用的。用户ArrayWritable在读取或写入时需要指定自定义子类型。编写时,用户还需要指定将数组转换为自定义ArrayWritable子类型的自定义转换器。阅读时,默认转换器会将自定义ArrayWritable子类型转换为Java Object[],然后将其腌制为Python元组。要获取array.array用于基本类型数组的Python ,用户需要指定自定义转换器。

保存和加载SequenceFile

与文本文件类似,可以通过指定路径来保存和加载SequenceFiles。可以指定键和值类,但是对于标准可写对象则不需要。

>>> 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输入/输出格式

对于“新”和“旧” Hadoop MapReduce API,PySpark还可以读取任何Hadoop InputFormat或编写任何Hadoop OutputFormat。如果需要,可以将Hadoop配置作为Python字典传递。这是使用Elasticsearch ESInputFormat的示例:

$ ./bin/pyspark --jars /path/to/elasticsearch-hadoop.jar
>>> conf = {"es.resource" : "index/type"}  # assume Elasticsearch 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 is converted to a Python dict
(u'Elasticsearch ID',
 {u'field1': True,
  u'field2': u'Some Text',
  u'field3': 12345})

请注意,如果InputFormat仅取决于Hadoop配置和/或输入路径,并且可以根据上表轻松地转换键和值类,则这种方法在这种情况下应该能很好地工作。

如果您具有自定义的序列化二进制数据(例如从Cassandra / HBase加载数据),则首先需要将Scala / Java端上的数据转换为可由Pyrolite的picker处理的数据。一个转换器特性提供了这一点。只需扩展此特征并在该convert 方法中实现您的转换代码即可。请记住,确保将此类以及访问所需的任何依赖项InputFormat打包到Spark作业jar中,并包含在PySpark类路径中。

有关 使用Cassandra / HBase 和自定义转换器的示例,请参见Python示例Converter示例InputFormatOutputFormat

RDD操作

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

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

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

基本

Scala语言

为了说明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在第一次计算后将其保存在内存中。

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仅是文件的指针。第二行定义lineLengthsmap转换的结果。再次,lineLengths 是不是马上计算,由于懒惰。最后,我们运行reduce,这是一个动作。此时,Spark将计算分解为任务,以在不同的机器上运行,每台机器既运行其映射的一部分,又运行本地还原,仅将其答案返回给驱动程序。

如果我们以后还要使用lineLengths,可以添加:

lineLengths.persist(StorageLevel.MEMORY_ONLY());

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

Python语言

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

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

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

如果我们以后还要使用lineLengths,可以添加:

lineLengths.persist()

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

将函数传递给Spark

Scala语言

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)
}

Java语言

Spark的API在很大程度上依赖于在驱动程序中传递函数以在群集上运行。在Java中,功能由实现org.apache.spark.api.java.function包中的接口的类表示 。有两种创建此类功能的方法:

  • 在您自己的类中(作为匿名内部类或命名类)实现Function接口,并将其实例传递给Spark。
  • 使用lambda表达式 来简洁地定义一个实现。

尽管本指南中的大部分内容都使用lambda语法来简化,但以长格式使用所有相同的API还是很容易的。例如,我们可以将上面的代码编写如下:

JavaRDD<String> lines = sc.textFile("data.txt");
JavaRDD<Integer> lineLengths = lines.map(new Function<String, Integer>() {
  public Integer call(String s) { return s.length(); }
});
int totalLength = lineLengths.reduce(new Function2<Integer, Integer, Integer>() {
  public Integer call(Integer a, Integer b) { return a + b; }
});

或者,如果内联编写函数很麻烦:

class GetLength implements Function<String, Integer> {
  public Integer call(String s) { return s.length(); }
}
class Sum implements Function2<Integer, Integer, Integer> {
  public Integer call(Integer a, Integer b) { return a + b; }
}

JavaRDD<String> lines = sc.textFile("data.txt");
JavaRDD<Integer> lineLengths = lines.map(new GetLength());
int totalLength = lineLengths.reduce(new Sum());

请注意,Java中的匿名内部类也可以访问封闭范围内的变量,只要它们被标记即可final。与其他语言一样,Spark会将这些变量的副本发送到每个工作程序节点。

Python语言

Spark的API在很大程度上依赖于在驱动程序中传递函数以在群集上运行。建议使用三种方法来执行此操作:

  • Lambda表达式,用于可以作为表达式编写的简单函数。(Lambda不支持多语句函数或不返回值的语句。)
  • def函数内部的Local 调用Spark,以获取更长的代码。
  • 模块中的顶级功能。

例如,要传递比使用所支持的函数更长的函数lambda,请考虑以下代码:

"""MyScript.py"""
if __name__ == "__main__":
    def myFunc(s):
        words = s.split(" ")
        return len(words)

    sc = SparkContext(...)
    sc.textFile("file.txt").map(myFunc)

请注意,虽然也可以在类实例中传递对方法的引用(与单例对象相对),但这需要将包含该类的对象与方法一起发送。例如,考虑:

class MyClass(object):
    def func(self, s):
        return s
    def doStuff(self, rdd):
        return rdd.map(self.func)

在这里,如果我们创建了一个new MyClass和调用doStuff就可以了,map里面有引用的 func方法是的MyClass实例,所以整个对象需要被发送到群集。

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

class MyClass(object):
    def __init__(self):
        self.field = "Hello"
    def doStuff(self, rdd):
        return rdd.map(lambda s: self.field + s)

为避免此问题,最简单的方法是将其复制field到局部变量中,而不是从外部访问它:

def doStuff(self, rdd):
    field = self.field
    return rdd.map(lambda s: field + s)

了解闭包 

关于Spark的难点之一是在跨集群执行代码时了解变量和方法的范围和生命周期。修改超出其范围的变量的RDD操作可能经常引起混乱。在下面的示例中,我们将查看foreach()用于增加计数器的代码,但是其他操作也会发生类似的问题。

考虑下面的朴素的RDD元素总和,其行为可能会有所不同,具体取决于执行是否在同一JVM中进行。一个常见的示例是在local模式(--master = local[n])中运行Spark 而不是将Spark应用程序部署到集群(例如,通过将spark-submit提交给YARN)时

Scala语言

var counter = 0
var rdd = sc.parallelize(data)

// Wrong: Don't do this!!
rdd.foreach(x => counter += x)

println("Counter value: " + counter)

Java语言

int counter = 0;
JavaRDD<Integer> rdd = sc.parallelize(data);

// Wrong: Don't do this!!
rdd.foreach(x -> counter += x);

println("Counter value: " + counter);

Python语言

counter = 0
rdd = sc.parallelize(data)

# Wrong: Don't do this!!
def increment_counter(x):
    global counter
    counter += x
rdd.foreach(increment_counter)

print("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)

使用键值对

Scala语言

虽然大多数Spark操作可在包含任何类型的对象的RDD上运行,但一些特殊操作仅可用于键-值对的RDD。最常见的是分布式“混洗”操作,例如通过键对元素进行分组或聚合。

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

Java语言

虽然大多数Spark操作可在包含任何类型的对象的RDD上运行,但一些特殊操作仅可用于键-值对的RDD。最常见的是分布式“混洗”操作,例如通过键对元素进行分组或聚合。

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

键值对的RDDJavaPairRDD类表示 。您可以使用特殊的map操作版本(例如 mapToPair和)从JavaRDD构造JavaPairRDD flatMapToPair。JavaPairRDD将同时具有标准的RDD功能和特殊的键值功能。

例如,以下代码reduceByKey对键值对使用运算来计算文件中每一行文本出现的次数:

JavaRDD<String> lines = sc.textFile("data.txt");
JavaPairRDD<String, Integer> pairs = lines.mapToPair(s -> new Tuple2(s, 1));
JavaPairRDD<String, Integer> counts = pairs.reduceByKey((a, b) -> a + b);

counts.sortByKey()例如,我们还可以使用按字母顺序对,最后 counts.collect()将它们作为对象数组带回到驱动程序中。

注意:在键-值对操作中使用自定义对象作为键时,必须确保自定义equals()方法与匹配hashCode()方法一起使用。有关完整的详细信息,请参见Object.hashCode()文档中概述的合同。

Python语言

虽然大多数Spark操作可在包含任何类型的对象的RDD上运行,但一些特殊操作仅可用于键-值对的RDD。最常见的是分布式“混洗”操作,例如通过键对元素进行分组或聚合。

在Python中,这些操作适用于包含内置Python元组(如)的RDD (1, 2)。只需创建此类元组,然后调用所需的操作即可。

例如,以下代码reduceByKey对键值对使用运算来计算文件中每一行文本出现的次数:

lines = sc.textFile("data.txt")
pairs = lines.map(lambda s: (s, 1))
counts = pairs.reduceByKey(lambda a, b: a + b)

counts.sortByKey()例如,我们还可以使用按字母顺序对,最后 counts.collect()将它们作为对象列表带回到驱动程序。

转变

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

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

动作

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

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

行动含义
减少func使用函数func(使用两个参数并返回一个参数)聚合数据集的元素。该函数应该是可交换的和关联的,以便可以并行正确地计算它。
收集()在驱动程序中将数据集的所有元素作为数组返回。这通常在返回足够小的数据子集的过滤器或其他操作之后很有用。
()返回数据集中的元素数。
第一()返回数据集的第一个元素(类似于take(1))。
n返回具有数据集的前n个元素的数组。
takeSamplewithReplacementnum,[ 种子 ])返回带有数据集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序列化以简单的格式编写数据集的元素,然后可以使用进行加载 SparkContext.objectFile()
countByKey()仅在类型(K,V)的RDD上可用。返回(K,Int)对的哈希图以及每个键的计数。
foreachfunc在数据集的每个元素上运行函数func。通常这样做是出于副作用,例如更新累加器或与外部存储系统进行交互。
注意:在之外修改除累加器以外的变量foreach()可能会导致不确定的行为。有关更多详细信息,请参见了解闭包

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

随机操作

Spark中的某些操作会触发一个称为随机播放的事件。改组是Spark的一种用于重新分配数据的机制,以便在分区之间对数据进行不同的分组。这通常涉及跨执行程序和机器复制数据,从而使改组变得复杂而昂贵。

背景

要了解随机播放期间发生的情况,我们可以考虑reduceByKey操作示例 。该reduceByKey操作将生成一个新的RDD,其中将单个键的所有值组合为一个元组-键以及针对与该键关联的所有值执行reduce函数的结果。挑战在于,并非单个键的所有值都不一定位于同一分区,甚至同一台机器上,但必须将它们共置一处以计算结果。

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

尽管新改组后的数据的每个分区中的元素集都是确定性的,分区本身的顺序也是如此,但这些元素的顺序不是确定性的。如果人们希望在改组后能有规律地排序数据,则可以使用:

  • mapPartitions 使用例如对每个分区进行排序 .sorted
  • repartitionAndSortWithinPartitions 在对分区进行有效排序的同时进行重新分区
  • sortBy 制作全球订购的RDD

这可能会导致一个洗牌的操作包括重新分区一样操作 repartitioncoalesceByKey”操作,比如(除计数)groupByKeyreduceByKey,并 加入操作,如cogroupjoin

绩效影响

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

在内部,单个地图任务的结果会保留在内存中,直到无法容纳为止。然后,根据目标分区对它们进行排序并写入单个文件。在简化方面,任务读取相关的已排序块。

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

随机播放还会在磁盘上生成大量中间文件。从Spark 1.3开始,将保留这些文件,直到不再使用相应的RDD并进行垃圾回收为止。这样做是为了在重新计算沿袭时无需重新创建随机文件。如果应用程序保留了对这些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作为反序列化的Java对象存储在JVM中。如果RDD不能容纳在内存中,则某些分区将不会被缓存,并且每次需要时都会即时重新计算。这是默认级别。
MEMORY_AND_DISK将RDD作为反序列化的Java对象存储在JVM中。如果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也会自动将一些中间数据持久保存在随机操作中(例如)persist。这样做是为了避免在混洗期间节点发生故障时重新计算整个输入。我们仍然建议用户persist如果打算重复使用它,请调用生成的RDD。

选择哪个存储级别?

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

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

  • 如果不是,请尝试使用MEMORY_ONLY_SER选择一个快速的序列化库,以使对象的空间效率更高,但访问速度仍然相当快。(Java和Scala)

  • 除非用于计算数据集的函数非常昂贵,否则它们会泄漏到磁盘上,否则它们会过滤大量数据。否则,重新计算分区可能与从磁盘读取分区一样快。

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

删除资料

Spark自动监视每个节点上的缓存使用情况,并以最近最少使用(LRU)的方式丢弃旧的数据分区。如果您想手动删除一个RDD而不是等待它脱离缓存,请使用该RDD.unpersist()方法。请注意,此方法默认情况下不会阻止。要阻塞直到释放资源,请blocking=true在调用此方法时指定。

 

共享变量

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

广播变量

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

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

v通过调用从变量创建广播变量SparkContext.broadcast(v)。broadcast变量是的包装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广播对象后不应修改该对象 ,以确保所有节点都具有相同的广播变量值(例如,如果变量稍后被传送到新节点)。

要释放广播变量复制到执行程序上的资源,请调用.unpersist()。如果此后再次使用该广播,它将重新广播。要永久释放广播变量使用的所有资源,请致电.destroy()。此后不能使用广播变量。请注意,这些方法默认情况下不会阻止。要阻塞直到释放资源,请blocking=true在调用它们时指定。

Java语言

Broadcast<int[]> broadcastVar = sc.broadcast(new int[] {1, 2, 3});

broadcastVar.value();
// returns [1, 2, 3]

创建广播变量之后,应使用它代替v集群中运行的任何函数中的值,这样才v不会多次将其传送给节点。另外,在v广播对象后不应修改该对象 ,以确保所有节点都具有相同的广播变量值(例如,如果变量稍后被传送到新节点)。

要释放广播变量复制到执行程序上的资源,请调用.unpersist()。如果此后再次使用该广播,它将重新广播。要永久释放广播变量使用的所有资源,请致电.destroy()。此后不能使用广播变量。请注意,这些方法默认情况下不会阻止。要阻塞直到释放资源,请blocking=true在调用它们时指定。

Python语言

>>> broadcastVar = sc.broadcast([1, 2, 3])
<pyspark.broadcast.Broadcast object at 0x102789f10>

>>> broadcastVar.value
[1, 2, 3]

创建广播变量之后,应使用它代替v集群中运行的任何函数中的值,这样才v不会多次将其传送给节点。另外,在v广播对象后不应修改该对象 ,以确保所有节点都具有相同的广播变量值(例如,如果变量稍后被传送到新节点)。

要释放广播变量复制到执行程序上的资源,请调用.unpersist()。如果此后再次使用该广播,它将重新广播。要永久释放广播变量使用的所有资源,请致电.destroy()。此后不能使用广播变量。请注意,这些方法默认情况下不会阻止。要阻塞直到释放资源,请blocking=true在调用它们时指定。

蓄能器

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

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

Spark UI中的累加器

UI中的跟踪累加器对于了解运行阶段的进度很有用(注意:Python尚不支持此功能)。

Scala语言

可以通过分别调用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.

Java语言

可以通过分别调用SparkContext.longAccumulator()SparkContext.doubleAccumulator() 累加Long或Double类型的值来创建数字累加器。然后,可以使用add方法将在集群上运行的任务添加到该集群中。但是,他们无法读取其值。只有驱动程序可以使用其value方法读取累加器的值。

下面的代码显示了一个累加器,用于累加一个数组的元素:

LongAccumulator accum = jsc.sc().longAccumulator();

sc.parallelize(Arrays.asList(1, 2, 3, 4)).foreach(x -> accum.add(x));
// ...
// 10/09/29 18:41:08 INFO SparkContext: Tasks finished in 0.317106 s

accum.value();
// returns 10

尽管此代码使用了对Long类型的累加器的内置支持,但是程序员也可以通过对AccumulatorV2进行子类化来创建自己的类型。AccumulatorV2抽象类具有几种必须被覆盖的方法:reset将累加器重置为零,add将另一个值添加到累加器中, merge将另一个相同类型的累加器合并到该方法中。API文档中包含其他必须重写的方法。例如,假设我们有一个MyVector代表数学向量的类,我们可以这样写:

class VectorAccumulatorV2 implements AccumulatorV2<MyVector, MyVector> {

  private MyVector myVector = MyVector.createZeroVector();

  public void reset() {
    myVector.reset();
  }

  public void add(MyVector v) {
    myVector.add(v);
  }
  ...
}

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

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

警告:Spark任务完成时,Spark将尝试将此任务中累积的更新合并到累加器。如果失败,Spark将忽略该失败,并仍将任务标记为成功并继续运行其他任务。因此,越野车累加器不会影响Spark作业,但是即使Spark作业成功,它也可能无法正确更新。

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

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

LongAccumulator accum = jsc.sc().longAccumulator();
data.map(x -> { accum.add(x); return f(x); });
// Here, accum is still 0 because no actions have caused the `map` to be computed.

Python语言

v通过调用从初始值创建累加器SparkContext.accumulator(v)。然后,可以使用add方法或+=运算符将在集群上运行的任务添加到集群中。但是,他们无法读取其值。只有驱动程序可以使用其value方法读取累加器的值。

下面的代码显示了一个累加器,用于累加一个数组的元素:

>>> accum = sc.accumulator(0)
>>> accum
Accumulator<id=0, value=0>

>>> sc.parallelize([1, 2, 3, 4]).foreach(lambda x: accum.add(x))
...
10/09/29 18:41:08 INFO SparkContext: Tasks finished in 0.317106 s

>>> accum.value
10

尽管此代码使用了对Int类型的累加器的内置支持,但程序员也可以通过将AccumulatorParam子类化来创建自己的类型。AccumulatorParam接口有两种方法:zero为您的数据类型提供“零值”,以及addInPlace将两个值相加。例如,假设我们有一个Vector代表数学向量的类,我们可以这样写:

class VectorAccumulatorParam(AccumulatorParam):
    def zero(self, initialValue):
        return Vector.zeros(initialValue.size)

    def addInPlace(self, v1, v2):
        v1 += v2
        return v1

# Then, create an Accumulator of this type:
vecAccum = sc.accumulator(Vector(...), VectorAccumulatorParam())

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

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

accum = sc.accumulator(0)
def g(x):
    accum.add(x)
    return f(x)
data.map(g)
# Here, accum is still 0 because no actions have caused the `map` to be computed.

部署到集群

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

从Java / Scala启动Spark作业

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

单元测试

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

从这可去

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

./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

为了帮助您优化程序,配置和 调优指南提供了有关最佳做法的信息。它们对于确保数据以有效格式存储在内存中尤其重要。为了获得部署方面的帮助,群集模式概述介绍了分布式操作和支持的群集管理器中涉及的组件。

最后,ScalaJavaPythonR中提供了完整的API文档 。

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: RDD编程Spark SQL是两种不同的数据处理方式。 RDD编程Spark最初的数据处理方式,它是一种基于分布式内存的数据处理模型,可以通过编代码来实现数据的处理和计算。RDD编程需要手动编代码来实现数据的转换和操作,比较灵活,但是需要较高的编程技能和时间成本。 Spark SQL是一种基于SQL语言的数据处理方式,它可以通过SQL语句来实现数据的查询和计算。Spark SQL可以将数据转换为DataFrame或Dataset的形式,提供了更加简单和易用的数据处理方式,适合于数据分析和数据挖掘等应用场景。 总的来说,RDD编程适合于需要灵活处理数据的场景,而Spark SQL适合于需要快速查询和分析数据的场景。在实际应用中,可以根据具体的需求选择不同的数据处理方式。 ### 回答2: RDD编程Spark SQL都是Spark的核心组件。RDDSpark中最基础的抽象,可以看作是一个不可改变的分布式对象集合。而Spark SQL则是Spark提供的基于SQL的抽象,可以通过编SQL查询来操作数据集。这两个组件都有自己的优缺点,可以根据具体需求选择使用。 首先,RDD编程更接近于原始的Spark,它提供了丰富的算子集合来进行操作,例如map、filter、reduce、join等等。它的优点在于灵活性较高,可以处理复杂的计算逻辑,并可以针对具体的问题进行精细调整,以获得更好的性能。同时,RDD也支持Java、Scala以及Python等多种编程语言,方便用户选择。 相比之下,Spark SQL的优点在于其便捷性和易用性。它提供了与SQL语言相似的编程方式,使得用户无需具备大量的编程技能即可快速完成复杂的数据处理任务。此外,Spark SQL还支持DataFrame抽象,可以将数据集看做是一张表格,使得数据的处理更加直观和简单。同时,Spark SQL还可以直接连接一些外部数据源,如Hive、HDFS、JDBC等等。 总之,RDD编程Spark SQL都是非常强大的组件,各有优劣。在实际使用中,应根据具体需求选择合适的组件来进行操作。如果需要进行复杂的计算逻辑,那么RDD编程可能更为合适;如果需要进行快速的数据处理,那么Spark SQL则更具有优势。 ### 回答3: RDD编程Spark SQL是Spark中两种不同的编程方式,它们分别适用于不同的数据处理场景。 RDD编程Spark最初推出的一种编程模型,全称为Resilient Distributed Datasets,中文可译为弹性分布式数据集。RDDSpark中最基本的抽象概念,它是一组跨越多个节点进行并行处理的元素集合。RDD具有以下特点:第一,RDD是不可变的,一旦创建就不能被改变;第二,RDD具有弹性,RDD存储的数据可以自动地在集群中进行分区和重复多份备份,从而提高数据处理的容错性和可靠性;第三,RDD支持函数式编程和面向对象编程两种编程方式,可以通过Transformations和Actions等操作对数据进行处理和计算。RDD编程方式适用于数据处理和计算复杂的场景,比如机器学习、图计算、数据挖掘、文本处理等。 Spark SQL是Spark中的一种高级模块,它用于处理结构化数据并提供了类似于SQL的接口。Spark SQL可以处理包括CSV、JSON、XML、Parquet等广泛的结构化数据格式,支持SQL查询、连接、聚合等复杂语句。Spark SQL还支持DataFrame API,DataFrame是一种基于RDD的数据结构,具有类似于关系型数据库中表的列和行的概念,不同的是DataFrame中的每一列都需要定义数据类型。使用Spark SQL可以有效地处理大规模结构化数据,适合于数据分析和数据挖掘等场景。 在实际使用中,RDD编程Spark SQL可以相互配合使用,根据具体的任务需求选择合适的编程方式,进行快速高效地数据处理和计算。同时,Spark还提供了一系列的API、工具和组件,如Spark Streaming、Spark GraphX、Spark MLlib等,可以根据不同的需求选择相应的模块和库来完成数据处理任务。总之,Spark是一个强大而灵活的数据处理工具,可以在不同场景下进行高效的数据处理和分析。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值