spark programming guide

来源:互联网 发布:淘宝皇冠和蓝冠 编辑:程序博客网 时间:2024/05/29 04:19
一: 概要

从高级来看,每一个spark应用都包含一个驱动程序,执行用户的main方法,在cluster上执行不同的并行计算。spark提供的主要抽象是RDD,这是一个可以在cluster的各个节点上并行计算的分区元素的集合。创造RDD的方法是:使用一个hadoop文件系统中的文件,或者其他的hadoop支持的文件系统,或者是驱动中存在的scala的集合,然后也可以通过tranformation来创造RDD。用户也可以要求spark把RDD的内容存放在内存中,让他可以在接下来的并行计算中复用来提高效率。最终,RDD可以从节点的失败中自我恢复。


第二个spark的抽象是共享变量,可以在并行计算中使用。默认是这样,spark在不同节点上执行一系列任务的时候,他会把每个变量的值复制给各个任务。有时候,一个变量需要跨任务之间共享,或者说在任务和驱动程序。spark支持两种类型的共享变量:
广播变量,可以在所有的节点的内存中cache一个值,accumulators,指的是只能够被加入的变量,比如说counters和sums。


此spark指南中使用交互式终端spark-shell更容易。



二:连接spark

spark1.6.1使用scala 2.10,想要使用scala来编写应用,你需要使用一个兼容的scala版本。

为了编写spark应用,你需要在spark中加入maven的依赖关系,maven中心配置如下:

groupId = org.apache.sparkartifactId = spark-core_2.10version = 1.6.1


另外,如果你想要访问hdfs集群,你也需要加入hadoop-client的依赖关系。


groupId = org.apache.hadoopartifactId = hadoop-clientversion = <your-hdfs-version>

最终:你需要在程序中导入需要的spark类:

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








三:初始化Spark

spark程序要做的第一件事儿就是创建一个SparkContext对象,告诉spark如何访问集群。为了创建SparkContext首先需要创建SparkConf对象,SparkConf对象包含的是你的应用的信息。

valconf=newSparkConf().setAppName(appName).setMaster(master)

newSparkContext(conf)

appName这个参数是你的应用的名字,master指的是一个spark或者YARN的URL,或者也可以使本地模式的。实际上,当在cluster执行spark应用的时候,master一般不要写死,可以在spark-submit执行的时候设置master变量。



四:使用spark-shell

在spark-shell中,一个特殊的SparkContext已经为你创建好了,在sc变量中。你无法自行创建自己的SparkContext。你可以设置上下文使用的master使用--master选项,你也可以使用--jar变量添加jar包给classpath。你也可以使用--packages变量来给你的shell添加依赖关系。

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

$ ./bin/spark-shell --master local[4] --packages "org.example:example:0.1"




五: RDD
spark的核心理念就是RDD,这是一个可以并行计算的容错的元素集合。创建RDD的方法有两种:并行化驱动程序中本身存在的集合,或者是引用外部存储系统中的数据集(HDFS,Hbase,等等)


1:并行化集合

并行化集合可以通过SparkContext的parallelize方法作用在本身的驱动程序中的集合来创建。从分布式数据集中引用的集合,直接可以并行计算。

valdata=Array(1,2,3,4,5)
valdistData=sc.parallelize(data)


一旦创建之后,分布式数据集可以并行计算。我们可以调用各种方法来处理。
另一个重要的参数是数据集被切割后的分区数,spark会给每一个分区启动一个任务。一般来说,每个CPU分配2-4个分区。正常来说,spark会基于集群的信息来设置分区数目。然而,你也可以自己设置,parallelize(数据,n)这个n就是你自己设置的分区数量。


2:外部数据集

spark可以从所有支持的存储系统中创建数据集(本地文件系统,HDFS,Hbase等等)。

文本RDD,可以使用textFile方法创建,URL可以使本地路径,也可以使hdfs://等等。他会把文本内容读取成为一个按照行的集合(其实是个字符串数组,Array("每行内容","xxxx")。

一旦创建之后,可以进行各种操作。

注意的事项:

如果使用本地文件系统,这个文件必须是可以访问的,其他节点也必须可以访问。

所有spark的文件输入方法,包括textFile,也支持在目录上,压缩的文件,还有通配符也支持。

textFile方法有两个参数,第二个参数是控制切割文件的分区数目。默认的,spark会为每一个文件block创建一个分区(hdfs block大小是64M),但是你可以手动设置,但是不能比block数量多。


除了文本文件,spark的scala API还支持以下数据格式:

