spark编程指南

spark编程指南

概述

每一个spark应用程序包含一个驱动程序,驱动程序用来运行用户的main函数并在集群上执行若干并行操作spark最主要的抽象叫做离散分布式数据集RDD。它是跨集群节点分区的数据集,并且可以被并行操作。RDD可以由hadoop文件,或者在驱动程序中的scala集合创建或者由之前的RDD执行transformations操作转化得到。用户可能需要spark RDD 被持久化到内存中,被并行操作有效复用。RDDs可以从失败的节点中得到恢复。

其次,spark的第二个抽象就是在并行操作中的变量共享。默认情况下,spark 以任务的形式在不同的节点上执行函数的时候,它会为每一个任务传送一份函数变量的副本。有些时候,可能需要跨任务共享变量,或者在驱动程序和任务之间共享变量。


spark支持两种共享变量类型:广播变量类型和累加器。广播变量类型可以被用来在所有节点上缓存一个数值,而累加器则是一个只被允许累加操作的变量,比如求和或者计数。


这个指南介绍了spark在scala/java/python不同语言中的不同特征, 使用spark-shell交互shell来实践一下例子非常容易实现。


scala
spark1.6使用scala2.10,使用scala编写spark应用程序,需要使用合适的scala版本。(2.10.x)
使用maven管理项目,你需要添加spark依赖。


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


如果需要接入hdfs集群,需要添加符合你hdfs版本的hadoop-client依赖包
groupId = org.apache.hadoop
artifactId = hadoop-client
version = <your-hdfs-version>


之后你需要在你的程序中引入spark 的类库。
import org.apache.spark.SparkContext
import org.apache.spark.SparkConf




1.3.0之前的版本可能需要引入import org.apache.spark.SparkContext._ 来做必要的隐式转换


初始化spark

spark编程必须要做的一步就是创建SparkContext 对象,sparkContext告诉spark如何访问集群sparkContext的创建需要SparkConf作为参数,sparkConf包含你应用的信息。


一个jvm只能有一个sparkContext处于运行状态,创建之前你需要使用stop()来停止之前运行的sparkContext。
val conf = new SparkConf().setAppName(appName).setMaster(master)
new SparkContext(conf)

其中,appName指定了你应用的名字,sparkUI上用于标识你的应用;
master 指定spark url ,mesos url 或者yarn 集群的url,或者使用"local"字符串做参数,使得程序以local模式运行。事实上,当运行在集群上的时候,不要将master硬编码到代码中,选择在使用spark-submit 运行应用的时候传入会更好。而在本地测试或者单元测试,使用本地模式运行就可以了。


使用Shell



在使用spark shell中,一个特殊的解释型sparkContex已经被创建,变量名是sc,这可能使得你自己的sparkContext失效无法使用,你可以使用
--master参数来指定你的sparkContext连接那个master,你可以使用--jars 参数来添加一些类路径,多个类路径使用逗号分隔。你也可以使用
--packages参数为你的shell 会话添加依赖,多个依赖之间使用逗号分隔,使用--repositories 参数指定依赖可能存在的其他的资料库。例如,
使用四核运行bin/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"


使用spark-shell --help 获取更多参数详细介绍。


离散分布式数据集(RDDs)
spark 围绕着RDD的概念展开,RDD是一个容错的,能并行操作的数据集合。
有两种方法创建RDDs:并行化你驱动程序中的数据集合,或者参照外部存储系统,比如共享式文件系统,HDFS,HBase或者其他提供了Hadoop输入格式
的数据源。


并行化一个数据集合



你可以使用SparkContext的parallelize 方法来并行化存在你程序中的数据集合。
这个集合的数据被复制用来组织成为一个可以并行操作的分布式数据集。下面我们
介绍一下如何使用包含1到5的数据集合来创建并行化的集合。
val data = Array(1, 2, 3, 4, 5)
val distData = sc.parallelize(data)


被创建后,分布式数据集合就可以被并行操作,例如,我们可以使用distData.reduce((a, b) => a + b) 来
累加这个数组的元素。之后我们再介绍在分布式数据集上的操作。


对于并行数据集的一个非常重要的参数是将这个数据集分成几个分区数,spark为集群中的每个分区运行一个task。典型的为集群中的每个cpu分配2-4个分区。
spark会根据你的集群自动分配分区数,你可以使用SparkContext的parallelize方法的第二个参数人工手动指定分区数量(sc.parallelize(data, 10))


外部数据集

spark 可以从任何hadoop支持的数据源上创建分布式数据集,包括你的本地文件系统,hdfs,cassandra,Hbase,Amazon S3等,spark支持文本文件,序列文件,
或者其他的hadoop 输入形式。


