Spark Core
文章目录
- Spark Core
- 一.RDD详解
- (一)为什么要有RDD?
- (二)什么是RDD
- (三)RDD的五大特性
- (四)RDD的算子分类
- (五)RDD的算子操作
- 1.WordCount
- 2.创建RDD
- 3.查看RDD的分区数量
- 4.map
- 5.filter
- 6.flatMap
- 7.sortBy
- 8.交集|并集|差集|笛卡尔积
- 9.join
- 10.groupByKey
- 11.cogroup(了解)
- 12.groupBy
- 13.reduce
- 14.reduceByKey
- 15.repartition
- 16.collect
- 17.count
- 18.distinct
- 19.top|take|first
- 20.keys|values|mapValues
- 21.collectAsMap
- 22.foreach|foreachPartition
- 23.mapPartitiions
- 24.mapPartitionsWithIndex
- 25.aggregate
- 26.aggregateByKey
- 27.combineByKey
- 总结:
一.RDD详解
(一)为什么要有RDD?
在许多迭代式算法(比如机器学习、图算法等)和交互式数据挖掘中,不同计算阶段之间会重用中间结果,即一个阶段的输出结果会作为下一个阶段的输入。但是,之前的MapReduce框架采用非循环式的数据流模型,把中间结果写入到HDFS中,带来了大量的数据复制、磁盘IO和序列化开销。且这些框架只能支持一些特定的计算模式(map/reduce),并没有提供一种通用的数据抽象。
AMP实验室发表的一篇关于RDD的论文:《Resilient Distributed Datasets: A Fault-Tolerant Abstraction for In-Memory Cluster Computing》就是为了解决这些问题的,RDD提供了一个抽象的数据模型,让我们不必担心底层数据的分布式特性,只需将具体的应用逻辑表达为一系列转换操作(函数),不同RDD之间的转换操作之间还可以形成依赖关系,进而实现管道化,从而避免了中间结果的存储,大大降低了数据复制、磁盘IO和序列化开销,比且还提供了更多的API(map/reduec/reducebykey/fileter…)。
总结: RDD做了什么?
- 中间结果保存在内存中,并重用;
- 提供了通用的抽象的分布式的数据模型;
- 提供了多种数据操作模式(支持函数式编程;
总之相比与传统的MapReduce,RDD更快速、更通用、更灵活。
(二)什么是RDD
RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,是Spark中最基本的数据抽象,代表一个不可变、可分区、里面的元素可并行计算的集合。
单词拆解:
- Dataset: 它是一个集合,可以存放很多元素。
- Distributed :它里面的元素是分布式存储的,可以用于分布式计算。
- Resilient :它是弹性的,RDD里面的中的数据可以保存在内存中或者磁盘里面。
存储的弹性:内存与磁盘的自动切换、存储灵活;
容错的弹性:数据丢失可以自动恢复;
计算的弹性:计算出错重试机制、计算的模式的灵活性;
分片的弹性:根据需要重新分片。
总结
- RDD具有数据流模型的特点:自动容错、位置感知性调度和可伸缩。
- RDD将Spark的底层的细节都隐藏起来了(任务调度、Task执行,任务失败重试等待)。
- 开发者使用起来可以像操作本地集合一样, 以函数式编程的方式对RDD进行各种并行计算。
(三)RDD的五大特性
来自package org.apache.spark.rdd
源码中的注释内容
/**
* 什么是RDD?
* A Resilient Distributed Dataset (RDD), the basic abstraction in Spark. Represents an immutable,
* partitioned collection of elements that can be operated on in parallel. This class contains the
* basic operations available on all RDDs, such as `map`, `filter`, and `persist`. In addition,
* [[org.apache.spark.rdd.PairRDDFunctions]] contains operations available only on RDDs of key-value
* pairs, such as `groupByKey` and `join`;
* [[org.apache.spark.rdd.DoubleRDDFunctions]] contains operations available only on RDDs of
* Doubles; and
* [[org.apache.spark.rdd.SequenceFileRDDFunctions]] contains operations available on RDDs that
* can be saved as SequenceFiles.
* All operations are automatically available on any RDD of the right type (e.g. RDD[(Int, Int)]
* through implicit.
*
* RDD的五大特性:
* Internally, each RDD is characterized by five main properties:
* - A list of partitions
* - A function for computing each split
* - A list of dependencies on other RDDs
* - Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned)
* - Optionally, a list of preferred locations to compute each split on (e.g. block locations for
* an HDFS file)
*
* All of the scheduling and execution in Spark is done based on these methods, allowing each RDD
* to implement its own way of computing itself. Indeed, users can implement custom RDDs (e.g. for
* reading data from a new storage system) by overriding these functions. Please refer to the
* <a href="http://people.csail.mit.edu/matei/papers/2012/nsdi_spark.pdf">Spark paper</a>
* for more details on RDD internals.
*/
-
分区列表
A list of partitions
对于RDD来说,每个分片都会被一个计算任务处理,分片数决定并行度。
用户可以在创建RDD时指定RDD的分片个数,如果没有指定,那么就会采用默认值。 -
作用于每个分片的计算函数
A function for computing each split
Spark中RDD的计算是以分片为单位的,计算函数会被作用到每个分片上。
-
对父RDD的依赖列表
A list of dependencies on other RDDs
RDD的每次转换都会生成一个新的RDD,所以RDD之间就会形成类似于流水线一样的前后依赖关系。在部分分区数据丢失时,Spark可以通过这个依赖关系重新计算丢失的分区数据,而不是对RDD的所有分区进行重新计算。(Spark的容错机制)
-
对
Key-Value对
数据类型的RDD分区器(默认使用Hash), 控制分区策略和分区数。Optionally, a Partitioner for key-value RDDs
可选项,对于KV类型的RDD会有一个Partitioner,即RDD的分区函数。当前Spark中实现了两种类型的分片函数,一个是基于哈希的HashPartitioner,另外一个是基于范围的RangePartitioner。只有对于key-value的RDD,才会有Partitioner,非key-value的RDD的Parititioner的值是None。Partitioner函数不但决定了RDD本身的分片数量,也决定了parent RDD Shuffle输出时的分片数量。
-
每个数据分区的地址列表(如HDFS上的block块的地址)
Optionally, a list of preferred locations to compute each split on
对于一个HDFS文件来说,这个列表保存的就是每个Partition所在的块的位置。按照"移动数据不如移动计算"的理念,Spark在进行任务调度的时候,会尽可能选择那些存有数据的worker节点来进行任务计算。
(四)RDD的算子分类
RDD的算子分为两类:
-
Transformation转换操作, 返回一个新的RDD; Transformation操作是延时计算的, 也就是说, 从一个RDD转换生成另一个RDD的转换操作不是马上执行, 需要等到有Action操作时才真正触发运算。
-
Action动作操作, 返回值不是RDD(无返回值或返回其他的), Action操作会触发Spark提交作业(job), 并将数据输出到Spark系统。
总结: RDD不实际存储真正要计算的数据, 只是记录了RDD的转换关系(调用了什么方法,传入什么函数,依赖哪些RDD,分区器是什么,数量块来源机器列表), RDD中的所有转换操作都是延迟执行(懒执行)的,也就是说并不会直接计算, 当要求返回结果给Driver的Action动作时,这些转换才会真正运行 。
(五)RDD的算子操作
1.WordCount
val res = sc.textFile("hdfs://node01:8020/spark/day01/words.txt").flatMap(x=>x.split(" ")).map((_,1)).reduceByKey(_+_)
res.collect
2.创建RDD
// 1.由外部存储系统的数据集创建
val rdd1 = sc.textFile("hdfs://node01:8020/spark/day01/words.txt")
// 2.通过已有的RDD经过算子转换生成新的RDD
val rdd2 = rdd1.flatMap(_.split(" "))
// 3.由一个已经存在的Scala数组|集合创建
val rdd3 = sc.parallelize(Array(1,2,3,4,5,6,7,8),3)//指定分区数量为3
// 或者 (makeRDD方法底层调用了parallelize方法)
val rdd4 = sc.makeRDD(List(1,2,3,4,5,6))
3.查看RDD的分区数量
//默认分区数为2
rdd4.partitions.length
4.map
//对RDD的每一个元素进行操作
val rdd1 = sc.parallelize(List(1,3,5,7,9))
rdd1.map(_*2).collect
5.filter
//对RDD当中的每一个元素进行条件判断, 返回true的留下
val rdd2 = sc.parallelize(List(1,2,3,6,43,31,67,21))
rdd2.filter(_>10).collect
6.flatMap
//对RDD中的每个元素先进行map再压扁
val rdd1 = sc.parallelize(Array("a b c", "d e f", "h i j"))
rdd1.flatMap(_.split(" ")).collect
7.sortBy
sortBy方法的参数列表不能用_代替函数
val rdd1 = sc.parallelize(List(5, 6, 4, 7, 3, 8, 2, 9, 1, 10))
rdd1.sortBy(x=>x,false).collect//false为降序, true或默认为升序
rdd1.sortBy(x=>x+"",true).collect//变成字符串按字典序
8.交集|并集|差集|笛卡尔积
val rdd1 = sc.parallelize(List(5, 6, 4, 3))
val rdd2 = sc.parallelize(List(1, 2, 3, 4))
//求交集
rdd1.intersection(rdd2).collect
//求并集
val rdd3 = rdd1.union(rdd2)
rdd3.collect//不会去重, 结果为:Array(5, 6, 4, 3, 1, 2, 3, 4)
//去重
rdd3.distinct.collect
//求差集
rdd1.subtract(rdd2).collect//Array(6, 5)
//求笛卡尔积
val rdd1 = sc.parallelize(List("jack", "tom"))//学生
val rdd2 = sc.parallelize(List("java", "python", "scala"))//课程
val rdd3 = rdd1.cartesian(rdd2).collect//结果就是所有学生可能的选课情况
9.join
//相同的key合并,value合成元祖
val rdd1 = sc.parallelize(List(("tom", 1), ("jerry", 2), ("kitty", 3)))
val rdd2 = sc.parallelize(List(("jerry", 9), ("tom", 8), ("shuke", 7), ("tom", 2)))
//内连接
rdd1.join(rdd2).collect
//Array((tom,(1,2)), (tom,(1,8)), (jerry,(2,9)))
//左外连接
rdd1.leftOuterJoin(rdd2).collect
//Array((tom,(1,Some(2))), (tom,(1,Some(8))), (jerry,(2,Some(9))), (kitty,(3,None)))
//右外连接
rdd1.rightOuterJoin(rdd2).collect
// Array((tom,(Some(1),8)), (tom,(Some(1),2)), (jerry,(Some(2),9)), (shuke,(None,7)))
10.groupByKey
//对具有相同键的值进行分组
val rdd6 = sc.parallelize(Array(("tom",1), ("jerry",2), ("kitty",3), ("jerry",9), ("tom",8), ("shuke",7), ("tom",2)))
rdd6.groupByKey.collect
//Array((tom,CompactBuffer(8, 2, 1)), (jerry,CompactBuffer(9, 2)), (shuke,CompactBuffer(7)), (kitty,CompactBuffer(3)))
11.cogroup(了解)
//先在RDD内部按照key分组,再在多个RDD间按照key分组
val rdd1 = sc.parallelize(List(("tom", 1), ("tom", 2), ("jerry", 3), ("kitty", 2)))
val rdd2 = sc.parallelize(List(("jerry", 2), ("tom", 1), ("shuke", 2)))
rdd1.cogroup(rdd2).collect
//Array((tom,(CompactBuffer(1, 2),CompactBuffer(1))), (jerry,(CompactBuffer(3),CompactBuffer(2))), (shuke,(CompactBuffer(),CompactBuffer(2))), (kitty,(CompactBuffer(2),CompactBuffer())))
12.groupBy
//根据指定函数中的规则对key进行分组
val rdd1= sc.parallelize(List(1,2,3,4,5,6))
rdd1.groupBy(x => {if(x%2 == 0) "even" else "odd"}).collect
//Array((even,CompactBuffer(4, 6, 2)), (odd,CompactBuffer(1, 3, 5)))
13.reduce
- reduce是action算子
//第一_ 上次一个运算的结果,第二个_ 这一次进来的元素
val rdd1 = sc.parallelize(List(1,2,3,4,5))
rdd1.reduce(_+_)//Int = 15
14.reduceByKey
//使用传入的func函数对具有相同键的值进行操作
val list: List[(String, Int)] = List(("tom",1), ("jerry",3), ("kitty",2), ("shuke",1), ("jerry",2), ("tom",3), ("shuke",2), ("kitty",5))
val rdd1 = sc.parallelize(list)
rdd1.reduceByKey(_+_).collect
//Array((tom,4), (jerry,5), (shuke,3), (kitty,7))
15.repartition
//改变分区数, 本质上是生成了一个新的RDD
val rdd1 = sc.parallelize(1 to 10,3)
//减少分区
rdd1.repartition(2).partitions.length//新生成的RDD的分区数为2
//增加分区
rdd1.repartition(4).partitions.length
//应用场景: 合并小文件, 在把处理结果保存到hdfs上之前可以减少分区数
sc.textFile("hdfs://node01:8020/spark/day01/words.txt").flatMap(_.split(" ")).map((_,1)).reduceByKey(_+_).repartition(1).saveAsTextFile("hdfs://node01:8020/spark/output8/")
16.collect
//Action操作,用于提取所有数据
val rdd1 = sc.parallelize(List(6,1,2,3,4,5), 2)
rdd1.collect
17.count
//求RDD中最外层元素的个数
val rdd3 = sc.parallelize(List(List("a b c", "a b b"),List("e f g", "a f g"), List("h i j", "a a b")))
rdd3.count//Long = 3
18.distinct
//去重
val rdd = sc.parallelize(Array(1,2,3,4,5,5,6,7,8,1,2,3,4), 3)
rdd.distinct.collect
19.top|take|first
- top
//取出最大的前N个, Action操作
val rdd1 = sc.parallelize(List(3,6,1,2,4,5))
rdd1.top(3)//Array(6, 5, 4)
- take
//从索引为0的元素开始,按顺序取n个, Action操作
rdd1.take(2)
rdd1.sortBy(x => x, false).take(2)
- first
//按照原来的顺序取前第一个, Action操作
rdd1.first
20.keys|values|mapValues
val rdd1 = sc.parallelize(List("dog", "tiger", "lion", "cat", "panther", "eagle"), 2)
val rdd2 = rdd1.map(x => (x.length, x))
rdd2.collect//Array((3,dog), (5,tiger), (4,lion), (3,cat), (7,panther), (5,eagle))
- keys: 返回所有键值对的key
rdd2.keys.collect//Array(3, 5, 4, 3, 7, 5)
- values: 返回所有键值对的value
rdd2.values.collect//Array(dog, tiger, lion, cat, panther, eagle)
- mapValues: 对键值对每个value都应用一个函数,key不会发生变化
val rdd1 = sc.parallelize(List((1,10),(2,20),(3,30)))
rdd1.mapValues(_*2).collect//Array((1,20), (2,40), (3,60))
21.collectAsMap
//Action操作
val rdd = sc.parallelize(List(("a", 1), ("a",3), ("b", 2)))
rdd.collectAsMap//Map(b -> 2, a -> 3) 对于重复的key, 后面的元素覆盖前面的元素
22.foreach|foreachPartition
- foreach: 把传入的函数给各个分区, 在分区内循环遍历该分区中的元素
- foreachPartition: 传入的函数作用于每个分区
- 注意: Action方法,任务执行的时候是在executor端执行的,所以它会将结果打印到executor端。
//在spark-shell中执行, 去web页面node01:8088查找
val rdd = sc.parallelize(List(1, 2, 3, 4, 5, 6, 7, 8, 9), 3)
rdd.foreach(x => println(x*100))//x每个元素,即一个一个的数字
rdd.foreachPartition(x => println(x.reduce(_ + _)))//x是每个分区
以下是在IDEA中操作:
def main(args: Array[String]): Unit = {
val config: SparkConf = new SparkConf().setMaster("local[*]").setAppName("WordCount")
val sc = new SparkContext(config)
//foreach: 将函数f应用于此RDD的所有元素
val rdd1 = sc.parallelize(List(1, 2, 3, 4, 5, 6, 7, 8, 9), 3)
rdd1.foreach(x => print(x*100 + " "))//x每个元素,即一个一个的数字
//因为是并行计算, 所以结果无序: 400 100 500 700 600 200 800 300 900
//将函数f应用于此RDD的每个分区
rdd1.foreachPartition(x => println(x.reduce(_ + _)))//x是每个分区
//结果分别是 6 15 24
}
(了解)应用场景: 在函数中要将RDD中的元素保存到数据库
foreach:会将函数作用到RDD中的每一条数据,那么有多少条数据, 操作数据库连接的开启关闭就得执行多少次
foreachPartition: 将函数作用到每一个分区, 那么每一个分区执行一次数据库连接的开启关闭, 有几个分区就会执行数据库连接开启关闭
23.mapPartitiions
- 将每一个分区传递给函数, Transformation操作
val rdd1 = sc.parallelize(List(1, 2, 3, 4, 5, 6, 7, 8, 9), 3)
rdd1.mapPartitions(x=>x.map(y=>y*2)).collect//Array(2, 4, 6, 8, 10, 12, 14, 16, 18)
24.mapPartitionsWithIndex
**功能:**取分区中对应的数据时,还可以将分区的编号取出来,这样就可以知道数据是属于哪个分区的(哪个区分对应的Task的数据)
val rdd1 = sc.parallelize(List(1,2,3,4,5,6,7,8,9), 2)
//该函数的功能是将对应分区中的数据取出来,并且带上分区编号
val func = (index: Int, iter: Iterator[Int]) => {
iter.map(x => "[partID:" + index + ", val: " + x + "]")
}
rdd1.mapPartitionsWithIndex.collect
/* 结果:
Array([partID:0, val: 1], [partID:0, val: 2], [partID:0, val: 3], [partID:0, val: 4], [partID:1, val: 5], [partID:1, val: 6], [partID:1, val: 7], [partID:1, val: 8], [partID:1, val: 9]) */
25.aggregate
对每个分区进行聚合
//0表示初始值,每个分区会有初始值,汇总的时候也有初始值
val rdd1 = sc.parallelize(List(1,2,3,4,5,6,7,8,9), 2)
rdd1.aggregate(0)(_+_,_+_) //45
rdd1.aggregate(10)(_+_,_+_) //75
26.aggregateByKey
相同的Key合并, 对value值进行聚合操作,聚合前设了一个初始值
传入的第一函数给各分区内部执行, 再用传入的第二个函数对分区后的结果进行第二次操作
val pairRDD = sc.parallelize(List( ("cat",2), ("cat", 5), ("mouse", 4),("cat", 12), ("dog", 12), ("mouse", 2)), 2)
pairRDD.aggregateByKey(0)(math.max(_, _), _ + _).collect//Array((dog,12), (cat,17), (mouse,6))
27.combineByKey
val rdd1 = sc.textFile("hdfs://node01:8020/spark/day01/words.txt").flatMap(_.split(" ")).map((_, 1))//Array((hello,1), (me,1), (hello,1), (you,1), (hello,1), (her,1))
val rdd2 = rdd1.combineByKey(x => x, //作用于每个value, 可进行类型转换, 结果作为下个函数输入
(a: Int, b: Int) => a + b, //该函数操作在每个分区内进行, 类似a=a+b
(m: Int, n: Int) => m + n//对每个分区的结果进行操作, 类似m=m+n
)
rdd2.collect//Array((hello,4), (me,1), (you,2), (her,3))
练习:
给定一个键值对RDD:
val rdd = sc.parallelize(Array(("spark",2),("hadoop",6),("hadoop",4),("spark",6)))
key表示图书名称,
value表示某天图书销量
就是计算每种图书的每天平均销量。
答案:
val rdd2 = rdd.groupByKey//Array((spark,CompactBuffer(6, 2)), (hadoop,CompactBuffer(6, 4)))
rdd2.map(x => (x._1,x._2.sum/x._2.size)).collect//Array((spark,4), (hadoop,5))
总结:
Transformation算子(转换)
转换 | 含义 |
---|---|
map(func) | 返回一个新的RDD,该RDD由每一个输入元素经过func函数转换后组成 |
filter(func) | 返回一个新的RDD,该RDD由经过func函数计算后返回值为true的输入元素组成 |
flatMap(func) | 类似于map,但是每一个输入元素可以被映射为0或多个输出元素(所以func应该返回一个序列,而不是单一元素) |
mapPartitions(func) | 类似于map,但独立地在RDD的每一个分片上运行,因此在类型为T的RDD上运行时,func的函数类型必须是Iterator[T] => Iterator[U] |
mapPartitionsWithIndex(func) | 类似于mapPartitions,但func带有一个整数参数表示分片的索引值,因此在类型为T的RDD上运行时,func的函数类型必须是(Int, Interator[T]) => Iterator[U] |
sample(withReplacement, fraction, seed) | 根据fraction指定的比例对数据进行采样,可以选择是否使用随机数进行替换,seed用于指定随机数生成器种子 |
union(otherDataset) | 对源RDD和参数RDD求并集后返回一个新的RDD |
intersection(otherDataset) | 对源RDD和参数RDD求交集后返回一个新的RDD |
distinct([numTasks])) | 对源RDD进行去重后返回一个新的RDD |
groupByKey([numTasks]) | 在一个(K,V)的RDD上调用,返回一个(K, Iterator[V])的RDD |
reduceByKey(func, [numTasks]) | 在一个(K,V)的RDD上调用,返回一个(K,V)的RDD,使用指定的reduce函数,将相同key的值聚合到一起,与groupByKey类似,reduce任务的个数可以通过第二个可选的参数来设置 |
aggregateByKey(zeroValue)(seqOp, combOp, [numTasks]) | |
sortByKey([ascending], [numTasks]) | 在一个(K,V)的RDD上调用,K必须实现Ordered接口,返回一个按照key进行排序的(K,V)的RDD |
sortBy(func,[ascending], [numTasks]) | 与sortByKey类似,但是更灵活 |
join(otherDataset, [numTasks]) | 在类型为(K,V)和(K,W)的RDD上调用,返回一个相同key对应的所有元素对在一起的(K,(V,W))的RDD |
cogroup(otherDataset, [numTasks]) | 在类型为(K,V)和(K,W)的RDD上调用,返回一个(K,(Iterable,Iterable))类型的RDD |
cartesian(otherDataset) | 笛卡尔积 |
pipe(command, [envVars]) | 对rdd进行管道操作 |
coalesce(numPartitions) | 减少 RDD 的分区数到指定值。在过滤大量数据之后,可以执行此操作 |
repartition(numPartitions) | 重新给 RDD 分区 |
repartitionAndSortWithinPartitions(partitioner) | 重新给 RDD 分区,并且每个分区内以记录的 key 排序 |
Action算子(行动/动作)
动作 | 含义 |
---|---|
reduce(func) | 通过func函数聚集RDD中的所有元素,这个功能必须是可交换且可并联的 |
collect() | 在驱动程序中,以数组的形式返回数据集的所有元素 |
count() | 返回RDD的元素个数 |
first() | 返回RDD的第一个元素(类似于take(1)) |
take(n) | 返回一个由数据集的前n个元素组成的数组 |
takeSample(withReplacement,num, [seed]) | 返回一个数组,该数组由从数据集中随机采样的num个元素组成,可以选择是否用随机数替换不足的部分,seed用于指定随机数生成器种子 |
takeOrdered(n, [ordering]) | 返回自然顺序或者自定义顺序的前 n 个元素 |
saveAsTextFile(path) | 将数据集的元素以textfile的形式保存到HDFS文件系统或者其他支持的文件系统,对于每个元素,Spark将会调用toString方法,将它装换为文件中的文本 |
saveAsSequenceFile(path) | 将数据集中的元素以Hadoop sequencefile的格式保存到指定的目录下,可以使HDFS或者其他Hadoop支持的文件系统。 |
saveAsObjectFile(path) | 将数据集的元素,以 Java 序列化的方式保存到指定的目录下 |
countByKey() | 针对(K,V)类型的RDD,返回一个(K,Int)的map,表示每一个key对应的元素个数。 |
foreach(func) | 在数据集的每一个元素上,运行函数func进行更新。 |
foreachPartition(func) | 在数据集的每一个分区上,运行函数func |