SparkContext.wholeTextFiles方法可以让你读取一个含有多个小文件的目录,返回(文件名,文件内容)对。这和textFile方法是不同的,textFile方法是返回文件中的每一行。


对于SequenceFiles,使用sequenceFile[K,V]方法,k和V是文件中的key和value。这些可以使hadoop的Writable接口的子类。另外,spark还允许你指定一些Writables的类型。

对于另外的hadoop输入格式,你可以使用hadoopRDD方法,使用一个抽象的JobConf和输入格式类,key 类和value类。

RDD.saveAsObjectFile和SparkContext.objectFile支持使用序列化Java对象来保存RDD。当他不如特定的数据结构Avro有效率的时候,可以使用简单的方式来保存RDD。



3:RDD操作

RDD支持两种操作:转换和action,转换是从存在的数据集中转换成另外一个,action是对现有的数据集进行操作,对驱动返回一个值。举个例子,map就是一个转换,把数据集中的每个元素都进行函数操作,返回一个新的RDD。reduce就是一个action,把RDD中所有元素使用函数计算,聚合在一起,返回一个最终值。

所有的transformation都是lazy的,很简单,你定义的时候,运算不会执行,只有在action需要结果的时候,transformation中的函数才会执行。

默认来说,每一个转换后的RDD执行action的时候都要重新计算一遍,为了提高效率,你可以把RDD 存储在内存中,使用cache或者persist方法。



传递函数给spark

spark的API支持传递函数来在cluster上运行。有两种方式可以做这些:
匿名函数方式(我在scala中曾经学到过这些)

全局单例对象中的静态方法。比如,你可以定义一个对象,把对象中的方法传递给RDD来执行。


注意,也可以参照类中的方法,需要在class中使用RDD来调用类中的方法,如下:



这里如果我们创建一个新的MyClass类,然后在上面调用doStuff方法的话,map方法调用的还是MyClass实例的方法,所以整个对象都需要发送给cluster,这类似于rdd.map(x=>this.func1(x))
(这里是scala中的this吧,指代的是:调用这个方法的对象本身)


简单来说,这两段代码是一样的





rdd.map(x => this.field + x)


为了解决这个问题,最简单的方法是把field复制到本地,而不是外部访问





四:了解闭包

关于spark很难理解的一点是,变量和方法在cluster中执行的范围和生命周期。经常会产生疑问的是:超出范围的修改变量的RDD操作。例子里,我们介绍一个使用foreach方法来增量counter。

例子:考虑到下面的原生RDD元素,可能在相同JVM中表现不同。在local模式和cluster模式下表现不同。




local模式  VS cluster模式

为了执行任务,spark把RDD的操作拆分成为tasks,每一个由一个executor执行。在执行之前,spark会计算task的closure。closure就是这些变量和方法必须对于executor是可见的,来执行RDD上的计算。closure是序列化的,而且发送给每个executor。

发送给executor的closure内的变量都是复制品,因此,当foreach方法调用counter变量的时候,她不再是driver模式下的counter。这仍然是一个内存中的counter,但是对于executor是不可见的。executor只能看到序列化closure的复制品。因此最终更新的counter的值,可能仍然是0,因为所有counter上的操作都是引用序列化closure的值。

