SparkRDD详解

SparkRDD

1.sparkshell

一般常用两个shell,一个是spark-shell用来写scala或者java语法。另一个是pyspark,用来写python。提交python或者jar包常用spark-submit。spark-sql顾名思义。

2.交互式分析

利用Spark shell 很容易学习Spark API,同时也Spark shell也是强大的交互式数据分析工具。Spark shell既支持Scala(Scala版本的shell在Java虚拟机中运行,所以在这个shell中可以引用现有的Java库),也支持Python。

3.RDD

Spark最主要的抽象概念是个分布式集合,也叫作弹性分布式数据集(Resilient Distributed Dataset – RDD)。(shell中有时候执行完scala操作会有一行显示 res应该就是指RDD,res是RDD中第一个R的缩写)RDD可以由Hadoop InputFormats读取HDFS文件创建得来,或者从其他RDD转换得到。

他是一个可分区的数据集,需要注意的是RDD本身不包含数据,只是记录数据的位置,充当一个映射的作用,当然,它也可以持久化或者缓存方便使用。

下面我们就先利用Spark源代码目录下的README文件来新建一个RDD,因为读取文件生成新的RDD也是产生了一个RDD,所以该textFile属于transformation算子:

scala> val textFile = sc.textFile("README.md")
textFile: spark.RDD[String] = spark.MappedRDD@2ee9b6e3

RDD有两种算子,action算子(actions)返回结果,transformation算子(transformations)返回一个新RDD。需要注意的是,transformation操作进入shell时并不执行,只有action算子开始运行后,所有的操作才开始。(换而言之,如果之前的tramsformation操作读取了错误的文件或者没正确执行,只有在action的时候才会报错)

我们先来看一下action算子:

scala> textFile.count() // Number of items in this RDD
res0: Long = 126
scala> textFile.first() // First item in this RDD
res1: String = # Apache Spark

transformation算子。我们利用filter``这个transformation算子返回一个只包含原始文件子集的新RDD。

scala> val linesWithSpark = textFile.filter(line => line.contains("Spark"))
linesWithSpark: spark.RDD[String] = spark.FilteredRDD@7dd4af09

连起来

scala> textFile.filter(line => line.contains("Spark")).count() // How many lines contain "Spark"?
res3: Long = 15

3.1.RDD partition

RDD一个分区对应一个task。一般每个cpu有2-4个分区。

分区和分片指的是同一个东西。

如果数据源来自hdfs,一个block会对应一个分区

弹性:不是指数据集可以动态拓展,是指血缘的容错机制

分布式:RDD是多节点存储的。类似于hdfs切割文件为block,RDD分为多个partition存放在不同节点。

每一个RDD是在不同的分区上并行执行的。

至于后续遇到shuffle的操作,RDD的partition可以根据Hash再次进行划分(一般pairRDD是使用key做Hash再取余来划分partition)

持久化:以hdfs为例,持久化时每个partition会存成一个文件,小于block的大小(128M)时,就会存成一个文件。如果大于,就会存为多个文件。

例子

假设,第一次保存RDD时10个partition,每个partition有140M。那么该RDD保存在hdfs上就会有20个block,下一批次重新读取hdfs上的这些数据,RDD的partition个数就会变为20个。再后续有类似union的操作,导致partition增加,但是程序有没有repartition或者进过shuffle的重新分区,这样就导致这部分数据的partition无限增加,这样一直下去肯定是会出问题的。所以,类似这样的情景,再程序开发结束一定要审查需不需要重新分区。

正确的做法是,140M*10/128M划分为整数分区。在存储

3.2共享变量

共享变量是指可以在并行操作中共享的变量。

广播变量(BroadCast):一份只读的数据,封装好后送给各个节点,这样就不用一个数据搬来搬去。

累加器(Accumulator):用来跨界点执行 累加操作比如计数和求和

3.3数据来源

并行化集合

