从系统的高层讲,每一个Spark应用程序都包含着一个驱动程序,驱动程序执行用户的main方法和执行在集群上的不同的parallel操作。Spark提供的一个主要的抽象是RDD(弹性分布式集合, resilient distributed dataset)。RDD是多个可在集群中分片的元素的集合。因此,它们可以被并行操作。RDD的创建可以是由HDFS中的一个文件开始创建(或者任何其他hadoop支持的文件系统),或者由在驱动程序中的Scala集合创建,然后转换这个集合。使用者也可以让RDD驻留在内存中,这样可以使RDD更有效的进行并行操作。最后,RDD可以从结点故障中自动的恢复。
Spark中的第二抽象是在并行操作中使用的共享广播变量。默认情况下,当Spark在不同的结点中运行一个作为一组任务的方法时,在每个任务的方法中都会有每个变量的拷贝。有时,某些变量需要在各个任务中共享,或者需要在驱动程序和任务之间共享。Spark支持两种类型的共享变量,共享广播变量(在所有结点的内存中缓存),累加器(变量只能被”加”,例如计数和求和)
这篇guide将展示在各个spark所支持语言中这些特色。如果你使用Spark交互式shell程序,这将变得很容易。bin/spark-shell来启动Scala shell,bin/pyspark来启动python shell。
和Scala相连接
Spark 2.2.0 默认是分布式的,和Scala 2.11相匹配。为了用scala编写应用程序,你需要使用兼容的scala版本(例如,2.11.X)
为了编写Spark应用程序,你可以添加通过maven中心仓库添加maven依赖:
groupId = org.apache.spark
artifactId = spark-core_2.11
version = 2.2.0
另外,如果你希望使用HDFS集群,则你需要添加hadoop-client依赖(对应你的hdfs版本):
groupId = org.apache.hadoop
artifactId = hadoop-client
version =
最后,你需要引入一些Spark class,加入下面的两行代码:
import org.apache.spark.SparkContext
import org.apache.spark.SparkConf
在一个Spark程序中,首先要做的是创建一个SparkContext对象。这个对象向Spark参数了如何利用集群。为了创建SparkContext对象,你首先需要创建SparkConf对象,该对象包含着你的应用信息:
val conf = new SparkConf().setAppName(appName).setMaster(master)
new SparkContext(conf)
参数appname是你的应用程序名称。参数maser是Spark,Mesos 或者YARN集群的URL,或者一个特殊的”local”来执行本地调试。在实际中,当在集群中运行时,你并不想在程序中硬编码master,而是采用spark-submit来提交应用程序。然而,为了本地调试和单元测试,你可以使用”local”来运行Spark。
在Sparkshell中,一个特殊的SparkContext已经为你创建好了,这个变量称为sc。使用你自己的SparkContext并没有用。你可以使用–master参数来指定连接到哪个master,你可以列出–jars参数来说明添加的JARs(类路径)。你也可以通过–package参数来添加依赖关系(逗号分隔的Maven坐标)。任何附加的仓库(依赖可能存在的地方)都能通过参数–repositoried给出。例如,以四核的方式执行spark-shell程序,采用:
$ ./bin/spark-shell --master local[4]
例如,在类路径中添加code.jar:
$ ./bin/spark-shell --master local[4] --jars code.jar
使用maven坐标添加依赖:
$ ./bin/spark-shell --master local[4] --packages "org.example:example:0.1"
使用shark-shell -help来获取完整的选项列表。在幕后,spark-shell通常是调用spark-submit脚本。
Spark是围绕着RDD的概念的。RDD是一个容错的,可以并行操作的元素的集合。有两种方式来创建RDD,从驱动程序已存在的集合创建,或者引用外部文件系统的一个集合,例如共享文件系统HDFS,HBase或者任何其他能提供Hadoop输出格式的数据源。
Parallelized Collections是通过调用SparkContext的parallelize方法生成的(参数为驱动程序中已存在的集合,是一个Scala Seq)。这个集合中的元素将被复制,来形成一个分布式的,可并行操作的集合。例如,下面是一个创建含有数字1-5的parallelized集合:
val data = Array(1, 2, 3, 4, 5)
val distData = sc.parallelize(data)
一旦创建RDD,这个分布式的集合(distData)就可以被并行操作。例如,我们可以调用 distData.reduce((a, b) => a + b)来对数组中所有元素求和,我们接下来将讨论分布式集合的操作。
parallel collections的一个很重要的参数是分片数。分片数描述了将集合切成多少个。Spark将为集群的每个分区运行一个任务。典型情况下,在你的集群中希望为每个CPU分配2-4个分片。一般的,Spark会根据你的集群自动设置分片数。然而,你可以通过传递parallelize方法的第二个参数来手动的设置(例如,sc.parallelize(data, 10))。注意,在代码中的有些地方会使用术语slice(和partition同义)来维持向后的兼容性。
Spark能从任何支持Hadoop的文件源中(这包括你的本地文件系统,HDFS, HDFS, Cassandra, HBase, Amazon S3)来创建RDD。Spark支持文本格文件,序列化文件和其他任何Hadoop输入格式的文件。
文本文件RDD可以通过SparkContext的textFile方法创建。参数为文件的URI(要么是本地文件路径,要么是URI,例如hdfs://, s3n://),并且是以行的集合的形式读的,下面是一个调用的例子:
scala> val distFile = sc.textFile("data.txt")
distFile: org.apache.spark.rdd.RDD[String] = data.txt MapPartitionsRDD[10] at textFile at :26
一旦创建,distFile能够执行dataset操作。例如,我们可以计算所有行的长度和,通过map和reduce方法:distFile.map(s => s.length).reduce((a, b) => a + b).
1.如果采用本地文件路径,这个文件必须是worker结点可读的。
2.所有Spark 的Input方法(包括textFile),都支持目录文件,压缩文件和在路径中使用通配符。你可以使用 textFile(“/my/directory”), textFile(“/my/directory/.txt”), 和 textFile(“/my/directory/.gz”)
3.textFile方法也可以有第二个参数来控制文件分片数。通常情况下,Spark为文件的每个块创建一个切片(在HDFS中,一个块的默认大小为128MB),但是你也可以设置更高的切片数(通过传递更高的参数)。要注意,分片数不能低于块数。
4.SparkContext.wholeTextFiles允许读一个文件目录,该目录下有许多小的文本文件,并且以(文件名,内容)对的方式返回。这和 textFile是不同的, textFile在每个文件的每一行都返回一条记录。同样的,你也可以设置切片数。
RDD支持两种类型的操作: transformations,从已存在的RDD中创建一个新的RDD,actions,在对集合操作后向驱动程序返回一个值。例如,map方法是 transformation,传递集合中的每个元素并通过一个函数返回一个新的RDD(结果RDD)。另一方面,reduce是一个acticon,使用一些方法来聚合所有的元素并向驱动程序返回最后的值(也有一个并行操作的方法reduceByKey返回一个分布式集合) 。
所有的transformations都是懒加载的,即它们不会马上计算结果。反而,它们仅仅记录操作和操作对应的RDD。仅当一个aciton需要向驱动程序返回值时,transformations才会执行计算。这种设计使得Spark运行更加高效。例如,我们可以意识到:通过map方法创建的集合可以被用于reduce方法,那么可以仅仅向驱动程序返回reduce后的结果,而不是一个更大的map后的集合。
默认情况下,每个 transformadRDD可能被重新计算出来(当在这个RDD上执行action操作时)。兰赫然,你可以让一个RDD驻留在内存中,通过使用persist或者cash方法,在这种情况下,Spark将在集群中保留这些元素,这样下一次访问时会更快。也支持将RDD驻留在磁盘上,或者在多个结点中复制。
一个很常见的习惯是使用rdd.foreach(println) 或者rdd.map(println)来企图打印RDD中的每个元素。在一个单机上,这会产生期待的结果并且打印出RDD的元素。然后,在集群模式下,执行者将将输出打印到执行者自己标准输出文件中,而不是驱动程序的。因此,驱动程序的标准输出文件将不会展示这些。为了在驱动程序上打印所有的元素,你可以首先使用collect()方法,将RDD改为驱动模式,因此rdd.collect().foreach(println())可以输出正确的结果。但是这可能使内存溢出,因为collect方法获取完整的RDD到一个单机中。如果你仅仅想打印某些元素,一个安全的方法是使用take():
rdd.take(100).foreach(println).
操作key-value对:
尽管Spark的操作包含任何类型的对象,一些特殊的操作仅能操作key-value对。最常见的是”shuffle(洗牌)”操作,例如通过key值分组或者聚集元素
在Scala中,这些操作会自动的变成可用的,只要RDD包含元组类型的对象。
例如,下面的代码使用 reduceByKey方法对每个key-value对操作,统计一个文件中各个行出现的次数:
val lines = sc.textFile("data.txt")
val pairs = lines.map(s => (s, 1))
val counts = pairs.reduceByKey((a, b) => a + b)
我们也可以使用sortByKey()方法,将key-value对按字母顺序的排序,最后使用collect()将它们送回驱动程序,这将返回一个对象数组。
注意:当key为自定义的对象时,你必须保证自定义对象中有equals方法和伴随的hashCode方法。
下表将列出Spark支持的常用的Transformations方法。
Trannsformation | Meaning |
---|---|
map(func) | 返回一个新的RDD,格式为:每个元素,通过func操作后的返回值。 |
filter(func) | 返回一个新的RDD,其元素满足func返回值为true |
flatMap(func) | 和map类似,但是每个输入元素可以变为0个或者多个输出元素(此,func应该返回一个Seq而不是单一的元素) |
mapPartitions(func) | 和map类似,但是在RDD的每个分片上独立的运行,因此func必须是Iterator => Iterator ,RDD为T类型 |
mapPartitionsWithIndex(func) | 和mapPartitions类似,但是为func提供了一个代表分区索引的整数,因此func必须是 (Int, Iterator) => Iterator 类型,RDD为T类型 |
sample(withReplacement, fraction, seed) | Sample a fraction fraction of the data, with or without replacement, using a given random number generator seed. |
union(otherDataset) | 返回一个新的集合,是两个集合的并集 |
intersection(otherDataset) | 返回一个新的集合,是两个集合的交集 |
distinct([numTasks])) | 返回一个新的集合,是两个集合的差集 |
groupByKey([numTasks]) | 当对一个(K,V)对的集合操作时,返回一个 (K,Iterable)的集合。注意:如果你分组是为了计算和或者聚合元素,采用 reduceByKey 或者 aggregateByKey效果更佳 |
reduceByKey(func, [numTasks]) | 当对一个(K,V)对的集合操作时,返回一个(K,V)对的集合。该集合中,每个key的value值是通过func聚合的。func必须是(V,V) => V类型。类似于groupByKey,可以设置分片数。 |
aggregateByKey(zeroValue)(seqOp, combOp, [numTasks]) | 当对一个(K,V)对的集合操作时,返回一个(K,U)对的集合,集合中每个队的value是通过combine方法和一个初始值计算出的。允许输出的聚合值的类型和输入类型不同。 |
sortByKey([ascending], [numTasks]) | 当对一个(K,V)对的集合操作,K实现了Order接口时,返回一个按照key值升序或者降序排序的集合(由参数ascending指定) |
join(otherDataset, [numTasks]) | 当对集合(K,V)和集合(K,W)操作时,返回一个 (K, (V, W)) 对,是全外连接。也可以使用leftOuterJoin, rightOuterJoin, and fullOuterJoin. |
cogroup(otherDataset, [numTasks]) | 对集合(K,V)和集合(K,W)操作,返回(K, (Iterable, Iterable))元组集合。这个方法也被称为groupWith |
cartesian(otherDataset) | 笛卡尔积, 对T集合和U集合操作,返回(T,U)对。 |
下表将列出Spark支持的常用的Action方法。
Action | Meaning |
---|---|
reduce(func) | 通过函数func聚集集合中的元素(func需要有两个参数和一个返回值) |
collect() | 以数组的形式,向驱动程序返回一个集合中的所有元素 |
count() | 返回集合的元素个数 |
first() | 返回集合的第一个元素 |
take(n) | 返回一个数组,该数组包含集合的前n个元素。 |
takeOrdered(n, [ordering]) | 返回排序后的前n个元素 |
saveAsTextFile(path) | 将集合中的元素写入文件,path给出文件的路径,可以是本地文件系统,HDFS,或者任何其他支持hadoop的文件系统。 Spark将对每个元素调用toString方法写入文件的一行。 |
saveAsSequenceFile(path) | 将集合中的元素以SequenceFile格式写入文件。RDD必须实现Hadoop的Writable接口。在Scala中,如果存在隐saveAsObjectFile(path) 隐式转换到Writable也是可用的。 |
saveAsObjectFile(path) | 以java序列化格式写入。能够通过SparkContext.objectFile().加载。 |
countByKey() | 仅对(K,V)对类型的RDD可用。返回一个(K,Int)对的hashmap,value为每个key的数量。 |
foreach(func) | 对集合的每个元素调用func方法。这个方法经常发生副作用。注意:引用foreach之外的变量可能发生未知的错误。建议采用Accumulator |