在local模式下,foreach方法可以正确执行。
(我实验了一下,local模式下的spark-shell counter也无法更新
为了确保在这些场景下可以正常执行,需要使用Accumulator。她在spark中,当任务被分割为多个在不同节点中运行的时候,提供了一种机制,可以正确update变量的值。

一般来讲,closures,构建循环或者本地定义的方法的时候,不能够用在全局模式中。有的代码可以在本地执行,有的代码不能在global执行。在做global的聚合的饿时候,记得使用Accumulator。






五:打印RDD中的元素

在cluster模式下,调用collect()方法来打印RDD中所有元素的内容。但是注意,这很可能导致driver内存溢出,因此可以使用take方法来打印出前N个元素,rdd.take(n).foreach(println)。



六:和key-value对工作

大部分RDD上的操作包含任何对象,但是有的操作只能在key-value对的RDD上执行。最常见的操作是shuffle操作,比如说按照key来聚合或者分组。

scala中,这些操作在RDD中包含元组对象是可用的。key-value对操作在PairRDDFunctions类中可用,自动缠绕在RDD的元组中。


比如说,以下的代码使用reduceByKey操作计算出文本中的相同行的行数。


也可以使用counts.sortByKey()方法,然后使用counts.collect返回。





七:transformation

以下是RDD API中的常用transformation操作。

  

八:actions

RDD API中常用的actions操作:






九:shuffle操作

spark内部的一些特定的操作触发出shuffle操作。shuffle是spark重新分配数据的运作机理,为了把数据分组到不同的分区。shuffle包含从executor和机器中拷贝数据,这让shuffle这个操作既复杂又昂贵。


背景:

为了弄明白在shuffle中执行了什么,我们可以考虑RDD中的reduceByKey操作,这是RDD的tansformation之一。reduceByKey操作生成一个新的RDD,所有同一个key的value都被连接到一个元组中,元组包括key和在所有和key相关的reduce函数的执行结果。

困难在于,同一个key的所有value并不一定都在相同的分区中,甚至不在相同机器中,但是他们必须要同步计算结果。

在spark中,数据并不是为了某一个特殊的操作而跨分区分布的。在计算的时候,单个任务会在单个分区上操作,因此,为了让reduceByKey的单个reduce任务执行,spark需要执行一个all-to-all的操作。它必须要从所有的分区中找到所有key的值,然后把这些值跨分区放到一起,来为每一个key计算得出最终结果,这个过程就叫做shuffle。

尽管新shuffle的数据在每个分区中的数据集是确定的,分区的顺序也是确定的,但是这些元素的顺序并不是确定的。如果你想要从shuffle操作中得到可以预计的排序数据,可以使用以下方法:


mapPartition来整理每个分区的使用

repartitionAndSortWithinPartitions来有效率的分类分区
sortBy来创建一个i额全局的有序RDD

repartition和coalesce会引发shuffle和重新分区操作。

groupByKey和reduceByKey会引发shuffle和ByKey操作

join和cogroup会引发shuffle和join操作



性能影响

shuffle操作是昂贵的,因为她牵涉了硬盘IO,数据序列化和网络IO。为了组织shuffle的数据,spark创造了一系列tasks,map任务来组织数据,reduce任务来聚合数据。这里的map和reduce和hadoop里的mapreduce不同。

内部来说,map任务产生的结果会存放在内存中直到溢出。然后她们会基于任务分区来分类,写到一个单独文件中。在reduce阶段,任务会读取相关分好类的block。

特定的shuffle操作会消耗大量的堆内存,因为她们会调用内存中的数据结构来组织记录。尤其是reduceByKey和aggregateByKey操作会在map时候创建自己的结构,ByKey操作毁在reduce阶段创造。当数据不能放在内存的时候,spark会把这些表放到硬盘中,导致额外的硬盘IO和垃圾回收。

shuffle还会产生大量的临时文件在硬盘上。spark 1.3中,这些文件会在相关的RDD不再使用之后释放。因为shuffle的文件不想要重新创造,重新计算。而垃圾回收可能是在很久之后,如果应用还在引用这些RDD或者GC没有及时执行。也就是说,长时间执行的spark任务可能会占用大量的硬盘空间。临时存储目录在spark.local.dir中配置。

shuffle调优的地址:http://spark.apache.org/docs/latest/configuration.html



十:RDD持久化

spark一个很重要的能力是在操作的时候把数据集存储在内存中。当你持久化一个RDD的时候,每一个节点都会存储他的分区,然后在内存中计算,在数据集的其他action的时候重用。这让action更快。cache是一个关键的工具。


你可以使用persist()或者cache()方法来持久化RDD。第一次计算的时候,RDD会被存储在内存中。spark的cache是容错的,一旦RDD的某个分区丢失之后,它会自动重新计算,重新创造RDD。

除此之外,每一个持久化的RDD可以使用不同的存储级别来存储。存储在硬盘中,内存中,或者存储在Tachyon中。这些需要传递一个StorageLevel对象给persist()方法。cache()方法使用的是默认的存储级别,也就是存储反序列化的对象在内存中)
全部的存储级别如下:




spark还会在shuffle操作中自动持久化一些临时数据,不需要调persist方法。这是为了避免shuffle过程中,一个节点失败之后,重新计算所有的输入。如果计划重用RDD的话,我们建议使用persist方法来持久化结果RDD。


选择什么存储模式


spark的存储模式是为了提供内存和CPU之间的折中选择。选择标准如下:


如果你的RDD可以全部放在内存中,就MEMORY_ONLY,这是CPU使用率最高的选项,也是最快的选项。

如果不是,选择MEMORY_ONLY_SER,选择一个快速序列化的lib来让对象占用的空间较少,折中速度。