已经存在集合,通过SparkContext.parallelize()生成一个新的RDD。集合中数据将复制到新的RDD中。

val data = Array(1, 2, 3, 4, 5)
val distData = sc.parallelize(data)
外部数据集

spark支持hadoop支持的任何数据源。

数据来源注意事项

如果是本地文件系统,那么所有的worker节点都得能用相同的路径访问到,或者挂载一个共享文件系统。

文件输入时都支持以下参数(目录 压缩文件 通配符)

textfile的可选参数是分区个数。默认spark会为每一个block创建一个分区。分区数必须大于等于block数

  • SparkContext.wholeTextFiles 可以读取一个包含很多小文本文件的目录,并且以 (filename, content) 键值对的形式返回结果。这与textFile 不同,textFile只返回文件的内容,每行作为一个元素。
  • 对于SequenceFiles,可以调用 SparkContext.sequenceFile[K, V],其中 K 和 V 分别是文件中key和value的类型。这些类型都应该是 Writable 接口的子类, 如:IntWritable and Text 等。另外,Spark 允许你为一些常用Writable指定原生类型,例如:sequenceFile[Int, String] 将自动读取 IntWritable 和 Text。
  • 对于其他的Hadoop InputFormat,你可以用 SparkContext.hadoopRDD 方法,并传入任意的JobConf 对象和 InputFormat,以及key class、value class。这和设置Hadoop job的输入源是同样的方法。你还可以使用 SparkContext.newAPIHadoopRDD,该方法接收一个基于新版Hadoop MapReduce API (org.apache.hadoop.mapreduce)的InputFormat作为参数。
  • RDD.saveAsObjectFile 和 SparkContext.objectFile 支持将RDD中元素以Java对象序列化方式不如Avro效率高,却为保存RDD提供了一种简便方式。

4.更多RDD算子

4.1wordcount实例

计算出文件中单词最多的行中有多少单词

scala> textFile.map(line => line.split(" ").size).reduce((a, b) => if (a > b) a else b)

首先,用一个map算子将每一行映射为一个整数,返回一个新RDD。然后,用reduce算子找出这个RDD中最大的单词数。map和reduce算组的参数都是scala 函数体(闭包),且函数体内可以使用任意的语言特性,或引用scala/java库。例如,我们可以调用其他函数。为了好理解,下面我们用Math.max作为例子:

scala> import java.lang.Math
import java.lang.Math

scala> textFile.map(line => line.split(" ").size).reduce((a, b) => Math.max(a, b))
res5: Int = 15

Hadoop上的MapReduce是大家耳熟能详的一种通用数据流模式。而Spark能够轻松地实现MapReduce流程:

scala> val wordCounts = textFile.flatMap(line => line.split(" ")).map(word => (word, 1)).reduceByKey((a, b) => a + b)
wordCounts: spark.RDD[(String, Int)] = spark.ShuffledAggregatedRDD@71f027b8

这个例子里,我使用了flatMap, map, and reduceByKey 这几个transformation算子,把每个单词及其在文件中出现的次数转成一个包含(String,int)键值对的RDD,计算出每个单词在文件中出现的次数:

scala> wordCounts.collect()
res6: Array[(String, Int)] = Array((means,1), (under,2), (this,3), (Because,1), (Python,2), (agree,1), (cluster.,1), ...)

4.2RDD算子介绍

分类

从功能上划分为AT两类算子

从存储数据上来说分为三类

基础类型的普通算子,例如String key-value的数据处理bykey算子 针对数字类型处理的计算算子

特性

Transformation操作是惰性的,只会记录操作动作。当有结果要返回给Driver时才执行操作。通过DAGScheduler和TaskScheduler分发到集群中运行。这个特性叫惰性求值

之所以这么设计是因为,在action之前,就可以优化DAG或者各种提前的优化,最后在高效的执行。