可以使用SparkContext’s textFile方法创建一个文本文件的RDDs,这个方法使用一个url作为参数,用来指定数据源,读取它,返回一个文本行集合。
scala> val distFile = sc.textFile("data.txt")//参数可以是本地磁盘文件,hdfs://,s3n://
distFile: RDD[String] = MappedRDD@1d4cee08


distFile 可以被其他的数据集操作操作,比如,我们可以使用map 和 reduce操作来累加所有行的长度。
distFile.map(s => s.length).reduce((a, b) => a + b).


使用spark读取文件时需要注意:
1.如果使用本地的文件系统,那么这个文件必须必须在所有的工作节点上都能以相同的路径访问到。要么拷贝该文件到所有的节点上,要么使用基于网络的文件共享。
2.spark基于文件输入的方法,包括textFile,可以支持运行在目录中,压缩文件中,或者通配符等。比如,你可以使用textFile("/my/directory"), textFile("/my/directory/*.txt"), and textFile("/my/directory/*.gz").
3.textFile第二个参数是可选操作,它指定了将文本文件分成的分区数,默认情况下,spark使用64M作为文件分区的大小参考标准,你也可以自己定义更高的size。但是分区数不能小于文件块数。


除了文本文件,spark的scala API还支持其他的数据形式:
1.SparkContext.wholeTextFiles 可以读取指定目录下的大量小文件,返回值是形如(fileName,content)的数值对,在这点上于textfile是不同的。
2.对于SequenceFile 文件类型, SparkContext’s sequenceFile[K, V] ,其中K,V分别指定了文件中Key,value的类型,K,V的类型是hadoop Writable接口的子类,比如IntWritable和Text,此外,spark允许你指定一个本地类型为一个通用的Writable类型,比如sequenceFile[Int, String] 会自动读取IntWritable和Text类型。
3.对于其他的hadoop 输入形式的文件,你可以使用SparkContext.hadoopRDD,该函数接受一个比较随意的JobConf和输入格式化类,Key 类,Value类。你将使用相同的方式为你的hadoop job设置你的数据源。当然,你也可以基于新的MapReduce API 的数据格式 使用 SparkContext.newAPIHadoopRDD
(org.apache.hadoop.mapreduce).
4.RDD.saveAsObjectFile 和SparkContext.objectFile支持以简单的方式保存由java序列号对象组成的RDD。虽然这不如专门格式Avro搞笑,但是确实
比较简单的保存RDD的方法。


RDD操作

RDDs支持两种数据操作:
transformations: 从一个已经存在的RDD创建一个新的RDD。
actions:从一个RDD返回一个数值给驱动程序。
比如map是一个transformation,它把一个数据集传递给一个函数,并产生新的RDD。
另一方面,reduce使用某个函数累加一个RDD中的数据,并返回最终的结果给驱动函数,reduce是一个action。并行函数reduceByKey返回一个分布式的数据集。


在spark中执行的所有transformations都是懒执行,它们不会马上去执行响应的计算。相反,它们这是记住对应的转化操作会作用于哪一个基本的RDD,只有当一个
action需要返回一个结果时对应的transformation才会真正被执行,这种设计思想使得spark可以更加高效的执行。比如,我们可能意识到map操作的结果可能会被用于
reduce操作,而且只有reduce的返回值会被返回给驱动函数,而不是较大的映射数据集。


默认情况下,每次执行action操作,对应的RDD都会被重新计算一次,然而,你可以将RDD持久化到内存中,需要使用persist()函数。这样可以使得你在下一次需要查询它时,

可以更加快速的访问到它,不仅支持持续化RDD到内存,还支持持续化一个RDD到硬盘上,或者跨结点备份。

基础

为了说明RDD基础,思考以下简单的程序:
val lines = sc.textFile("data.txt")
val lineLengths = lines.map(s => s.length)
val totalLength = lineLengths.reduce((a, b) => a + b)

lineNo.1:从一个文本文件创建一个基本的RDD,这个数据集没有被加载到内存中,也没有执行别的操作。lines只是这个文件的指针。
lineNo.2:lineLengths 定义为map 转化的结果集。在强调一遍,map操作不会马上执行,应为是懒执行的。
lineNo.3:我们执行reduce操作,reduce是一个action操作,有对应的数值返回值。执行reduce的时候,spark会将计算任务分散到各个节点上,每个机器执行属于自己那部分的map操作
和本地的reduce操作,并返回各自的结果给主调函数。
如果我们之后会在此用到lineLengths,我们可以在执行reduce之前使用persist()函数,这样在执行第一次计算的时候会将lineLenghts保存在内存中。

传递函数给Spark

