综述:
在高层中,每个spark应用由一个运行用户主函数的driver program和执行各种集群上的parallel operations所组成。spark最主要的概念:RDD弹性分布式数据集,它是一个跨越“可并行操作集群”所有节点的基本分区的集合。RDDs可被多种方式创建:hadoop文件系统(或者其他hadoop支持的文件系统),或者现有的在主程序上的scala集合。用户也要求spark存一个RDD在内存,允许它被高效的反复使用。最后,RDDs可以自动恢复。
在spark中第二个概念是shared variables(共享变量)可以被用在并行操作。默认情况下,spark在不同节点上并行运行函数的时候,它将函数中的每一个变量复制到每个任务中。有时候,一个变量需要跨越任务去共享,或者在任务和程序之间共享。spark支持两种方式的共享:广播变量broadcast variables(可以用来缓存在所有节点内存上的值),累加器accumulators(只有加操作,用于计数和求和)
这个指导显示spark所支持的语言的特点,如果你有spark交互式界面,可以简单地跟随学习。
连接Spark=Linking with Spark:
spark2.2.0支持lambda表达式来写函数,否则你可以使用在 org.apache.spark.api.java.function 中的类。
注意在spark2.2.0中删除对java7的支持。
用java去写一个spark应用,你需要添加对spark的依赖,spark可以通过maven central获得:
groupId = org.apache.spark artifactId = spark-core_2.11 version = 2.2.0
此外,如果你希望访问HDFS集群,你需要为你的HDFS版本添加hadoop-client依赖项:
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;
初始化spark=Initializing Spark
spark程序必须要做的第一件事情是创建一个JavaSparkContext对象,告诉spark如何去访问集群。创建一个sparkContext你首先需要去构造一个SparkConf对象(包含了你的应用的信息)。
SparkConf conf = new SparkConf().setAppName(appName).setMaster(master); JavaSparkContext sc = new JavaSparkContext(conf);
appName参数是你的应用展示在集群UI上的名字,master是一个spark、mesos、yarn集群的URL,或者是一个特殊的本地字符串(运行在本地模式)。在实践中,当运行在集群的时候,你不希望在程序中硬编码master,而是通过spark-submit启动程序并接收它。然而,对于本地测试和单元测试,你可以通过“local”来运行spark进程。
弹性分布式数据集RDDs
spark围绕着一个称作RDD(resilient distributed dataset)的概念,它是一个可并行操作的容错集合。创建RDD有两种方式:1.在你的driver program中并行化一个已有的集合,2.在外部存储系统中引用数据集,例如共享文件系统、HDFS、任何提供Hadoop InputFormat的数据源。
并行集合
并行集合 (Parallelized collections) 的创建是通过在一个已有的集合(Scala Seq
)上调用 SparkContext 的 parallelize
方法实现的。集合中的元素被复制到一个可并行操作的分布式数据集中。在这里演示了如何在一个包含数字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)去进行列表元素的求和。以后我们再讲分布式上面的操作。
并行集合的一个重要参数是分区数(partitions),表示一个数据集被切分的分数。spark会在集群上的每一个分区运行一个任务。一般给集群上的CPU设置2-4个分区partition。通常spark会根据你的集群自动设置分区数。当然你也可以利用parallelize的第二个参数手动设置(例如:sc.parallelize(data,10)).注意:在一些代码中会使用术语“slices”切片“”(分区的同义词)去保持向下兼容性。
外部数据集
spark可以从任何hadoop支持的外部存储源来创建数据集,包括你的本地文件系统,HDFS,cassandra
,HBase、Amazon S3……。spark支持文本文件,SequenceFiles、任何其他Hadoop imputeformat。
文本文件RDDs可以使用SparkContext的textFile方法创建,在这个方法里传入文件的URI(本地路径,HDFS……),并且把它作为一个行集合读入。举例调用:
JavaRDD<String> distFile = sc.textFile("data.txt");
一旦创建,就可以通过数据集操作去使用distFile。例如,我们可以将所有行的长度相加:
distFile.map(s->s.length()).reduce((a,b)->a+b)
注意,spark读文件时:
1.如果使用本地文件系统的路径,文件必须能够被worker节点访问,要么复制文件到所有worker节点,要么使用网络方式共享文件系统。
2.所有基于文件的方法,包括textFile,支持文件目录,压缩文件、通配符。例如:你可以使用
textFile("/my/directory")
textFile("/my/directory/*.txt")
textFile("/my/directory/*.gz")
3.textFile第二参数可以控制分区数,默认的spark为每一个文件块创建一个分区(HDFS文件块大小默认为128MB)。你也可以设置更大数字的分区数,但是不能比文件块的数目小。
除了文本文件,spark JAVA API还支持其他数据格式:
1.JavaSparkContext.wholeTextFiles让你读取一个目录里多个小的文本文件,并且返回他们的每一个(名字,内容)对。而textFile记录的是文本文件的每一行。
2.对于 SequenceFiles,可以使用 SparkContext 的 sequenceFile[K, V]
方法创建,K 和 V 分别对应的是 key 和 values 的类型。像 IntWritable 与 Text 一样,它们必须是 Hadoop 的 Writable 接口的子类。另外,对于几种通用的 Writables,Spark 允许你指定原声类型来替代。例如: sequenceFile[Int, String]
将会自动读取 IntWritables 和 Text。
3.对于其他的 Hadoop InputFormats,你可以使用 JavaSparkContext.hadoopRDD 方法,它可以指定任意的 JobConf
,输入格式(InputFormat),key 类型,values 类型。你可以跟设置 Hadoop job 一样的方法设置输入源。你还可以在新的 MapReduce 接口(org.apache.hadoop.mapreduce)基础上使用JavaSparkContext.newAPIHadoopRDD
4.JavaRDD.saveAsObjectFile
和 JavaSparkContext.objectFile
支持保存一个RDD,保存格式是一个简单的 Java 对象序列化格式。这是一种效率不高的专有格式,如 Avro,它提供了简单的方法来保存任何一个 RDD。
RDD操作
RDDs 支持 2 种类型的操作:转换(transformations) 从已经存在的数据集中创建一个新的数据集;动作(actions) 在数据集上进行计算之后返回一个值到驱动程序。例如,map
是一个转换操作,它将每一个数据集元素传递给一个函数并且返回一个新的 RDD。另一方面,reduce
是一个动作,它使用相同的函数来聚合 RDD 的所有元素,并且将最终的结果返回到驱动程序(不过也有一个并行 reduceByKey
能返回一个分布式数据集)。
在 Spark 中,所有的转换(transformations)都是惰性(lazy)的,它们不会马上计算它们的结果。相反的,它们仅仅记录转换操作是应用到哪些基础数据集(例如一个文件)上的。转换仅仅在这个时候计算:当动作(action) 需要一个结果返回给驱动程序的时候。这个设计能够让 Spark 运行得更加高效。例如,我们可以实现:通过 map
创建一个新数据集在 reduce
中使用,并且仅仅返回 reduce
的结果给 driver,而不是整个大的映射过的数据集。
默认情况下,每一个转换过的 RDD 会在每次执行动作(action)的时候重新计算一次。然而,你也可以使用 persist
(或 cache
)方法持久化(persist
)一个 RDD 到内存中。在这个情况下,Spark 会在集群上保存相关的元素,在你下次查询的时候会变得更快。在这里也同样支持持久化 RDD 到磁盘,或在多个节点间复制。
基础:
为了说明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
仅仅是一个指向文件的指针。第二行是定义 lineLengths
,它是 map
转换(transformation)的结果。同样,lineLengths
由于懒惰模式也没有立即计算。最后,我们执行 reduce
,它是一个动作(action)。在这个地方,Spark 把计算分成多个任务(task),并且让它们运行在多个机器上。每台机器都运行自己的 map 部分和本地 reduce 部分。然后仅仅将结果返回给驱动程序。
如果我们想要再次使用 lineLengths
,我们可以添加:
lineLengths.persist(StorageLevel.MEMORY_ONLY());
在 reduce
之前,它会导致 lineLengths
在第一次计算完成之后保存到内存中。
传递函数给 Spark
Spark的API很大程度上依赖于驱动程序中的传递函数来在集群上运行。在Java中,函数由实现org.apache.spark.api.java.function包中的接口的类来表示 。创建这样的功能有两种方法:
- 在您自己的类中实现Function接口,作为匿名内部类或命名的内部类,并将其实例传递给Spark。
- 使用lambda表达式 来简洁地定义一个实现。
虽然本指南的大部分使用lambda语法来简洁,但是可以轻松地使用long-form中的所有相同的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; } });
请注意,Java中的匿名内部类也可以访问封闭范围中的变量,只要它们被标记final
。Spark将会将这些变量的副本以与其他语言一样的方式运送到每个工作节点。
理解闭包
在集群中执行代码时,一个关于 Spark 更难的事情是理解的变量和方法的范围和生命周期。
修改其范围之外的变量 RDD 操作可以混淆的常见原因。在下面的例子中,我们将看一下使用的 foreach() 代码递增累加计数器,但类似的问题,也可能会出现其他操作上。
示例
考虑一个简单的 RDD 元素求和,以下行为可能不同,具体取决于是否在同一个 JVM 中执行。
一个常见的例子是当 Spark 运行在本地模式(--master = local[n])时,与部署 Spark 应用到群集(例如,通过 spark-submit 到 YARN):