每一个action运行的时候,与其关联的所有TransformationRDD都会重新计算。也可以用presist方法将数据持久化刀到内存或硬盘中,为了下次更快访问,可以把数据存储到集群中。

lineLengths.persist() 可以缓存在内存中
Action

执行各个stage的任务,结果返回给Driver

reduce( (T, T) ⇒ U )

规约所有的值

val rdd = sc.parallelize(Seq(("手机", 10.0), ("手机", 15.0), ("电脑", 20.0)))
val result = rdd.reduce((curr, agg) => ("总价", curr._2 + agg._2))
println(result)
sc.parallelize(Seq(("a", 1), ("a", 1), ("b", 1)))  .reduceByKey( (curr, agg) => curr + agg )  .collect() 

输出(总价,45.0)

reduce后的lambda表达式意思是生成一个新RDD,其中第一个值是常量,第二个值是所有元素第二个值的累加。

  • reduce 和 reduceByKey 是完全不同的, reduce 是一个 action, 并不是 Shuffled 操作
  • 本质上 reduce 就是现在每个 partition 上求值, 最终把每个 partition 的结果再汇总

collect()

rdd1.collect()

用数组的形式返回数据集中所有元素

count

返回元素个数

countByKey()

得出整个数据集中key出现的次数,常用来解决数据倾斜,用来查看倾斜的key

val rdd = sc.parallelize(Seq(("手机", 10.0), ("手机", 15.0), ("电脑", 20.0)))
val result = rdd.countByKey()
println(result)
  • 返回结果为 Map(key → count)
  • 常在解决数据倾斜问题时使用, 查看倾斜的 Key

first

返回第一个元素

take(N)

返回前N个元素

foreach( T ⇒ … )

遍历每一个元素

takeSample(withReplacement, fract)

类似于sample,但是这个是action直接返回结果

fold(zeroValue)( (T, T) ⇒ U )

指定初始值和聚合函数,折叠聚合整个数据集

saveAsTextFile(path)

保存文件到path路径

saveAsSequenceFile(path)

结果存入sequence文件

Transformation

创建新的RDD,可以基于旧的RDD或者文件创建。

Map

把RDD中一对一数据转换为另一种形式

sc.parallelize(Seq(1, 2, 3))
.map( num => num * 10 )
.collect()

输出是10,20,30。这是num是指原来的本体,操作就是每个旧numX10得到新num

