1、概述
总的来讲,每一个spark驱动程序应用都由一个驱动程序组成,该驱动程序包含一个由用户编写的main方法,该方法会在集群上执行一些并行计算操作。Spark最重要的一个概念是弹性分布式数据集,简称RDD,RDD是一个数据容器,他将分布式在集群上各个节点上的数据抽象为一个数据集,并且RDD能够进行一系列的并行计算操作。可以将RDD理解为一个分布式的List,该List的数据为分布在各个节点上的数据。RDD通过读取Hadoop文件系统中的一个文件进行创建,也可以有一个RDD经过转换得到。用户也可以将RDD缓存到内存,从而高效的处理RDD,提高计算效率。另外,RDD有良好的容错机制。
Spark另外一个重要概念是共享变量。在并行计算时,可以方便的使用共享变量。在默认情况下,执行Spark任务时会在多个节点上并行执行多个task,Spark将每个每个变量的副本分发给各个task。在一些场景下,需要一个能够在各个task间共享的变量。Spark支持两种类型的共享变量:
广播变量:将一个只读变量缓存到集群的每个节点上。例如,将一份数据的只读缓存分发到每个节点。
累加变量:只允许add操作,用于计数、求和。
2、引入spark
groupId = org.apache.spark
artifactId = spark-core_2.10
version = 1.6.0
groupId = org.apache.hadoop
artifactId = hadoop-client
version = <your-hdfs-version>
import org.apache.spark.SparkContext
import org.apache.spark.SparkConf
3、初始化Spark
使用Scala编写Spark程序首先需要创建一个SparkContext对象(Java则使用JavaSparkContext)。SparkContext对象指定了Spark应用访问集群的方式。创建SparkContext需要先创建一个SparkConf对象,SparkConf对象包含了Spark应用的一些列信息。
//scala val conf = new SparkConf().setAppName(appName).setMaster(Master) new SparkContext(conf) ========================== //Java SparkConf conf = new SparkConf().setAppName(appName).setMaster(master); JavaSparkContext sc = new JavaSparkContext(conf);
appName参数为应用程序在集群的UI上显示的名字。master为Spark、Mesos、Yarn URL或local。使用local值时,表示在本地模式下运行程序。应用程序的执行模型也可以在使用spark-submit命令提交任务时进行指定。
3.1 使用Spark Shell
在spark Shell下,一个特殊的Sparkcontext对象已经帮用户创建好,变量为sc。使用参数--master设置master参数值,使用参数--jars设置依赖包,多个jar包使用逗号分隔。可以使用--packages参数指定maven坐标来添加依赖包,多个坐标使用逗号分隔。可以使用参数--repositories添加外部的repository。示例如下:
本地模式下,使用4个核运行Spark程序:
$ ./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"
4 、弹性分布式数据集(RDDs)
Spark最重要的一个概念就是RDD,RDD是一个有容错机制的元素容器,他可以进行并行计算操作。得到RDD的方式有两个:
通过并行化驱动程序中已有的一个集合来获得
通过外部存储系统(例如共享的文件系统,HDFS、HBase等)的数据集进行创建
4.1 并行集合
在驱动程序中,在一个已经存在的集合上(例如一个Scala的Seq)调用SparkContext的parallelize方法可以创建一个并行集合。集合里的元素将被复制到一个可被并行操作的分布式数据集中。下面为并行化一个保存数据1到5的集合进行示例:
//Scals val data = Array(1,2,3,4,5) val distData = sc.parallelize(data) //Java List<Integer> data = Arrays.asList(1,2,3,4,5); JavaRDD<Integer> distData = sc.parallelize(data);
当分布式数据集创建之后,就可以进行并行操作。例如,可以调用方法distData.reduce((a.b)=>a+b)求数组内元素的和。Spark支持的分布式数据集上的操作将在后面章节中详细描述。
并行集合的一个重要的参数是表示将数据划分为几个分区的分区数。Spark将在集群上每个数据分区上启动一个task。通常情况下,你可以在集群上为每个CPU设置2-4个分区。一般情况下,Spark基于集群自动设置分区数目。也可以手动进行设置,设置该参数需要将参数值作为第二个参数传给parallelize方法,例如:sc.parallelize(data,10).注意:在代码中,部分位置使用术语slices(而不是partition),这么做的原因是为了保持版本的向后兼容性。
4.2 外部数据库
Spark可以通过Hadoop支持的外部数据源创建分布式数据集,Hadoop支持的数据源有本地系统、HDFS、Cassandra、HBase、Amazon S3、Spark支持的文本文件、SequenceFiles、Hadoop InputFormat.
SparkContext的testFile方法可以创建文本文件RDD。使用这个方法需要传递文本文件的URI,URI可以为本机文件路径、hdf://、s3n://等。该方法读取文本文件的每一行到容器中。示例如下:
//Scala val distFile = sc.textFile("data.txt") distFile: RDD[String] = MappedRDD@1d4cee08 //Java JavaRDD<String> distFile = sc.textFile("data.txt");
创建之后,distFile就可以进行数据集的通用操作。例如,使用map和reduce操作计算素有行的长度的总和:distFile.map(s => s.length).reduce((a,b) => a+b).
使用Spark读取文件需要注意以下几点:
程序中如果使用到本地文件路径,在其他worker节点上该文件必须在同一目录,并具有访问权限。在这种情况下,可以将文件复制到所有的work结点。也可以使用网络内的共享文件系统。
Spark所有的基于文件输入的方法(包括textFile),都支持文件夹、压缩文件、通配符。例如:textFile("/my/directory")、textFile(“/my/directory/*.txt”)、textFile(“/my/directory/*.gz”)
textFile方法提供了一个可选的第二参数,用于控制文件的分区数。默认情况下,Spark为文件的每个块创建一个分区(块使用HDFS的默认值64M),通过设置这个第二个参数可以修改这个默认值。需要注意的是,分区数不能小于块数。
除此之外,Spark还支持其他的数据格式:
sparkContext.wholeTextFiles能够读取指定目录下的许多小文本文件,返回(filename,content)对。而textFile只能读取一个文本文件,返回该文本文件的每一行。
对于sequenceFiles可以使用SparkContext的sequenceFile[K,V]方法,其中K是文件中key和value的类型。他们必须为像IntWritable和Text那样,是Hadoop的Writable接口的子类。另外,对于通用的Writable,Spark允许用户指定原生类型。例如,sequenceFile[Int,String]将自动读取IntWritable和Text。
对于其他hadoop InputFormat,可以使用SparkContext.hadoopRDD方法,该方法接收任意类型的JobConf和JocConf和输入格式类、键值类型。可以像设置Hadoop job那样设置输入源。对于InputFormat还可以使用基于新版本MapReduce API的AparkContext.newAPIHadoopRDD。
RDD.saveAsObjectFile和SparkContext.objectFile能够保存包含简单的序列化java对象的RDD。但是这个方法不如AVRO高效。
4.3 RDD操作
RDD支持两种类型的操作:
transformation:从一个RDD转换为一个新的RDD
action:基于一个数据集进行运算,并返回RDD
例如,map是一个transformation操作,map将数据集的每一个元素按指定的函数转换为一个RDD返回。reduce是一个aciton操作,reduce将RDD的所有元素按指定的函数进行聚合并返回结果给驱动程序(还有一个并行的reduceByKey能够返回一个分布式的数据集)
Spark的所有transformation操作都是懒执行,他们并不立马执行,而是先记录对数据集的一系列transformation操作。在执行一个需要执行一个action操作时,会执行该数据数据集上所有的transformation操作,然后返回结果。这种设计让Spark的运算更高效。例如,对一个数据集map操作之后使用reduce只返回结果,而不能返回庞大的map运算的结果集。
默认情况下,每个转换的RDD在执行action操作时都会重新计算。即使两个action操作会使用同一个转换的RDD,该RDD也会重新计算。在这种情况下,可以使用persist方法或cache方法将RDD缓存到内存,这样在下次使用这个RDD时将会提高计算效率。在这里,也支持RDD持久化到磁盘,或在多个节点上复制。
4.3.1 基础
参考下面的程序,了解RDD的基本轮廓
//Scala val lines = sc.textFile(“data.text") val lineLengths = lines.map(s=>s.length) val totalLength = lineLengths.reduce((a,b)=>a + b) //Java 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仅仅是一个执行文件的指针。第二行为transformation操作map的结果。此时lineLengths也没有进行运算,因为map操作为懒执行。最后,执行action操作reduce。此时Spark将运算分隔成多个任务分发给多个机器,每个机器执行各自部分的map并进行本地的reduce,最后返回运行结果给驱动程序。
如果在后面的运算中仍会用到lineLengths,可以将其缓存,在reduce操作之前添加如下代码,该persist将在lineLengths第一次计算得到后将其缓存到内存:
//scala
linelengths.persist()
//Java
lineLengths.persist(StorageLevel.MEMEORE_ONLY());
4.3.2 把函数传递到Spark(Passing Functions to Spark)
//Scala
Spark的API,在很大程度上依赖把驱动程序中的函数传递到集群上运行。这有两种推荐的实现方式:
使用匿名函数的语法,这可以让代码更加简洁。
使用全局单例对象的静态方法。比如,你可以定义函数对象object MyFunctions,然后将该对象的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)}
}
}
上面的示例中,如果我们创建了一个类实例new MyClass,并且调用了实例的doStuff方法,该方法中的map操作调用了这个MyClass实例的func1方法,所以需要将整个对象传递到集群中。类似于写成: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
在Java8中,使用lambda表达式来简明地定义函数的实现
为了保持简洁性,本指南中大量使用了lambda语法,这在长格式中很容易使用所有相同的APIs。比如我们可以把上面的代码写成:
JavaRDD<String> lines = sc.textFile("data.txt") JavaRDD lineLengths = lines.map(new Function Integer>(){ public Integer call (String s) { return s.length();} }); int totaLength = lineLengths.reduce(new Function2 Integer, Integer>(){ public Integer call(Integer a, Integer b){return a + b} });
同样的功能,使用内联式的实现显得更为笨重繁琐,代码如下:
class GetLeng implements Function Integer>{ public Integer call(String s) {return s.length();} } class sum implements Funciton2 Integer, Integer>{ public Integer call(Integer a, Integer b){ reurn a + b; } } JavaRDD lines = sc.textFile("data.txt"); JavaRDD lineLengths = lines.map(new GetLength()); int totalLength = lineLengths.reduce(new Sum());
注意,java中的内部匿名类,只要带有final关键字,就可以访问类范围内的变量。Spark也会把变量复制到每一个worker节点。
4.3.3 理解闭包
使用Spark的一个难点为:理解程序在集群中执行时变量和方法的生命周期。RDD操作可以在变量范围之外修改变量,这是一个经常导致迷惑的地方。比如下面的例子,使用foreach()方法增加计时器countter的值。
4.3.3.1示例
参考下面简单的RDD元素求和示例,求和运算是否在同一个JVM中执行,其复杂度也不同。Spark可以在local模式下(--master = local[n])执行应用,也可以将该Spark应用提交到集群上执行(例如通过spark-submit提交到YARN):
//scala
var counter = 0 var rdd = sc.parallelize(data) //wrong rdd.foreach(x=>counter ++ x) println("counter value:" + counter)
//Java
int counter = 0; JavaRDD<Integer> rdd = sc.parallelize(data); //wrong rdd.foreach(x->counter +=x); println("counter value:" + counter);
上面是错误的,应该使用累加器
4.3.3.2 本地模式VS集群模式
在本地模式下仅有一个JVM,上面的代码将直接计算RDD中元素和,并存储到counter中。此时RDD和变量counter都在driver节点同一内存空间中。
然而,在集群模式下,情况会变得复杂,上面的代码并不会按照预期的方式执行。为了执行这个job,Spark把处理RDD的操作分割成多个任务,每个任务将被一个executor处理。在执行之前,Spark首先计算闭包。闭包必须对executor可见的变量和方法,在对RDD进行运算时建辉用到这些变量和方法(本例中指foreach())。这个闭包会被序列化,并发送给每个executor。在local模式下,只有一个executor,所以所有的变量和方法都是用同一个闭包。在其他模式下情况跟local模式不一样,每个executor在不同的worker节点上运行,每个executor都有一个单独的闭包。
在这里,发送给每个executor的闭包内的变量是当前变量的副本,因此当counter在foreach中被引用时,已经不是在driver节点上的counter了。在driver节点的内存中仍然有一个counter,但这个counter对executor不可见。executor只能操作序列化的闭包中的counter副本。因此,最终counter的值仍然是0,因为所有对counter的操作都是在序列化的闭包内的counter上进行的。
在类似这种场景下,为了保证良好的行为确保,应该使用累加器。Spark中的累加器站门为在集群中多个节点间更新