Spark编程指南

Spark 编程指南

概览

从上层来看,集群中每个Spark应用都包含一个驱动程序来运行用户的main函数并且执行不同的并行操作。Spark提供的主要抽象概念就是弹性分布式数据集(RDD),其实就是一个数据集合分区后放在了集群的不同节点上,而且它可以被并行处理。RDD可以通过Hadoop的文件系统被创建(或者任何其它Hadoop支持的文件系统),或者是驱动程序中一个存在的Scala集合转换得到。用户可能也希望Spark能够保存一个RDD到内存中,从而在并行操作中可以更加高效的进行复用。最后,RDD可以从节点故障中自动恢复。
Spark中另外一个抽象概念是共享变量,它可以在不同的并行操作中被使用。默认情况下,当Spark并行运行一个函数,在不同的节点上开启一系列的任务时,它会将函数中使用的每一个变量复制到每一个任务节点上。但是有些时候,我们也需要在不同的任务或者任务与驱动程序之间共享一些变量。Spark提供了两种共享变量的方式:broadcast variables(广播变量)和accumulators(累加器),广播变量可以将值缓存在所有节点的内存中,而累加器则是只能被用于加操作的变量,比如计数器或者求和。
这篇指南展示了上面的各种特性,并用Spark支持的每一种语言来表示。如果你启动了Spark的可交互shell-无论是Scala形式的bin/spark-shell或者是Python形式的bin/pyspark,那么跟着本篇指南走下去,这容易到爆了。

连接Spark

Scala

Spark 1.5.2使用了Scala的2.10版本。为了使用Scala编写应用,你应该使用一个兼容的Scala版本(比如 2.10.X)。为了编写Spark应用,你需要使用Maven添加一个Spark依赖。Spark在Maven仓库中已经可用,坐标如下:

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