map(word => (word, 1)同理,原来是是一个单词,比如xcw,现在变成xcw,1(需要注意的是这里得到的应该是新的RDD)

flatMap

和Map类似,但是是一对多。

sc.parallelize(Seq("Hello lily", "Hello lucy", "Hello tim"))
.flatMap( line => line.split(" ") )
.collect()

输出是hello lily hello lucy hello tim 通过split分开

filter

过滤信息

sc.parallelize(Seq(1, 2, 3))
.filter( value => value >= 3 )
.collect()

输出是3

mapPartitions(List[T] ⇒ List[U])

RDD[T] ⇒ RDD[U] 和 map 类似, 但是针对整个分区的数据转换

mapPartitionsWithIndex

和 mapPartitions 类似, 只是在函数中增加了分区的 Index

mapvalues

和map类似,但是只对value操作

sc.parallelize(Seq(("a", 1), ("b", 2), ("c", 3)))
.mapValues( value => value * 10 )
.collect()

输出是 a,10 b,20 c,30

sample

从当前数据集中抽取一部分数据出来,这个算子常用来缩小数据集规模。

sc.parallelize(Seq(1, 2, 3, 4, 5, 6, 7, 8, 9, 10))
.sample(withReplacement = true, 0.6, 2)
.collect()

第一个参数是指被抽取的数是否放回原数组继续抽取,如果是true。生成的数据集可能是有数据重复。

第二个参数是抽取比例,此处为60%

第三个是第一次随机抽取的下标。一般使用默认值。

输出是1 3 3 5 5 10.一共六个数。

union

把两列数据拼起来

val rdd1 = sc.parallelize(Seq(1, 2, 3))
val rdd2 = sc.parallelize(Seq(4, 5, 6))
rdd1.union(rdd2)
.collect()

输出是12 3 4 5 6

join

不同RDD的相同key链接起来。

val rdd1 = sc.parallelize(Seq(("a", 1), ("a", 2), ("b", 1)))
val rdd2 = sc.parallelize(Seq(("a", 10), ("a", 11), ("a", 12)))
rdd1.join(rdd2).collect()

输出是 Array[(String, (Int, Int))] = Array((a,(1,10)), (a,(1,11)), (a,(1,12)), (a,(2,10)), (a,(2,11)), (a,(2,12)))

join(other, numPartitions) 第一个参数是要join的rdd,partitioner or numPartitions 可选, 可以通过传递分区函数或者分区数量来改变分区。

join的结果是笛卡尔积。相当于rdd1的a key 会去匹配rdd2 的所有a key

intersection

用于求左侧和右侧集合都有的数据,就是交集。

val rdd1 = sc.parallelize(Seq(1, 2, 3, 4, 5))
val rdd2 = sc.parallelize(Seq(4, 5, 6, 7, 8))
rdd1.intersection(rdd2)
.collect()

输出是 4,5

subtract(other, numPartitions)

差集,可以设置分区数。

Distinct

原数组重复数据去重

sc.parallelize(Seq(1, 1, 2, 2, 3))
.distinct()
.collect()

输出是1,2,3

reduceByKey

先针对数据生成tuple(比如相同key的放一起)。然后对每个组执行reduce算子

sc.parallelize(Seq(("a", 1), ("a", 1), ("b", 1)))
.reduceByKey( (curr, agg) => curr + agg )
.collect()

输出是a 2 b 1。因为俩a,1是一个tuple。然后两者相加,a,2

ReduceByKey 只能作用于 Key-Value 型数据, Key-Value 型数据在当前语境中特指 Tuple2

ReduceByKey 是一个需要 Shuffled 的操作

和其它的 Shuffled 相比, ReduceByKey是高效的, 因为类似 MapReduce 的, 在 Map 端有一个 Cominer, 这样 I/O 的数据便会减少

groupByKey

根据key生成新的集合

sc.parallelize(Seq(("a", 1), ("a", 1), ("b", 1)))
.groupByKey()
.collect()

输出是两个集合,一个a,1,1 一个b,1

如果加一个a,2。则在输出a的集合里加个2

GroupByKey 算子的主要作用是按照 Key 分组, 和 ReduceByKey 有点类似, 但是 GroupByKey 并不求聚合, 只是列举 Key 对应的所有 Value

注意点

GroupByKey 是一个 Shuffled

GroupByKey 和 ReduceByKey 不同, 因为需要列举 Key 对应的所有数据, 所以无法在 Map 端做 Combine, 所以 GroupByKey 的性能并没有 ReduceByKey 好

combineByKey

把输入里的数据根据key,合并

var rdd = sc.makeRDD(Array(("A",2),("A",1),("A",3),("B",1),("B",2),("C",1)))
val collect: Array[(String, String)] = rdd.combineByKey(
      (v: Int) => v + "_",
      (c: String, v: Int) => c + "@" + v,//同一分区内
      (c1: String, c2: String) => c1 + "$" + c2
    ).collect

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rmUtEmBk-1669020639306)(SparkRDD.assets/image-20221107173521337.png)]

输出是

(B,1_$2_), (C,1_), (A,2_@1$3_)

首先需要知道,该lambda所有操作都是针对value,也就是参数列表里没有对key的操作。

第一个参数是初始化参数,根据分区(设置分区数为3,RDD.getpartitions可以看分区数,也就是共三个分区,每个分区两个元素),每个分区的不同key都会初始化一次。(比如某个分区有AAB三个key,则初始化第一个A和最后一个B,需要注意的是每个分区都不冲突,比如A在一分区初始化了,不影响A这个Key在第二个分区初始化)

