Spark Core概括

Spark Core

一.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做了什么?

  1. 中间结果保存在内存中,并重用;
  2. 提供了通用的抽象的分布式的数据模型;
  3. 提供了多种数据操作模式(支持函数式编程;

总之相比与传统的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.
 */
  1. 分区列表 A list of partitions

    对于RDD来说,每个分片都会被一个计算任务处理,分片数决定并行度。
    用户可以在创建RDD时指定RDD的分片个数,如果没有指定,那么就会采用默认值。

  2. 作用于每个分片的计算函数 A function for computing each split

    Spark中RDD的计算是以分片为单位的,计算函数会被作用到每个分片上。

  3. 对父RDD的依赖列表 A list of dependencies on other RDDs

    RDD的每次转换都会生成一个新的RDD,所以RDD之间就会形成类似于流水线一样的前后依赖关系。在部分分区数据丢失时,Spark可以通过这个依赖关系重新计算丢失的分区数据,而不是对RDD的所有分区进行重新计算。(Spark的容错机制)

  4. 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输出时的分片数量。

  5. 每个数据分区的地址列表(如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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值