SparkCore——RDD编程(2)
本节学习RDD编程,比如RDD的创建,转换算子和行动的算子的使用等等。
一、编程模型
在Spark中,RDD被表示为对象,通过对象上的方法调用来对RDD进行转换。经过一系列的transformations定义RDD之后,就可以调用actions触发RDD的计算,action可以是向应用程序返回结果(count, collect等),或者是向存储系统保存数据(saveAsTextFile等)。在Spark中,只有遇到action,才会执行RDD的计算(即延迟计算),这样在运行时可以通过管道的方式传输多个转换。
要使用Spark,开发者需要编写一个Driver程序,它被提交到集群以调度运行Worker,如下图所示。Driver中定义了一个或多个RDD,并调用RDD上的action,Worker则执行RDD分区计算任务。
二、RDD编程
一般来说,除非有非常非常明确的理由,否则不要手动创建RDD。它们是很低级的API,虽然它提供了大量的功能,但同时缺少结构化API中可用的许多优化。在绝大多数情况下,DataFrame比RDD更高效、更稳定并且具有更强的表达能力。当你需要对数据的物理分布进行细粒度控制(自定义数据分区)时,可能才需要使用RDD。
RDD编程包括RDD的创建和操作。RDD的操作又分为转换操作(transform) 和 行动操作(action)操作。
三、创建RDD
目前有两种类型的基础RDD:
一种是并行集合(Parallelized Collections),接收一个已经存在的Scala 集合,然后进行各种并行计算;
另一种是从外部存储创建RDD,外部存储可以是文本文件或Hadoop文件系统HDFS,还可以是从Hadoop接口API创建。(当然,也可以从其他RDD转换创建新的RDD)
①并行集合创建RDD
并行化集合是通过调用SparkContext的parallelize方法,在一个已经存在的Scala 集合上创建的(一个Seq对象)。集合的对象将会被复制,创建出一个可以被并行操作的分布式数据集。
-
parallelize() 创建RDD
parallelize()方法源码如下:def parallelize[T: ClassTag]( seq: Seq[T], numSlices: Int = defaultParallelism): RDD[T] = withScope { assertNotStopped() new ParallelCollectionRDD[T](this, seq, numSlices, Map[Int, Seq[String]]()) }
-
makeRDD()创建RDD
makeRDD()底层调用的parallelize(),本质上是一样的。makeRDD还可以指定每一个分区的首选位置def makeRDD[T: ClassTag]( seq: Seq[T], numSlices: Int = defaultParallelism): RDD[T] = withScope { parallelize(seq, numSlices) }
-
示例:使用
parallelize()和makeRDD()
创建RDD
说明: 示例是在spark-shell演示的,sc 代表 SparkContext。
②外部存储创建RDD
Spark可以将任何Hadoop所支持的存储资源转化成RDD,如本地文件、HDFS、Cassandra、HBase、Amazon S3等。Spark 支持TextFile、SequenceFiles 和任何Hadoop InputFormat格式。常用的读取方法有:textFile、sequenceFile、hadoopFile、hadoopRDD等等。
-
示例: 使用textFile读取本地 和 HDFS文件
TextFlie源码如下:
def textFile( path: String, minPartitions: Int = defaultMinPartitions): RDD[String] = withScope { assertNotStopped() hadoopFile(path, classOf[TextInputFormat], classOf[LongWritable], classOf[Text], minPartitions).map(pair => pair._2.toString).setName(path) }
读取本地文件和HDFS文件:
四、转换操作(transform)
RDD转换操作整体上分为值Value类型和键值对Key-Value类型。转换操作需要在已有RDD上指定转换操作来创建一个新的RDD,这也将导致新的RDD依赖于原有的RDD。
1.Vaule类型的转换操作
① distinct
-
在RDD上调用distinct方法用于删除RDD中的重复项。
-
示例:求出Seq(1,2,2,3,4)不重复元素个数。
注:RDD是懒执行的,只有遇到行动操作(行动算子),才会真正的执行代码。所以示例需要加上行动算子count才会有执行结果。
② filter(func)
-
过滤,返回一个新的RDD,该RDD由经过func函数计算后返回值为true的输入元素组成。
-
示例:创建一个RDD(由字符串组成),过滤出一个新RDD(以"S"开头的单词)
val myCollection = "Spark The Definitive Guide : Big Data Processing Made Simple".split(" ") val words = sc.parallelize(myCollection) words.filter(word => word.contains("S")).collect() words.filter(_.contains("S")).collect()
-
注意:不能写成以下形式:
words.filter( _ => _.contains("S")).collect() words.filter( (_:String) => _.contains("S")).collect()
写法涉及到Scala函数字面量知识,可以参考文章:Scala函数式编程高级
③ map(func)
-
指定一个func函数,将给定数据集中的记录一条一条地输入该函数处理以得到你期望的结果。
-
示例:将words 组合成二元组
words.map(word => (word,1)).collect words.map((_,1)).collect
④ flatMap(func)
-
flatMap是对上面的map函数的一个简单扩展。对集合中每个元素进行操作然后再扁平化。
-
示例:将words1 组合成二元组。为了更好的显示map 和 flaMap区别,新定义一个words1
val words1 = sc.parallelize(List("Spark Spark","The The","Definitive Definitive", "Guide Guide")) words1.flatMap(word => word.split(" ")).collect()
而是用map的结果:words1.map(word => word.split(" ")).collect()
从结果看出:执行map算子,得到的结果是类型是:Array[Array[String]]
,而使用flatMap得到的是Array[String]
。没有嵌套的结构,直接在map结果上再执行了一次扁平化的操作。
⑤ mapPartitions(func)
-
类似于map,但独立地在RDD的每一个分片上运行,因此在类型为T的RDD上运行时,func的函数类型必须是Iterator[T] => Iterator[U]。假设有N个元素,有M个分区,那么map的函数的将被调用N次,而mapPartitions被调用M次,一个函数一次处理所有分区。
-
示例:将words 组合成二元组
words.mapPartitions(x => x.map((_,1))).collect()
-
注意:不能直接写成:
words.mapPartitions((_,1))
,func的函数类型必须是Iterator[T] => Iterator[U]
⑥ mapPartitionsWithIndex(func)
-
类似于mapPartitions,但func带有一个整数参数表示分片的索引值,因此在类型为T的RDD上运行时,func的函数类型必须是(Int, Interator[T]) => Iterator[U];
-
示例:创建一个RDD,使每个元素跟所在分区形成一个元组组成一个新的RDD
words.mapPartitionsWithIndex((index,word) =>(word.map((index,_))) ).collect
⑦ groupBy(func)
-
分组,按照传入函数的返回值进行分组。将相同的key对应的值放入一个迭代器。
-
示例:创建一个RDD,按照元素模以2的值进行分组
rdd1.groupBy(_ % 2).collect()
⑧ coalesce、repartition
-
coalesce 作用:缩减分区数,用于大数据集过滤后,提高小数据集的执行效率。
-
repartition作用:根据分区数,重新通过网络随机洗牌所有数据。
-
区别:
- coalesce重新分区,可以选择是否进行shuffle过程。由参数shuffle: Boolean = false/true决定。
- repartition底层调用的coalesce,默认是进行shuffle的。源码如下:
def repartition(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T] = withScope { coalesce(numPartitions, shuffle = true) }
- 案例:对rdd1缩减分区。
⑨ sortBy(func,[ascending], [numTasks])
- 使用func先对数据进行处理,按照处理后的数据比较结果排序,默认为正序。
- 示例:将rdd1按照不同的规则进行排序
2.Key-Value类型的转换操作
① partitionBy
- 对pairRDD (Key-Value RDD)进行分区操作,如果原有的partionRDD和现有的partionRDD是一致的话就不进行分区, 否则会生成ShuffleRDD,即会产生shuffle过程。
- 示例:创建一个4个分区的RDD,对其重新分区
val kvRdd = sc.parallelize(Array((1,"aaa"),(2,"bbb"),(3,"ccc"),(4,"ddd")),4) val kvRdd1 = kvRdd.partitionBy(new org.apache.spark.HashPartitioner(2))
② mapValues
- 与map类似,但是针对pairRDD类型只对值进行操作。
- 示例:给kvRdd的vaule拼接字符串"|||"
scala> val kvRdd2 = kvRdd.mapValues(_+"|||").collect()
③ flatMapVaules
- 与flatMap类似,flatMapVaules针对[K,V]RDD中的V值进行flatMap。
④ grouByKey
- 分组 groupByKey操作用于将RDD[K,V]中每个K对应的V值合并到一个集合Iterable[V]中。
- 示例:将RDD中每个K对应的V值合并到集合Iterable[V]中
kvRdd.groupByKey().collect
- 应用:使用groupByKey完成单词统计,即wordcount:
//读取本地 /tmp/README.md文件,统计README.md文件中单词出现的数量 val lines = sc.textFile("/tmp/README.md")//读取文件 val wordCount = lines.flatMap(_.split(" ")).map((_,1)).groupByKey().map(t =>(t._1,t._2.sum))//wordcount wordCount.collect()//收集显示结果
⑤ ReduceByKey(func,[numTasks])
- reduceByKey操作用于将RDD[K,V]中每个 K对应的V值根据映射函数来运算,其中参numPartitions
用于指定分区数。 - 示例: 计算相同key对应值的和。
pairRDD.reduceByKey(_+_)
- 应用:使用reduceByKey完成单词统计,即wordcount:
val lines = sc.textFile("/tmp/README.md")//读取文件 val wordCount = lines.flatMap(_.split(" ")).map((_,1)).reduceByKey(_+_) wordCount.collect()//收集显示结果
⑥ aggragateByKey
- 参数:(zeroValue:U,[partitioner: Partitioner]) (seqOp: (U, V) => U,combOp: (U, U) => U)
- 参数描述:
(1)zeroValue:给每一个分区中的每一个key一个初始值;
(2)seqOp:函数用于在每一个分区中用初始值逐步迭代value;
(3)combOp:函数用于合并每个分区间的结果。 - 作用:在kv对的RDD中,按key将value进行分组合并,合并时,分区内部将每个value和初始值作为seq函数的参数,进行计算,返回的结果作为一个新的kv对,然后再将结果按照key进行合并,最后将每个分组(分区之间)的value传递给combine函数进行计算(先将前两个value进行计算,将返回结果和下一个value传给combine函数,以此类推),将key与计算结果作为一个新的kv对输出。
- 示例:创建kvRdd3,取出每个分区相同key对应的最大值,然后相加。
val kvRdd3 = sc.parallelize(List(("a",3),("a",2),("c",4),("b",3),("c",6),("c",8)),2) val agg = kvRdd3.aggregateByKey(0)(math.max(_,_),(_+_)) agg.collect()
- 案例解析:
- 应用:使用aggregateByKey完成单词统计,即wordcount:
val lines = sc.textFile("/tmp/README.md")//读取文件 val wordCount = lines.flatMap(_.split(" ")).map((_,1)).aggregateByKey(0)(_+_,_+_) wordCount.collect()//收集显示结果
⑦ foldByKey
- 参数:(zeroValue: V)(func: (V, V) => V): RDD[(K, V)]
- 是 aggregateByKey的简化操作,seqop和combop相同
- 示例:创建一个pairRDD,计算相同key对应值的相加结果
kvRdd3.foldByKey(0)(_+_)
- 应用:使用foldByKey完成单词统计,即wordcount:
val lines = sc.textFile("/tmp/README.md")//读取文件 val wordCount = lines.flatMap(_.split(" ")).map((_,1)).foldByKey(0)(_+_) wordCount.collect()//收集显示结果
⑧ combineByKey
-
参数:(createCombiner: V => C, mergeValue: (C, V) => C, mergeCombiners: (C, C) => C)
-
参数****描述:
(1)createCombiner: combineByKey() 会遍历分区中的所有元素,因此每个元素的键要么还没有遇到过,要么就和之前的某个元素的键相同。如果这是一个新的元素,combineByKey()会使用一个叫作createCombiner()的函数来创建那个键对应的累加器的初始值(初始k对应v的计算规则)。
(2)mergeValue: 如果这是一个在处理当前分区之前已经遇到的键,它会使用mergeValue()方法将该键的累加器对应的当前值与这个新的值进行合并(分区内已处理k的v 与 相同k的新v 计算规则)
(3)mergeCombiners: 由于每个分区都是独立处理的, 因此对于同一个键可以有多个累加器。如果有两个或者更多的分区都有对应同一个键的累加器, 就需要使用用户提供的 mergeCombiners() 方法将各个分区的结果进行合并(不同分区之间相同k 的 v 计算规则)。 -
对相同K,把V合并成一个集合。用于将RDD[K,V]转换成RDD[K,C],这里的V类型和C类型可以相同也可以不同。
-
示例:创建一个pairRDD,计算每个key出现的次数以及可以对应值的总和
val kvRdd4 = sc.parallelize(Array(("a", 88), ("b", 95), ("a", 91), ("b", 93), ("a", 95), ("b", 98)),2) val combine = kvRdd4.combineByKey( (_,1),//初始累加器,处理没有遇到过的k 的v值 (acc:(Int,Int),v) => (acc._1+v, acc._2 + 1),//处理分区内之前遇到过的k的新v (acc1: (Int,Int), acc2:(Int,Int)) => (acc1._1 + acc2._1, acc1._2 + acc2._2) //处理不同分区之间相同k的v值 ) combine.collect()
-
应用:使用combineByKey完成单词统计,即wordcount:
val lines = sc.textFile("/tmp/README.md")//读取文件 val wordCount = lines.flatMap(_.split(" ")).map((_,1)).combineByKey( v=>v, (acc:Int,v:Int) => acc + v, (acc1:Int,acc2:Int)=>(acc1+acc2) ) wordCount.collect()//收集显示结果
⑨ sortByKey([ascending], [numTasks])
-
在一个(K,V)的RDD上调用,K必须实现Ordered接口,返回一个按照key进行排序的(K,V)的RDD
-
示例:创建一个pairRDD,按照key的正序和倒序进行排序
不能保证相同的key有序。
⑩ join(otherDataset, [numTasks])
- 在类型为(K,V)和(K,W)的RDD上调用,返回一个相同key对应的所有元素对在一起的(K,(V,W))的RDD
- 案例:创建两个pairRDD,并将key相同的数据聚合到一个元组。
pairRDD.join(pairRDD1).collect()
3.双Value类型的转换操作
双Value类型的转换操比较简单,常用来求并交差补集,不做详细讲解。常用算子如下:
- union(otherDataset) :计算两个RRD的并集,对源RDD和参数RDD求并集后返回一个新的RDD
- subtract (otherDataset) :计算两个RRD的差集,去除两个RDD中相同的元素,保留不同的RDD
- intersection(otherDataset):计算两个RRD的交集,对源RDD和参数RDD求交集后返回新的RDD
- cartesian(otherDataset) :计算两个RDD的笛卡尔积
- zip(otherDataset):将两个RDD组合成Key/Value形式的RDD,这里默认两个RDD的partition数量以及元素数量都相同,否则会抛出异常。
五、行动操作(action)
RDD行动操作用来触发具体的转换操作。行动操作或者将数据收集到驱动器,或者将其写到外部数源。
① reduce(func)
- 通过func函数 对RDD中的所有元素进行二元计算,先聚合分区内数据,再聚合分区间数据。
② count()
- 返回RDD中元素个数。
③ first()
- 返回RDD中的第一个元素
④ take(n)
- 返回RDD的前n个元组组成的数组。
⑤ takeOrdered(n)
- 返回RDD排序后的前n个元组组成的数组。
⑥ collect()
- 在驱动程序中,以数组的形式返回数据集的所有元素。
⑦ foreach(func)、foreachPartition(func)
- 在数据集的每一个元素上,运行函数func更新。
- foreach 遍历RDD,将函数f应用于每一个元素。要注意如果对RDD执行foreach,只会在Executor端有效,而并不是Driver端,比如:rdd.foreach(println),只会在Executor的stdout中打印出来,Driver端是看不到的。foreachPartition和foreach类似,只不过是对每一个分区使用f。
⑧ aggregate
- 参数:(zeroValue: U)(seqOp: (U, T) ⇒ U, combOp: (U, U) ⇒ U)
- 作用:aggregate函数将每个分区里面的元素通过seqOp和初始值进行聚合,然后用combine函数将每个分区的结果和初始值(zeroValue)进行combine操作。这个函数最终返回的类型不需要和RDD中元素类型一致。
- 案例:将rdd1所有的元素相加
rdd1.aggregate(0)(_+_, _+_)
- 注意:分区内的元素先和初始值做计算,然后初始值还要和每个分区间的结果做计算。
⑨ fold(num)(func)
- 折叠操作,aggregate的简化操作,seqop和combop一样。
- 案例:将rdd1所有的元素相加
rdd1.fold(0)(_+_)
⑩ countByKey()
- 针对(K,V)类型的RDD,返回一个(K,Int)的map,表示每一个key对应的元素个数。
- 示例:建一个PairRDD,统计每种key的个数。
val pairRDD = sc.parallelize(List((1,3),(1,2),(1,4),(2,3),(3,6),(3,8)),3) pairRDD.countByKey()
⑪ 存储行动操作
对于RDD,不能以常规的方式读取它并“保存”到数据源中。而是必须遍历分区才能将每个分区的内容保存到某个外部数据库,Spark会把RDD中每个分区都读取出来,并写到指定位置中。常见方法如下:
-
saveAsTextFile(path):将数据集的元素以textfile的形式保存到HDFS文件系统或者其他支持的文件系统,对于每个元素,Spark将会调用toString方法,将它装换为文件中的文本
-
saveAsSequenceFile(path) :将数据集中的元素以Hadoop sequencefile的格式保存到指定的目录下,可以使HDFS或者其他Hadoop支持的文件系统。
-
saveAsObjectFile(path):用于将RDD中的元素序列化成对象,存储到文件中。
六、Spark wordcount的5种写法
val lines = sc.textFile("/tmp/README.md")//读取文件
①使用groupByKey
val wordCount = lines.flatMap(_.split(" ")).map((_,1)).groupByKey().map(t =>(t._1,t._2.sum))//wordcount
②使用reduceByKey
val wordCount = lines.flatMap(_.split(" ")).map((_,1)).reduceByKey(_+_)
③使用aggregateByKey
val wordCount = lines.flatMap(_.split(" ")).map((_,1)).aggregateByKey(0)(_+_,_+_)
④使用foldByKey
val wordCount = lines.flatMap(_.split(" ")).map((_,1)).foldByKey(0)(_+_)
⑤使用combineByKey
val wordCount = lines.flatMap(_.split(" ")).map((_,1)).combineByKey(
v=>v,
(acc:Int,v:Int) => acc + v,
(acc1:Int,acc2:Int)=>(acc1+acc2)
)
wordCount.collect()//收集显示结果
七、分布式共享变量
除了弹性分布式数据集RDD外,Spark的第二种低级 API 是“分布式共享变量”。它包括两种类型:广播变量(broadcast variable)和累加器(accumulator)。具体地说,累加器将所有任务中的数据累加到一个共享结果中(例如,实现一个计数器,以便可以查看有多少输入记录无法解析)。广播变量允许你在所有工作节点上保存一个共享值,当在Spark各种操作中重用它时,就不需要将其重新在机器间传输。
1.广播变量
-
通过广播变量可以在集群上有效地共享(只读的)不变量,而不需要将其封装到函数中去。在驱动节点上使用变量的一般方法是简单地在函数闭包(function closure)中引用它(例如,在map操作中),但这种方式效率很低,尤其是对于大数据变量来说 (如数据库表或机器学习模型)。原因在于,当在闭包(closure)中使用变量时,必须在工作节点上执行多次反序列化 (每个任务一次)。此外,如果你在多个Spark操作和作业中使用相同的变量,它将被重复发送到工作节点上的每一个作业中,而不是只发送
一次。 -
广播变量用来高效分发较大的对象。向所有工作节点发送一个较大的只读值,以供一个或多个Spark操作使用。比如,如果你的应用需要向所有节点发送一个较大的只读查询表,甚至是机器学习算法中的一个很大的特征向量,广播变量用起来都很顺手。 在多个并行操作中使用同一个变量,但是 Spark会为每个任务分别发送。
-
使用广播变量:
val broadcastVar = sc.broadcast(Array(1, 2, 3)) broadcastVar.value
2.累加器
- Spark第二种类型的共享变量是累加器,它用于将转换操作更新的值以高效和容错的方式传输到驱动节点。累加器提供一个累加用的变量,Spark集群可以以按行方式对其进行安全更新,你可以用它来进行调试(例如,跟踪每个分区中某个变量的值) 或创建低级聚合。累加器仅支持由满足交换律和结合律的操作进行累加的变量,因此对累加器的操作可以被高效并行,你可以使用累加器实现计数器 (如 MapReduce) 或求和操作。Spark提供对数字类型累加器的原生支持,也支持添加对新类型的支持。