第二个参数是分区间的操作,左边参数是相同key的元素,右边是其需要执行的函数。这里string和int是指相同key的value值,

不一样是因为第一步初始化时,每个分区的key都初始化一次。则每个分区的每个key至少有一个初始化后加了_,所以是string类型,int类型则是没初始化的元素。

以一个分区为例,这个分区所有key都初始化过了,则rdd算子干的事就是从上往下遍历。遇到一个初始化过的key就记住位置,下面有相同key时进行函数操作。可以理解为对第一个初始化过的key不停的累加后续key的值。

第三个参数是分区间的合并,所有分区内执行完第二部操作后还要继续合并。这里的输入参数都是string是因为上一步初始化后的key是string,他们累加其他key后也是string。所以这一步的输入值都是string。

aggregateByKey()

按照key聚合所有的value(底层调用的是combinebykey)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HpzrAQeJ-1669020639307)(SparkRDD.assets/image-20221107173546062.png)]

val rdd = sc.parallelize(Seq(("手机", 10.0), ("手机", 15.0), ("电脑", 20.0)))
val result = rdd.aggregateByKey(0.8)(
seqOp = (zero, price) => price * zero,
combOp = (curr, agg) => curr + agg
).collect()
println(result)

调用

rdd.aggregateByKey(zeroValue)(seqOp, combOp)

参数

zeroValue 初始值(后续可能用到的变量)

seqOp 转换每一个值的函数(第一个参数是初始值)

comboOp 将转换过的值聚合的函数

注意点 为什么需要两个函数? aggregateByKey 运行将一个 RDD[(K, V)] 聚合为 RDD[(K, U)], 如果要做到这件事的话, 就需要先对数据做一次转换, 将每条数据从 V 转为 U, seqOp 就是干这件事的.当 seqOp 的事情结束以后, comboOp 把其结果聚合

和 reduceByKey 的区别::

aggregateByKey 最终聚合结果的类型和传入的初始值类型保持一致(因为初始值的乘积操作会使数据类型改变)

reduceByKey 在集合中选取第一个值作为初始值, 并且聚合过的数据类型不能改变

foldByKey

和reduceBykey类似。根据相同key聚合value。但是这个可以带一个初始值。

sc.parallelize(Seq(("a", 1), ("a", 1), ("b", 1)))
.foldByKey(zeroValue = 10)( (curr, agg) => curr + agg )
.collect()
  • FoldByKey 是 AggregateByKey 的简化版本, seqOp 和 combOp 是同一个函数
  • FoldByKey 指定的初始值作用于每一个 Value
  • 根据上述一二可知,curr+age的操作不仅是同key间的聚合操作,还是每个key和初始值间的操作。

cogroup(other, numPartitions)

将多个RDD中相同的key分组

val rdd1 = sc.parallelize(Seq(("a", 1), ("a", 2), ("a", 5), ("b", 2), ("b", 6), ("c", 3), ("d", 2)))
val rdd2 = sc.parallelize(Seq(("a", 10), ("b", 1), ("d", 3)))
val rdd3 = sc.parallelize(Seq(("b", 10), ("a", 1)))
val result1 = rdd1.cogroup(rdd2).collect()
val result2 = rdd1.cogroup(rdd2, rdd3).collect()
/*
执行结果:
Array(
(d,(CompactBuffer(2),CompactBuffer(3))),
(a,(CompactBuffer(1, 2, 5),CompactBuffer(10))),
(b,(CompactBuffer(2, 6),CompactBuffer(1))),
(c,(CompactBuffer(3),CompactBuffer()))
)
*/
println(result1)
/*
执行结果:
Array(
(d,(CompactBuffer(2),CompactBuffer(3),CompactBuffer())),
(a,(CompactBuffer(1, 2, 5),CompactBuffer(10),CompactBuffer(1))),
(b,(CompactBuffer(2, 6),CompactBuffer(1),Co...
*/
println(result2)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5IiSZyCB-1669020639307)(SparkRDD.assets/image-20221108101101354.png)]