在集群上运行的时候,Spark的API很大程度上依赖传递给驱动程序的函数。这里有两种推荐的方式:
1.匿名函数,常用语短代码块
2.静态方法。比如你可以定义一个object MyFunctions,并传递MyFunctions.func1
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) }
}

这里,如果我们创建一个MyClass的实例对象并在其上调用doStuff操作,其中的map操作引用了那个实例的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),都是引用了整个对象,为了避免这个问题,最简单的方法就是复制这个字段给局部变量,而非从外部访问。

def doStuff(rdd: RDD[String]): RDD[String] = {
  val field_ = this.field
  rdd.map(x => field_ + x)
}

理解闭包

对于spark比较难懂的一件事就是当跨集群执行操作的时候,理解变量和方法的作用范围和生命周期。
修改其作用范围之外的变量的RDD操作是困惑的主要原因。下面我们将使用foreach()来累加计数。其实在其他的操作也会存在同样的问题。


例子:
思考本地的RDD数据集的和。结果可能会根据你是否运行在同一个jvm中产生不同的表现结果。这方面最常见的一个例子就是运行在本地模式下与运行在集群中的应用。
var counter = 0
var rdd = sc.parallelize(data)

// Wrong: Don't do this!!
rdd.foreach(x => counter += x)

println("Counter value: " + counter)


本地模式与集群模式

以上代码的执行行为是未可知的,也可能不会像预期的那样去工作。运行任务集,spark将RDD操作的流程分发给不同的任务,每个任务被一个执行器去执行。
运行之前,spark计算出任务的闭包。这个闭包里包含了整个执行器在执行计算过程中可能要访问的变量和方法。这个闭包会被序列化并发送给每个执行器。


发送给每个执行器的闭包里的变量现在被拷贝,如果在foreach中counter被引用,它便不再是驱动节点上的counter。在驱动节点上的内存中仍然会有一个counter,但是对执行器不再可见。执行器只能看到被拷贝的序列化后的闭包。因此,counter的最终值还是0因为所有的在counter上的操作都是引用的闭包中的counter。


在本地模式下,在某种情况下foreach函数实际上和驱动一样是运行在一个jvm中的,将引用原来的counter,实际上也可能会更新它。


为了保证在这种场景下比较好的实现和运行,我们需要使用累加器。在spark中累加器被特定用于在执行分跨集群多个节点时提供变量安全更新机制。
本章节的 Accumulators 部分将更加详细介绍这些。


通常,闭包-结构类似于循环或者本地定义的方法,不能用来改变全局状态。spark无法定义和保证改变外部引用的行为。一些可能在本地能正常运行的代码,但是在分布式模式下却无法像预期那样工作。Accumulators用于一些全局的累加。




打印RDD的数据项

另一种常见的语法就是试图使用rdd.foreach(println) 或者 rdd.map(println)方法打印RDD中的元素。
单机上,上述操作会生成期望的输出并打印所有的RDD元素。然而在,在集群模式下,调用输出到标准输出只会在
执行该函数的控制器的标准输出上,并不是驱动程序所在的那个结点上的标准输出。因此不会显示全部完整的元素。
为了在驱动程序所在的节点上输出所有的元素,我们可以使用collect()函数聚集所有的元素,rdd.collect().foreach(println)。这可能会导致drive所在的节点内存耗尽。因为collect()会将获取整个RDD到一个机器。如果你只是需要打印一些元素。一种安全的方式就是使用take(): rdd.take(100).foreach(println).




使用键值对

大量运行在RDD上的spark操作包含任何类型的对象,有些操作只适用于键值对。最常见的一种操作就是"shuffle",比如按键值分组和聚合。
在scala中,这些操作自动适用于包含二元组对象的RDD,(scala语言内置的元组)。键值对操作在 PairRDDFunctions类中很实用,PariRDDFunctions类
围绕元组的RDD自动换行。


例如,下面的代码使用reduceByKey操作于键值对,进而统计出每行文本在文件中出现的次数。
val lines = sc.textFile("data.txt")
val pairs = lines.map(s => (s, 1))
val counts = pairs.reduceByKey((a, b) => a + b)


我们也可以使用counts.sortByKey()实现这些键值对的字符顺序排序,最后通过调用collect()以数组的形式返回给驱动程序。
注意:当在键值对操作中使用自定义的对象时,必须确保自定义equals()方法匹配一个hashCode()方法。详情请参考Object.hashCode()相关文档。




Transformations

下面列出了一些常用的spark转化操作。


map(func) : 将一个数据集通过给定的函数转化为一个新的分布式数据集。


filter(func): 将一个数据集通过给定的函数转化,返回函数执行返回值为true的数据集。


flatMap(func): 和map相似,但是每个数据输入项可以被映射为0个或者多个输出项。