不要把RDD存储在硬盘中,除非计算非常昂贵,或者要filter一大堆数据。否则,重新计算一个分区和从硬盘中读取一样快。

使用replicated的存储等级,如果你想要快速容错。所有的存储等级都可以重新计算丢失的数据来实现容错,但是replicated等级让你可以不用等待重新计算分区,继续执行任务。

如果你的环境内存富有,可以使用试验性质的选项:OFF_HEAP

允许多个executor共享在Tachyon中的内存池,大量减少垃圾回收成本,cache的数据不会因为单个算子崩溃而丢失。



删除数据

spark会自动监控每个节点上的cache使用,删除旧的数据分区,如果你想要自动删除RDD ,使用unpersist()方法。





十一: 共享变量

正常情况下,spark操作调用函数在某个节点的时候,他是在变量的复制品上工作。这些变量被复制到各个节点,执行后的结果并没有返回给驱动程序。这样的话,不能共享实时更新。spark提供了两种变量来实现时时更新,broadcast变量和accumulators。


广播变量


广播变量是会在每一个节点上保留一个只读的变量,action操作分为几个步骤执行,由shuffle操作分割。spark会自动广播每个阶段任务需要的数据。广播的方式是使用序列化和范序列化格式来缓存。这意味着显示创建广播变量只有在跨越多个阶段的任务需要相同的数据的时候有用,或者缓存的数据在反序列化格式的时候有用。

广播变量通过调用SparkContext.broadcast()方法创造。broadcast变量是(v),他的值可以通过调用value方法得到。


broadcast变量创建之后,需要调用他而不是原数据,元数据就不会多次copy给各个节点。另外原数据的值不应该被改变,为了确保所有节点的值都是一样的。


Accumulators

accumulators是只能通过相关操作添加的变量,因此可以在并行计算的时候有效支持。她们可以用于实现counters或者sums。spark原生支持数字类型的accumulators,开发者可以手动添加新类型的支持。如果给accumulator命令的话,她们会战士在SPARK的UI界面中。这对于开发者理解执行阶段很有帮助。

调用SparkContext.accumulator(v)方法来创建accumlator。执行在cluster上的任务然后可以把她添加到任务中,使用add方法或者+=方法。但是cluster上的任务不能读取她的值,只有驱动程序才能读取accumulator的值,使用value方法。

代码如下:使用accumulator添加一个数组的元素。



(上面例子我的看法,使用accumulator方法的时候accumulator(v,名称)两个参数,值和这个accumulator的名称,名称会展示在spark的web界面中。
)



上面的代码是使用了accumulator内部的Int类型的支持,开发者也可以调AccumulatorParam子类创建自己的类型。接口有两个方法,zero方法提供你的数据类型的初始值,addInPlace方法可以把两个值相加。比如,我们有Vector方法代表数学向量。



在scala中,spark还支持更普遍的Accumulable接口来累加数据,当结果数据和添加的元素不同类型时。调用SparkContext.accumulableCollection方法来累加普通scala集合类型。

对于只是在actions中执行的accumulator update,spark保证每一个任务对于accumulator的update只有一次。重启任务不会再次update accumulator的值。在transformation中,用户需要知道每一个任务对于accumulator的update可能不止一次。

accumulator并不会改变spark lazy的求值模式。如果RDD中执行一次操作,只有在action的时候,accumulator才会update RDD中的值。通常情况下,accumulator的update通常不会实时执行,因为RDD的lazy模式,transformation的时候,代码不会执行,只有在action的时候才会直接执行。





十二:其余议题

代码提交给spark

一旦你把你的代码打包为JAR包之后,就可以使用spark-submit命令提交给spark

http://spark.apache.org/docs/latest/submitting-applications.html

从scala中发布SPARK 任务

org.apache.spark.launcher 包提供了发布spark任务的类,使用简单的JAVA api。

单元测试:

spark对于任何流行的单元测试框架都是友好的。设置好sparkContext的master选项为local,执行操作,然后调用SparkContext.stop()方法来停止应用。确保你可以在最后的block或者测试框架的tearDown方法中停止SparkContext,spark不支持一个程序中同时运行两个context。


十三:前方的道路


你可以在此查看spark例子程序:http://spark.apache.org/examples.html



优化程序的话,可以查看配置和调优的指南:
http://spark.apache.org/docs/latest/configuration.html

http://spark.apache.org/docs/latest/tuning.html


集群模式的详解:

http://spark.apache.org/docs/latest/cluster-overview.html







































































0 0
原创粉丝点击