作用

多个 RDD 协同分组, 将多个 RDD 中 Key 相同的 Value 分组

调用

cogroup(rdd1, rdd2, rdd3, [partitioner or numPartitions])

参数

rdd… 最多可以传三个 RDD 进去, 加上调用者, 可以为四个 RDD 协同分组

partitioner or numPartitions 可选, 可以通过传递分区函数或者分区数来改变分区

注意点

对 RDD1, RDD2, RDD3 进行 cogroup, 结果中就一定会有三个 List, 如果没有 Value 则是空 List, 这一点类似于 SQL 的全连接, 返回所有结果, 即使没有关联上

CoGroup 是一个需要 Shuffled 的操作

sortBy(ascending, numPartitions)

排序算法

val rdd1 = sc.parallelize(Seq(("a", 3), ("b", 2), ("c", 1)))
val sortByResult = rdd1.sortBy( item => item._2 ).collect()
val sortByKeyResult = rdd1.sortByKey().collect()
println(sortByResult)     Array((c,1), (b,2), (a,3))
println(sortByKeyResult)  Array((a,3), (b,2), (c,1))

作用

排序相关相关的算子有两个, 一个是 sortBy,其根据value排序(此处指定了排序第二个元素,也就是value)

另外一个是 sortByKey,根据key排序,默认升序

调用

sortBy(func, ascending, numPartitions)

参数

func 通过这个函数返回要排序的字段

ascending 是否升序

numPartitions 分区数

注意点

普通的 RDD 没有 sortByKey, 只有 Key-Value 的 RDD 才有

sortBy 可以指定按照哪个字段来排序, sortByKey 直接按照 Key 来排序

partitionBy(partitioner)

使用用传入的 partitioner 重新分区, 如果和当前分区函数相同, 则忽略操作

coalesce(numPartitions)

减少分区数

val rdd = sc.parallelize(Seq(("a", 3), ("b", 2), ("c", 1)))
val oldNum = rdd.partitions.length
val coalesceRdd = rdd.coalesce(4, shuffle = true)
val coalesceNum = coalesceRdd.partitions.length
val repartitionRdd = rdd.repartition(4)
val repartitionNum = repartitionRdd.partitions.length
print(oldNum, coalesceNum, repartitionNum)

作用

一般涉及到分区操作的算子常见的有两个, repartitioin 和 coalesce, 两个算子都可以调大或者调小分区数量

调用

repartitioin(numPartitions)

coalesce(numPartitions, shuffle)

参数

numPartitions 新的分区数

shuffle 是否 shuffle, 如果新的分区数量比原分区数大, 必须 Shuffled, 否则重分区无效

注意点

repartition 和 coalesce 的不同就在于 coalesce 可以控制是否 Shuffle

repartition 是一个 Shuffled 操作

repartitionAndSortWithinPartitions

重新分区的同时升序排序, 在 partitioner 中排序, 比先重分区再排序要效率高, 建议使用在需要分区后再排序的场景使用

4.3总结

RDD算子会生成专用算子

map,flatmap,filter会生成MapPartitionsRDD

coalesce,repartition会生成VoalescedRDD

常见的 RDD 有两种类型

转换型的 RDD, Transformation

动作型的 RDD, Action

常见的 Transformation 类型的 RDD

map

flatMap

filter

groupBy

reduceByKey

常见的 Action 类型的 RDD

collect

countByKey

reduce

5.缓存

Spark同样支持把数据集拉到集群范围的内存缓存中。这对于需要重复访问的数据非常有用,比如:查询一些小而”热“(频繁访问)的数据集 或者 运行一些迭代算法(如 PageRank)。

xxx.cache()即可
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值