mapPartitions(func): 和map相似,但是单独运行在RDD每个partition上,所以函数必须是Iterator<T> => Iterator<U>类型,输入类型是T。


mapPartitionsWithIndex(func): 和mapPartitions相似,但是提供了标识索引参数的函数。


sample(withReplacement, fraction, seed) 使用一个给定的随机数种子,采样一个数据片段。


union(otherDataset) : 将源数据集和参数数据集合并后的数据集返回。


intersection(otherDataset): 将源数据集和参数数据集的交集作为新的数据集返回。


distinct([numTasks])): 返回源数据集去重后的数据集。


groupByKey([numTasks]): 在键值对形式的数据集上操作,返回(K, Iterable<V>) pair类型。

如果你这是聚合操作,使用reduceByKey or aggregateByKey会更好。


默认情况下,并行的级别取决于父RDD的分区数,你可以通过设置numTasks来设置不同数量的task数目。

reduceByKey(func, [numTasks]): 作用于键值对的数据集上,返回值也是键值对,只是值是通过func通过key聚合后的新value。同样可以设置numTask数目。


aggregateByKey(zeroValue)(seqOp, combOp, [numTasks]):在 (K, V) 类型的数据集上调用该函数,返回一个(K, U) 类型的数据集。其中U的值是源数据集对每个key按照给定的组合函数和中值0进行聚合计算以后的返回值.允许聚合以后的数据类型和输入的value的数据类型不同。


像groupByKey,reduce task的数目可以通过第二个参数可以配置。


sortByKey([ascending], [numTasks]):在(K, V)类型的数据集上调用该函数,通过key的升序或者降序排序后返回新的数据集。其中ascending 是布尔类型。


join(otherDataset, [numTasks]):操作在类型为(K, V) and (K, W)的数据集上,返回一个(K, (V, W))类型的数据集。外链接类型支持leftOutJoin,rightOutJoin, 和  fullOutJoin.


cogroup(otherDataset, [numTasks]): 操作在类型(K, V) and (K, W)类型的数据集上,返回一个(K, (Iterable<V>, Iterable<W>))类型的数据集。


cartesian(otherDataset):操作的数据集类型是T and U,返回一个类型为(T, U) 键值对的数据集。

pipe(command, [envVars]):通过shell命令管道操作RDD的每一个分区,RDD数据元素被写入处理过程的输入,标准输出的文本行被作为string的RDD返回。


coalesce(numPartitions):减少RDD的分区到指定的分区数量,在过滤下一个大的数据集之后更高效的执行操作会有用。


repartition(numPartitions):随机重新洗牌RDD中的数据元素,然后创建更多或者更少的分区,并在它们之间进行平衡。常用于打乱网络上的所有数据。


repartitionAndSortWithinPartitions(partitioner):根据给定的分区器或者分区函数进行重新分区,对每个结果分区,按照key进行排序。这比调用repartition之后再在每个结果分区中按照key排序更加高效。它可以推动分拣进入shuffle操作。

Actions:

下面列出了一些常用的spark Actions操作。


reduce(func):使用给定的函数聚合数据集中的元素。该函数必须是可交换的,关联的,这样它才能并行准确的计算。


collect():以数组的形式返回RDD上的所有元素。对于一个返回值是小数据集的操作,该操作是非常有用的。


count():返回数据集中的数据长度。


first():返回数据集的第一个元素,效果类似于take(1)


take(n): 返回数据集的前n个元素。


takeSample(withReplacement, num, [seed]):按照随机量,返回指定个数的数据元素。


takeOrdered(n, [ordering]):返回按自然顺序或者自定义顺序排好序之后的数据集的前n个元素。


saveAsTextFile(path):将数据集作为一个文本文件集保存到本地的文件系统,HDFS或者其他hadoop支持的文件系统。spark可以通过tostring将数据元素转成文件的文本行。


saveAsSequenceFile(path) :将数据集作为一个序列文件或者文本文件集保存到本地的文件系统,HDFS或者其他hadoop支持的文件系统。这个函数适用于键值对RDD,前提是该RDD实现了hadoop的Writable接口。scala里也适用于可以隐式转换为Writable的数据类型。(spark包含了基本类型的转化,比如int,double,String等)


saveAsObjectFile(path) :使用java 序列化以一种简单的方式写数据集元素。可以使用 SparkContext.objectFile() 加载数据。


countByKey():只适用于(K, V)类型的RDD,返回一个(K, Int) pairs类型的hashmap,其中K是每个数据元素的键,Int是每个键出现的数量。


foreach(func):在数据集的每个元素上调用给定的函数,这通常被用于更新累加器或者和外部存储系统交互。
注意:修改foreach以外的累加器比修改其他的变量更容易引起不可预测的结果。



  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值