此外,如果你想访问HDFS集群,你需要根据你的HDFS版本添加一个hadoop-client依赖。一些比较常用的HDFS版本已经列在了third party distributions(http://spark.apache.org/docs/latest/hadoop-third-party-distributions.html)页面。

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 1.5.2使用Java 7或者更高版本。如果你正在使用Java 8,Spark支持lambda表达式来简化方法的编写,否则你需要使用包org.apache.spark.api.java.function下的类。
为了使用Java编写Spark应用,你需要使用Maven添加一个Spark依赖。Spark在Maven仓库中已经可用,坐标如下:

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

此外,如果你想访问HDFS集群,你需要根据你的HDFS版本添加一个hadoop-client依赖。一些比较常用的HDFS版本已经列在了third party distributions(http://spark.apache.org/docs/latest/hadoop-third-party-distributions.html)页面。

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 1.5.2能够在Python 2.6+或者python 3.4+版本上工作。它可以使用标准的CPython的解释器,所以类似于Numpy的C库是可以使用的。它在PyPy 2.3+的版本上也可以正常工作。
为了用Python来运行Spark应用,请使用Spark目录中的bin/spark-submit脚本。这个脚本将会加载Spark中的Java/Scala库并且允许你向集群提交应用。你也可以使用bin/pyspark来启动一个可交互的Python shell。
如果想访问HDFS的数据,你需要使用一个编译好的PySpark,它必须与你的HDFS版本相关联。一些比较常用的HDFS版本已经列在了third party distributions(http://spark.apache.org/docs/latest/hadoop-third-party-distributions.html)页面。在Spark的主页上,一些常见的HDFS版本预编译包(http://spark.apache.org/downloads.html)也都提供给了大家。
最后,你需要在项目中引入一些Spark类。添加下面这行:

from pyspark import SparkContext, SparkConf

PySpark需要driver和worker中的最低Python版本一致。它使用PATH中配置的默认python版本,你可以通过PYSPARK_PYTHON指定你想要的Python版本,例如:

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

Scala

Spark项目第一步必须创建一个SparkContext对象,它将告诉Spark怎么去访问集群。那么为了创建SparkContext,我们必须构建一个SparkObject对象,它封装了当前应用的一些信息。
一个JVM中只有一个SparkContext是活跃的。在创建新的SparkContext时必须调用stop()关闭原来活跃的SparkContext。

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

appName这个参数是显示在SparkUI界面上的应用名字。master是一个Spark,Mesos或者YARN集群URL,或者是特殊的“local”字符串表示以本地模式运行。在实际应用中,当你在集群上运行应用时,你可能并不希望在项目中硬编码master参数,而是使用spark-submit启动应用并传递master到项目中。然而,为了本地测试或者单元测试,你可以传递“local”在同进程内运行Spark。

Java

步骤与Scala基本相同,只是创建的对象不再是SparkContext,而是JavaSparkContext。

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

Python

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

Scala

在Spark的shell中,一个特殊的SparkContext对象已经为你创建好了,它的变量名为sc。所以你创建你自己的SparkContext对象并不会起作用。你可以通过使用–master参数来设置context要连接到的master,并且你可以为–jars参数传递一个逗号分隔的列表来将JAR包添加到classpath中。你也可以通过提供一个逗号分隔的maven坐标列表给–packages参数来向当前的shell会话中添加依赖。任何可能包含依赖包的仓库都可以通过给–repositories参数传递进去。比如,为了启动4个工作线程运行bin/spark-shell,使用如下命令:

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

或者,添加code.jar到classpath下,使用:

$ ./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依然建立好了。它启动命令如下:

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

将python文件添加到查询路径使用如下命令:

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

它也可以在增强的python解释器IPython中启动PySpark shell。PySpark可以在IPython 1.0.0或者更高版本中工作。为了使用IPython,当运行bin/pyspark命令时请设置PYSPARK_DRIVER_PYTHON变量:

$ PYSPARK_DRIVER_PYTHON=ipython ./bin/pyspark

你可以通过设置PYSPARK_DRIVER_PYTHON_OPTS来定制ipython命令。比如,为了启动IPython Notebook支持PyLab plot:

$ PYSPARK_DRIVER_PYTHON=ipython PYSPARK_DRIVER_PYTHON_OPTS='notebook' ./bin/pyspark

当IPython Notebook服务器启动以后,你就可以通过”Files”选项卡创建一个新的”Python 2”。在notebook里,你可以在启动Spark之前输入命令 %pylab inline来将其作为notebook的一部分。

弹性分布式数据集(RDDs)

Spark处理主要就是围绕着RDD的概念进行,RDD是包含很多元素并具有容错性的数据集合,它能够被并行处理。这里有两种创建RDD的方式:并行化一个驱动程序中已经存在的集合,或者从外部文件系统中引用一个数据集,比如HDFS,HBase或者任何提供Hadoop输入格式的数据源。

并行化集合

Scala

并行化集合是通过对你的驱动程序中已经醋在的集合调用SparkContext’S parallelize方法得到的。元素集合会被拷贝从而形成一个可以被并行处理的分布式数据集。例如,下面的例子创建了一个并行化的集合,内容是数字1-5:

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

集合一旦创建,它就可以并行化处理了。例如,我们可能调用distData.reduce((a,b)=>a+b)方法来把数组的元素加起来。一会我们会继续来描述分布式数据集上的操作。
并行化集合一个比较重要的参数是数据集被切分的分区的数目。Spark将会对集群中每一个分区执行一个task。比较典型的一个策略是集群中的每一个CPU弄2-4个分区。正常情况下,Spark会根据集群的配置自动设置分区的数目。然而,你也可以通过parallelize方法的第二个参数手动的设置(e.g. sc.parallelize(data, 10))。
注意:代码中某些地方使用了term slices(词片? 分区的同义词)的概念来保持向后兼容。

Java

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

注意:在本指南中,我们会经常使用lambda表达式来简化Java 方法的书写,但是在旧版本中,你需要实现org.apache.spark.api.java.function包下的接口。我们将在下面描述向Spark传递函数的一些细节。

Python

data = [1,2,3,4,5]
distData = sc.parallelize(data)
外部数据集

Scala

Spark可以从任何支持Hadoop的存储源创建分布式数据集,包括本地文件系统,HDFS,Cassandra,HBase,Amazon S3等。Spark支持文本文件,SequenceFile(HDFS文件类型的一种,里面是可序列化的字符数组)以及任何其它Hadoop InputFormat。
文本文件RDD可以通过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读取文件时一些需要注意的地方:

  • 如何使用本地文件系统的路径,这个文件也必须能够被worker节点以同样的路径访问。你可以采用将文件复制到所有worker节点的方式或者使用一个网络文件共享系统。
  • Spark中所有基于文件输入的方法,包括textFile,都支持对目录,压缩文件以及通配符文件进行调用。比如,你可以这样使用,textFile(“/my/directory”),textFile(“/my/directory/.txt”)以及textFile(“/my/directory/.gz”)。
  • textFile方法也接收额外的第二个参数,用以控制文件被分区的数目。默认情况下,Spark为文件的每一个块创建一个分区(HDFS中块大小默认为64MB),但是你也可以通过传递一个较大的数来创建更多数目的分区。需要注意的是你不能让分区的数量小于块的数量。

除了textFile,Spark中Scala语言API还支持其它的数据格式:

  • SparkContext.wholeTextFiles 允许你读取一个包含了很多小文件的目录,并且它会为每一个文件返回一个<文件名,文件内容>的K-V对。这个方法与textFile正好是个对比,textFile返回的结果中,每一个文件的每一行都是一个记录。
  • 对于SequenceFile,可以使用SparkContext的sequenceFile[K, V]方法,其中K与V代表着文件中key和value的类型。它们应该是Hadoop中Writable借口的子类,比如IntWritable和Text。此外,Spark允许用户对一些通用的Writables指定native(其实就是语言本身的)类型;比如,sequenceFile[Int, String]将会自动读取IntWritables和Texts
  • 对于其它的Hadoop InputFormats,你可以使用SparkContext.hadoopRDD方法,它需要传入一个JobCOnf和input format的类,key类型的类和value类型的类。把这些设置好,你就利用你的输入源得到了一个Hadoop作业。你也可以使用SparContext.newAPIHadoopRDD来处理那些基于”新版”MapReduce API(org.apache.hadoop.mapreduce)的InputFormats。
  • RDD.saveAsObjectFile and SparkContext.objectFile support saving an RDD in a simple format consisting of serialized Java objects. While this is not as efficient as specialized formats like Avro, it offers an easy way to save any RDD.
  • RDD.saveAsObjectFile和SparkContext.objectFile方法支持将RDD存储为一个较简单的格式,它包括序列化的Java对象。然而这种方法与与专用的序列化格式相比并不高效,比如Avro,但是它提供了一个较简便的存储任何RDD的方式。
RDD操作

RDD支持两种类型的操作:转换(它可以从已存在的数据集再生成一个新的数据集)和动作(对数据集进行计算后返回给驱动程序值)。例如,map是一个转换,它可以对数据集中的每一个元素执行一个函数并将结果作为一个一个RDD。另一方面,reduce是一个动作,它通过一些函数对RDD中的所有元素进行聚合并将最终结果返回给driver program(尽管还有一个并行处理的函数reduceByKey,它可以返回一个分布式数据集)。
Spark中所有的转换都是懒执行的,就是说调用转换并不会立刻计算结果。反之,Spark会记住所有应用在基数据集上的转换。转换只有在一个动作被调用需要给driver program返回结果时才会被计算。这种设计可以让Spark运行更加高效-例如,我们我们知道一个从map转换生成的数据集通常都要在reduce动作中被使用并且只会把reduce生成的值返回给driver,而不是那个map生成的数据集。
默认情况下,每一个转换生成的RDD在应用动作时都会被计算一次。然而,你可能需要使用persist(或者cache)方法将一个RDD持久化到内存中,这样在下次你再使用这个RDD时Spark能够快速进行访问。Spark也支持将序列化RDD到硬盘中,或者在不同的节点之间复制。

基础

为了说明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操作的一部分并且在本机执行了聚合操作,并将自己的结果发送给driver program。
如果我们之后还想使用lineLengths,我们可以在reduce之前添加下面的语句

lineLengths.persist()

此时lineLength会在第一次计算之后保存在内存中。

向Spark传递函数

Spark的API重度依赖于在driver program中传递函数给集群执行。有两种推荐的方式:
Anonymous function syntax, which can be used for short pieces of code.
Static methods in a global singleton object. For example, you can define object MyFunctions and then pass MyFunctions.func1, as follows:

  • 匿名函数的语法可以使代码简介
  • 单例object中的静态方法。例如,你可以定义一个MyFunctions对象并且传递MyFunctions.func1函数,就像下面这样:
object MyFunctions {
  def func1(s: String): String = { ... }
}

myRdd.map(MyFunctions.func1)

Note that while it is also possible to pass a reference to a method in a class instance (as opposed to a singleton object), this requires sending the object that contains that class along with the method. For example, consider:
需要注意的

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值