spark_API

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中的累加器站门为在集群中多个节点间更新


原文地址:https://www.cnblogs.com/jinb/p/9653155.html