SparkCore
一、RDD概述
在介绍RDD之前先介绍一下java中的IO:
缓冲流(缓冲流提高效率):
装饰者设计模式表示一种功能的扩展,在这里真正读文件的是in而不是buggerIn,javaIO强大的原因就是他可以动态的扩展他的功能
字符流:
使用BufferedReader可以一行一行的读取数据,但要注意并不是所有的字符流都可以一行一行的读数据。
前面的步骤只是转换包装并没有真正的读取,只有用的时候即readLine的时候才读取数据,readLine会找buffer里去读,没有就往前找一直找到第一步才会真正的读取
rdd图解(以wordCount为例):
从图片以及源码来看,把当前的rdd传给new出的rdd又返回的,rdd和javaIO基本上是类似的:
(1)上面IO中所有的返回值都是InputStream即抽象类/父类,他们具有相同的父类RDD也是一个抽象类,比较通用。
(2)io读数据不是一new的时候就读,当做了转换之后readLine的时候才会读,同样只有collect触发的时候才会真正的读数据。
当然他们也有区别:
(1)rdd从属于分布式的集群操作因为rdd读的文件天生就是和集群有关系的做集群计算的而java的IO流只能读一个具体的文件。
(2)rdd其实是封装了逻辑,rdd里面不存数据而javaIO里面的那些纯粹是一个转换,例buffer里面其实是有数据的,当数据达到一个统一的阈值再将数据发送给用户,会有一个暂时存储是概念
不管怎么做所有的rdd都是一层一层的包装,目的是转换结构达成统计想要的一种逻辑操作。rdd里封装了逻辑,他里面没有保存数据,只有真正的执行才会把数据拿过来
1.1 什么是RDD
RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,是Spark中最基本的数据(说计算/逻辑抽象更合适)抽象(它本身是不存数据的)。代码中是一个抽象类,它代表一个不可变、可分区、里面的元素可并行计算的集合。
注:(1)这里的不可变其实是也能变只不过没有提供改变的方法,例:String不可 变,其实也能变可以通过反射去改变。但是就像subsString什么的全是产生一个新 的字符串,就像**.map方法其实也不是改变rdd而是创建了一个新的rdd**
(2)分区和并行计算有很大的关系,如果把大量数据都放在一个分区里面的话就没办法提供并行计算了,kafka,hbase等都有分区,大数据的框架原理基本相同。分区即将很多数据放在不同的分区里面,目的是一个分区可以发送给一个Executor,每个executor都在不同的NM里,每个NM是每个不同的机器,会有自己的CPU和内核等同于真正的并行运行,所以rdd可以真正的并行。就比如kafka的消费者组,消费者组里面可以有多个消费者,一个消费者去读一个或多个分区,一个分区不能同时给两个消费者,同样rdd中的一个分区不能同时让两个Executor去执行
(这里我说一下并行和并发:
并行:多个cpu核,多个任务同时进行;
并发:一般是线程并发即一个cpu的内核有多个线程来抢占资源,线程没有停止关闭还在运行,因为只有一个线程可以抢占资源,那么其他线程就阻塞了;我们所学的多线程并不是真正的多个线程同时运行,而是一个线程抢占完资源释放之后下一个线程再抢占,只是速度很快,给人的感觉是多个线程同时执行,其实同一个时间同一个操作只会有一个线程去运行。)
1.2 RDD的属性
1)一组分区(Partition),即数据集的基本组成单位;
2)一个计算每个分区的函数就是所谓的partioner计算数据可以放在哪个分区里;
3)RDD之间的依赖关系/血缘;
4)一个Partitioner,即RDD的分片函数;
5)一个列表,存储存取每个Partition的优先位置(preferred location)。
RDD源码中表示rdd的依赖关系其实是再他的内部保存了
这个计算任务所用的数据在上面节点上,所以在这台机器上计算最优,不需要通过网络来传输数据,如果要是由第二台机器执行计算,那他需要从第一台将数据取过来。所以移动数据不如移动计算,这里数据所在的位置就叫做优先位置。
但是再RDD源码中为什么优先位置是多个呢?
现在就要说到本地化(线程本地化、节点本地化、机架本地化)的概念了,最好的方式是数据和计算再同一个进程(process)中即进程本地化,但是比如有一个Executor有数据,但是它正在执行计算,把另外一个计算任务给它,他可能做不了,那么可以在同一个节点上可以多启动executor(一个NM中可以有多个Executor,NM中可以有多个Container,每个容器都可以启动Executor),将数据发给另外一个Executor,虽然不在一个进程里但在同一台机器即节点本地化,但不能保证一定有executor,所以还有一种交机架本地化即把数据发给同一个机架上的其他机器。
1.3 RDD特点
RDD表示只读的(即不能改)分区的数据集,对RDD进行改动,只能通过RDD的转换操作,即由一个RDD得到一个新的RDD,新的RDD包含了从其他RDD衍生所必需的信息即产生依赖。RDDs之间存在依赖,RDD的执行是按照血缘关系延时计算的(不是当去依赖的时候就做计算了,而是真正用的时候采取执行计算)。如果血缘关系较长,可以通过持久化RDD来切断血缘关系。
1.3.1 分区
RDD逻辑上是分区的,目的是为了能够并行计算,每个分区的数据是抽象存在的,计算的时候会通过一个compute函数得到每个分区的数据。如果RDD是通过已有的文件系统构建,则compute函数是读取指定文件系统中的数据,如果RDD是通过其他RDD转换而来,则compute函数是执行转换逻辑将其他RDD的数据进行转换。
1.3.2 只读
如下图所示,RDD是只读的,要想改变RDD中的数据,只能在现有的RDD基础上创建新的RDD。由一个RDD转换到另一个RDD,可以通过丰富的操作算子实现,不再像MapReduce那样只能写map和reduce了
所谓的算子就是操作
转换算子:transformations,它是用来将RDD进行转化,构建RDD的血缘关系;行动算子:actions,它是用来触发RDD的计算,得到RDD的相关计算结果或者将RDD保存的文件系统中。
1.3.3 依赖
RDDs通过操作算子进行转换,转换得到的新RDD包含了从其他RDDs衍生所必需的信息,RDDs之间维护着这种血缘关系,也称之为依赖。如下图所示,依赖包括两种,一种是窄依赖,RDDs之间分区是一一对应的,另一种是宽依赖,下游RDD的每个分区与上游RDD(也称之为父RDD)的每个分区都有关,是多对多的关系。
1.3.4 缓存
如果在应用程序中多次使用同一个RDD,可以将该RDD缓存起来,该RDD只有在第一次计算的时候会根据血缘关系得到分区的数据,在后续其他地方用到该RDD的时候,会直接从缓存处取而不用再根据血缘关系计算,这样就加速后期的重用。如下图所示,RDD-1经过一系列的转换后得到RDD-n并保存到hdfs,RDD-1在这一过程中会有个中间结果,如果将其缓存到内存,那么在随后的RDD-1转换到RDD-m这一过程中,就不会计算其之前的RDD-0了。即将血缘关系缓存起来,为了防止中间某些重要信息丢失、血缘中断
1.3.5 CheckPoint
虽然RDD的血缘关系天然地可以实现容错,当RDD的某个分区数据失败或丢失,可以通过血缘关系重建。但是对于长时间迭代型应用来说,随着迭代的进行,RDDs之间的血缘关系会越来越长,一旦在后续迭代过程中出错,则需要通过非常长的血缘关系去重建,势必影响性能。为此,RDD支持checkpoint将数据保存到持久化的存储中,这样就可以切断之前的血缘关系,因为checkpoint后的RDD不需要知道它的父RDDs了,它可以从checkpoint处拿到数据。
二、RDD编程
2.1 编程模型
在Spark中,RDD被表示为对象,通过对象上的方法调用来对RDD进行转换。经过一系列的transformations定义RDD之后,就可以调用actions触发RDD的计算,action可以是向应用程序返回结果(count, collect等),或者是向存储系统保存数据(saveAsTextFile等)。在Spark中,只有遇到action,才会执行RDD的计算(即延迟计算),这样在运行时可以通过管道的方式传输多个转换。
要使用Spark,开发者需要编写一个Driver程序,它被提交到集群以调度运行Worker。Driver中定义了一个或多个RDD,并调用RDD上的action,Worker则执行RDD分区计算任务。
2.2 RDD的创建
在Spark中创建RDD的创建方式可以分为三种:从集合中创建RDD;从外部存储创建RDD;从其他RDD创建(不是纯粹的创建,表示的是rdd的转换即把一个rdd转换成另外一个rdd)。
从集合中创建RDD:
def main(args: Array[String]): Unit = {
val config: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Spark_RDD")
val sc = new SparkContext(config)
//创建rdd
//1)从内存中创建makeRDD
val listRDD: RDD[Int] = sc.makeRDD(List(1,2,3,4)) //makeRDD其实里面是调用的parallelize
//使用自定义分区
//val listRDD: RDD[Int] = sc.makeRDD(List(1,2,3,4),2)
//val value: RDD[String] = sc.makeRDD(Array("1","2"))//这里返回类型取决于读取的结果,由rdd自动完成
//2)从内存中创建parallelize
//这里返回一个rdd是因为parallelize里面new了一个rdd
val arrayRDD: RDD[Int] = sc.parallelize(Array(1,2,3,4)) //并行其实和分区有关系
listRDD.saveAsTextFile("output")
}
看makeRDD源码发现:
看makeRDD源码发现指定了分片即分区,在conf中如果配置了spark.default.parallelism,就将该值取出来作为分区数,如果没指定默认为当前系统的核数,就将所有的可用的cpu的内核总数与2进行比较,取最大值。就像我代码中现在是local[*]模式运行,取当前本机的最大内核数,我机器是4核,所以若没有指定并行度,运行之后发现并行度是4即分区是4(并行度就是分区)
按理说应该是8/3=2 2 4,可是为什么是2 3 3 呢?
查看源码:
蓝色部分是主要的逻辑,所以sc.makeRDD(Array(1,2,3,4,5,6,7,8),3).glom.collect按蓝色部分的逻辑是(0,2),(2,5),(5,8),所以是2 3 3而不是8/3=2 2 4
从外部存储创建RDD:
def main(args: Array[String]): Unit = {
val config: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Spark_RDD")
val sc = new SparkContext(config)
//从外部存储中创建(默认情况下可以读取项目路径也可以读取 其他路径:HDFS)
//默认从文件中读取的数据都是字符串类型
//val fileRDD: RDD[String] = sc.textFile("in")
//读取文件时,传递的分区参数为最小分区数,但是不一定是这个分区数,取决于hadoop读取文件时的分片规则
val fileRDD: RDD[String] = sc.textFile("in",2)
fileRDD.saveAsTextFile("output")
}
textFile源码:
textFile中第二个参数minPartion指的是最小分区,因为文件内容不确定,所以实际分区数可能会比最小分区数大,最后到底多少个分区取决于其分片规则,比如自定义了最小分区是2,input文件中由5个数据(12345放于一行),将数据进行保存过程中会发现分区数是3,因为读文件用到是hadoopFile方式,即spark底层的文件读取是依赖于Hadoop的文件规则,即切片分区规则与hadoop完全相同,hadoop文件读取文件有一个切分的规则,这里最小分区数为2,那么5/2 => 2 2 1因为除不尽所以有3个分区,但是计算分区和往分区里面放数据是两个不同的操作,具体往哪个分区里放数据又是另外一套规则,取决于hadop的分片规则,hadoop是按行读取的,读取一行数据(12345)判断是否大于2,如果大于2就将数据放于0号分区中,然后再取下一行,所以上面例子中查看每个分区中的内容时发现0号分区有12345,其他两个分区中没有数据。
从其他rdd创建详见2.3
2.3RDD的转换
RDD整体上分为Value类型和Key-Value类型
2.3.1 Value类型(单一数据交互)
2.3.1.1 map(func)案例
map其实就是一个结构的变化,把当前的数据结构做了一个逻辑上的改变
map方法关心的是每一条数据
图中mapPartions这个函数只走两遍
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("Spark_Oper1")
val sc = new SparkContext(conf)
val listRDD: RDD[Int] = sc.makeRDD(1 to 10) //to包含,until是不包含
//listRDD.map(_*2)
//map其实就是一个结构的变化,把当前的数据结构做了一个逻辑上的改变
val mapRDD: RDD[Int] = listRDD.map(x => x*2)
//listRDD和mapRDD尽管类型相同但是两个不同的rdd,rdd体现装饰者设计模式,会产生一种新的对象,对功能进行补充
mapRDD.collect().foreach(x=>{
println(x)
})
}
2.3.1.2 mapPartitions(func) 案例
mapPartitions关心的是每一个分区
图中mapPartions这个函数只走两遍
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Spark_Oper2")
val sc = new SparkContext(conf)
val listRDD = sc.makeRDD(1 to 10)
//mapPartitions可以对一个RDD中所有的分区进行遍历,运行次数与数据无关,例:如果只有两个分区,这个逻辑只走两遍
val mapPartitionsRDD: RDD[Int] = listRDD.mapPartitions(datas => {//这里datas是一个可迭代的集合并不是rdd
datas.map(datas=>datas*2)//这里这个map走的是scala的map,其参数是一个可迭代的集合,不是spark的map
})
mapPartitionsRDD.collect().foreach(x=>{println(x)})
}
由上诉代码得知:
mapPartitions效率优于map算子,减少了发送到执行器执行交互次数,map中x => x2算计算,map的所有数据可以发给不同的executor,以上述代码为例,需要发送10次,而mapPartitions中datas.map(data=>data2)是一个计算,即将这个整体发给executor,图中当前是两个分区那么发给两个executor就够了而且只需发两次,所以性能快,网络交互越频繁效率越低。但是mapPartitions可能会出现内存溢出(OOM)因为mapPartitions是把一个分区内所有的数据发给一个executor了,若数据太多,executor可能内存没有那么大,(如果一个分区内地数据要是分着发给executor的话,不释放,因为分区是个整体,如果先发两个G,不会释放,因为还有引用存在),所以实际情况下依情况而选择用什么。
2.3.1.3 mapPartitionsWithIndex(func) 案例
mapPartitionsWithIndex是对每个分区进行转换但同时要关联分区索引,关心的是当前的分区索引即分区号。
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Spark_Oper2")
val sc = new SparkContext(conf)
val listRDD = sc.makeRDD(1 to 10,2)
//val listRDD = sc.makeRDD(1 to 10)
//一般多个参数的时候会用模式匹配
val tupleRDD: RDD[(Int, String)] = listRDD.mapPartitionsWithIndex { //可能会有多行,所以用花括号
case (num, datas) => { //case()表示函数的入口,{}内表示逻辑
datas.map((_, "分区号:" + num))
}
}
tupleRDD.collect().foreach(x=>{
println(x)
})
}
说到这里说一下Spark中Driver与Executor的关系:
所以UI界面中显示的当前任务的执行情况都是由Driver采集的,Executor给的
所有算子中的计算功能都是Executor来做的,Driver会判断计算到底给哪个Executor
上面是再Driver里执行的,_*2是再executor里执行的
那么问题来了:如果在map里面用到了外面的东西该怎么办
Driver与Executor可能在一台机器也可能不在一台机器,所以如果不在一台机器,这个i要通过网络传给executor,有IO的话就会有序列化,所以这个i要一定能够序列化,如果不能序列化的化传不过去。
2.3.1.4 flatMap(func) 案例
//将List类型转换为Int类型
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Spark_Oper2")
val sc = new SparkContext(conf)
val listRDD: RDD[List[Int]] = sc.makeRDD(Array(List(1,2),List(3,4)))
//flatMap返回一个可迭代的
val flatMapRDD: RDD[Int] = listRDD.flatMap(datas => datas)
flatMapRDD.collect().foreach(x=>{//1 2 3 4
println(x)
})
}
2.3.1.5 map()和mapPartition()的区别
- map():每次处理一条数据。
- mapPartition():每次处理一个分区的数据,这个分区的数据处理完后,原RDD中分区的数据才能释放,可能导致OOM。
- 开发指导:当内存空间较大的时候建议使用mapPartition(),以提高处理效率。
2.3.1.6 glom案例
作用:将一个分区的数据放到一个数组中
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Spark_Oper2")
val sc = new SparkContext(conf)
val listRDD = sc.makeRDD(1 to 16,4)
//将一个分区的数据放到一个数组中
val glomRDD: RDD[Array[Int]] = listRDD.glom()
glomRDD.collect().foreach(array=>{
println(array.mkString(","))
})
}
2.3.1.7 groupBy(func)案例
1.作用:分组,按照传入函数的返回值进行分组,将相同的key对于的值放入一个迭代器
2.需求:创建一个RDD,按照元素模以2的值进行分组
/**
* 分组,按照传入函数的返回值进行分组,将相同的key对于的值放入一个迭代器
*/
object groupByTest {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("groupByTest")
val sc = new SparkContext(conf)
val listRDD = sc.makeRDD(1 to 4)
//返回一个元组(K-V),K表示分组的key即模以2的结果,v表示分组的数据集合
val groupRDD: RDD[(Int, Iterable[Int])] = listRDD.groupBy(_%2)
groupRDD.collect().foreach(x=>{
print(x)
})
}
}
2.3.1.8 filter案例
object filterTest{
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("filterTest")
val sc = new SparkContext(conf)
val listRDD: RDD[Int] = sc.makeRDD(List(1,2,3,4))
val filterRDD = listRDD.filter(x => x % 2 == 0) //true表示留下来,false不留
filterRDD.collect().foreach(x=>println(x))
}
}
2.3.1.9 sample(withReplacement,fraction,seed)案例
作用:以指定的随机种子随机抽样,withReplacement表示是抽出的数据是否放回,true为有放回的抽样,false为无放回的抽样,seed用于指定随机数生成器种子。
从源码可以看出,又放回抽样和无放回抽样采用的算法不同,若withReplacement为true,采用泊松算法,若为false采用伯努利算法。
fraction代表打分的概念(其中1表示肯定能抽出来,0表示肯定抽不出来),是一个打分标准,随机数种子seed给每个数随机打分,打分大于该标准则留下来
sample(withReplacement,fraction,seed)案例如下:
object sampleTest {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("sampleTest")
val sc = new SparkContext(conf)
val listRDD = sc.makeRDD(1 to 10)
//从指定数据集合中进行抽样处理,会根据不同的算法进行抽样
//val sampleRDD = listRDD.sample(false,0.4,1)
//这里改为true代表有放回,fraction设的尽量大一点,这样的话,重复的数也会被重复抽取
val sampleRDD = listRDD.sample(true,4,1)
sampleRDD.collect().foreach(println)
}
}
随机种子:
java中没有真正的随机数,有一个随机算法,随机算法是固定的,所以只要随机数种子是固定的,那么最后得到的结果也一定是固定的,因为随机算法固定能推出来,所以想要实现真正的随机数可以用时间戳作为随机种子
public class randomTest {
public static void main(String[] args) {
Random random = new Random(10);
for(int i=0;i<10;i++){
System.out.println(random.nextInt());
}
System.out.println("***********");
random = new Random(10);
for(int i=0;i<10;i++){
System.out.println(random.nextInt());
}
//只要种子相同那么随机出来的结果也是相同的
//所以真正的随机数是这样写的(那时间戳当它的种子)
random=new Random(System.currentTimeMillis());
}
}
看一下上面这两种方式的实现方式有没有不同:
Math.random():
其实Math.random()内部也调用了nextDouble()所以与new Random().nextDouble()其实是一回事儿,只不过前者进行了封装。
但是从源码来看,randdomNumberGenerator这个是静态的,那么意味着只有一份,所以内部不管调用了多少个nextDouble下其实只用了一个对象,
而new Random().nextDouble()中
其实是用了一个系统时间,所以每new一回,种子数就在变
所以,这两种方式的选取还是要适情况而定
2.3.1.10 distinct([numTasks])案例
作用:对源RDD进行去重后返回一个新的RDD。默认情况下,只有8个并行任务来操作,但是可以传入一个可选的numTasks参数改变它
import org.apache.spark.{SparkConf, SparkContext}
object distinctTest {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("distinctTest")
val sc = new SparkContext(conf)
val listRDD = sc.makeRDD(List(1,2,1,5,2,9,6,1))
//val distinctRDD = listRDD.distinct()
//distinctRDD.collect().foreach(println)//结果:1,9,5,6,2 顺序变了
//使用distinct算子对数据进行去重,但是因为去重后会致数据减少,所以可以改变默认的分区数量
val distinctRDD = listRDD.distinct(2)
distinctRDD.saveAsTextFile("output")
}
}
distinct图解:
假设一个rdd里有4个分区
所以distinct操作会走一步shuffle操作,需要等待
如果两个分区没有交叉,没有交叉意味着分区没变的情况下不需要等待,比如map操作:
任务的划分&任务的调度
任务划分原则:一个分区划分为一个任务,一个任务会被分配到一个Executor中去执行(不能将一个分区的数据划分成多个任务)
任务以分区的方式来调度(与kafka类似,kafka中是一个消费者组中的一个消费者去消费一个分区中的数据),这里的话就是Executor去消费任务,这个Executor可以理解为消费者。一个Executor去消费一个分区中的任务
同样的数据执行map算子和distinct算子任务数分别为2和4(同样的数据,任务数越多执行越慢)
假如rdd的分区数为2,对于map来讲没有shuffle,数据不会打乱重组,所以这种情况下,他们可以当作一个整体来做即只需要按照数据的流转顺序往下走就行,不需要等待,所以当作一个任务来做,发给一个Executor
distinct的话,有4个任务,有shuffle read和shuffle write,中间会停在这儿,把p0和p1形成两个任务,这个分区做shuffle的时候要write,将两个分区的任务(不重复的)写在一个文件当中,然后右边去读,读的时候又形成了两个任务,所以总共4个任务,这四个任务有先有后,所以处理慢,而且还会有的处理过程
2.3.1.11 coalesce(numPartitions)案例
作用:缩减分区数,用于大数据集过滤后,提高小数据集的执行效率(所谓的缩减分区可以理解为合并分区,默认无shuffle)
import org.apache.spark.{SparkConf, SparkContext}
object coalesceTest {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("coalesceTest")
val sc = new SparkContext(conf)
val listRDD = sc.makeRDD(1 to 16,4)
println("缩减分区前:"+listRDD.partitions.size)
//缩减分区(所谓的缩减分区可以理解为合并分区,没有打乱重组,所以没有shuffle)
val coalesceRDD = listRDD.coalesce(3)
println("缩减分区后:"+coalesceRDD.partitions.size)
}
}
2.3.1.12 repartition(numPartitions)案例
作用:根据分区数,重新通过网络随机洗牌所有数据
应用场景:比如十个分区变两个,那么如果只是使用coalesce进行合并分区,那么会造成数据倾斜,所以完全可以打乱重组,使用repartition重新分区,因为打乱重组,所以有shuffle
object repartitionTest{
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("repartitionTest")
val sc = new SparkContext(conf)
val listRDD = sc.makeRDD(1 to 16,4)
listRDD.glom().collect().foreach(array=>{
println(array.mkString(","))
})
val repartitionRDD = listRDD.repartition(2)
repartitionRDD.glom().collect().foreach(array=>{
println(array.mkString(","))
})
}
}
coalesce和repartition的区别:
coalesce重新分区,可以选择是否进行shuffle过程,由参数shuffle:Boolean=false/true决定,默认是不shuffle的
repartition实际上是调用coalesce,默认是进行shuffle的,源码如下:
def repartition(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T] = withScope {
coalesce(numPartitions, shuffle = true)
}
2.3.1.14 sortBy(func,[ascending],[numTasks])案例
作用:使用func先对数据进行处理,按照处理后的数据比较结果排序,默认为正序即true,默认分区数为当前分区数,当然也可以改变
object sortByTest{
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("sortByTest")
val sc = new SparkContext(conf)
val listRDD = sc.parallelize(List(2,1,3,4))
val sortByRDD = listRDD.sortBy(x=>x,false)
sortByRDD.collect().foreach(println)
}
}
2.3.2双Value类型交互(两个数据交互)
2.3.2.1 union(otherDataset) 案例
作用:对源RDD和参数RDD求并集后返回一个新的RDD
object unionTest{
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("sortByTest")
val sc = new SparkContext(conf)
val rdd1 = sc.parallelize(1 to 5)
val rdd2 = sc.parallelize(5 to 10)
val resRDD = rdd1.union(rdd2)
resRDD.collect().foreach(println)
}
}
2.3.2.2 subtract (otherDataset) 案例
作用:计算差的一种函数,去除两个RDD中相同的元素,不同的RDD将保留下来
object subtractTest{
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("sortByTest")
val sc = new SparkContext(conf)
val rdd1 = sc.parallelize(3 to 8)
val rdd2 = sc.parallelize(1 to 5)
rdd1.subtract(rdd2).collect().foreach(println)
}
}
2.3.2.3 intersection(otherDataset) 案例
作用:对源RDD和参数RDD求交集后返回一个新的RDD
2.3.2.4 cartesian(otherDataset) 案例
作用:笛卡尔积(尽量避免使用)
2.3.2.5 zip(otherDataset)案例
作用:将两个RDD组合成Key/Value形式的RDD,这里默认两个RDD的partition数量以及元素数量都相同,否则会抛出异常(zip可以理解为拉链,需要一个齿和一个齿对上,但是在scala中元素数量可以不相同,可以直接忽略)。
2.3.3 Key-Value类型
2.3.3.1 partitionBy案例
作用:对pairRDD进行分区操作,如果原有的partionRDD和现有的partionRDD是一致的话就不进行分区, 否则会生成ShuffleRDD,即会产生shuffle过程。
import org.apache.spark.{HashPartitioner, SparkConf, SparkContext}
object partitionByTest {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("partitionByTest")
val sc = new SparkContext(conf)
val rdd1 = sc.parallelize(Array((1,"aaa"),(2,"bbb"),(3,"ccc"),(4,"ddd")),4)
//传一个对象,这个对象叫作分区器,即自己来写分区器
val rdd2 = rdd1.partitionBy(new HashPartitioner(2))
rdd2.glom().collect().foreach(array=>{
println(array.mkString(","))
})
}
}
由源码可知是拿hashCode和分区数量来计算分区号码
自定义分区器:
1.继承Partitioner类
2.重写numPartitions和getPartition方法
import org.apache.spark.{Partitioner, SparkConf, SparkContext}
/**
* 自定义分区器
*/
object MyPartitinerTest {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("MyPartitiner")
val sc = new SparkContext(conf)
val listRDD = sc.makeRDD(List(("a",1),("b",2),("c",3)))
//val listRDD = sc.makeRDD(List(1,2,1,5,2,9,6,1))
//如果rdd结构不是k/v类型的,那partitionBy这个算子就出不来
val psrtitionRDD = listRDD.partitionBy(new MyPartitioner(3))
psrtitionRDD.saveAsTextFile("output")
}
}
/**
* 声明分区器
* 1.继承Partitioner类
* 2.重写numPartitions和getPartition方法
*/
class MyPartitioner(partitions:Int)extends Partitioner{
override def numPartitions: Int ={
partitions
}
override def getPartition(key: Any): Int = {
1
}
}
2.3.3.2 groupByKey案例
作用:groupByKey也是对每个key进行操作,但只生成一个sequence。(会打乱重组,有shuffle)
object groupByKeyTest{//会打乱重组,有shuffle
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("groupByKeyTest")
val sc = new SparkContext(conf)
val words=Array("one","two","two","three","three","three")
val wordPairRDD = sc.parallelize(words).map(word=>(word,1))
val group = wordPairRDD.groupByKey()
//group.collect().foreach(println)
val resRDD = group.map(t=>(t._1,t._2.sum))
resRDD.collect().foreach(println)//wordcount的另一种方法
}
}
2.3.3.3 reduceByKey(func, [numTasks]) 案例
在一个(K,V)的RDD上调用,返回一个(K,V)的RDD,使用指定的reduce函数,将相同key的值聚合到一起,reduce任务的个数(等同于分区数量也等同于并行度)可以通过第二个可选的参数来设置。(会打乱重组,有shuffle但是有一步预聚合的操作)
代码省略
2.3.3.4 reduceByKey和groupByKey的区别
- reduceByKey:按照key进行聚合,在shuffle之前有combine(预聚合,往磁盘写和读的数据变少了)操作,返回结果是RDD[k,v].
- groupByKey:按照key进行分组,直接进行shuffle。
- 开发指导:reduceByKey比groupByKey,建议使用。但是需要注意是否会影响业务逻辑。
有shuffle的话性能确实会慢,但是shuffle过程中有一个预聚合的话会提升他的性能
2.3.3.5 aggregateByKey案例
有分区内和分区间的概念,所以参数就会很复杂
参数:(zeroValue:U,[partitioner: Partitioner]) (seqOp: (U, V) => U,combOp: (U, U) => U)
参数描述:
(1)zeroValue:给每一个分区中的每一个key一个初始值,初始值不见得为0(那个U是泛型);(scala中计算规则一般都是两两进行运算,要是没有初始值的话第一个key计算不了所以赋初始值,也就是说只有key第一回出现的时候初始值才有用)
(2)seqOp:函数用于在每一个分区中用初始值逐步迭代value(一个分区的内部形成一个序列,所以可以理解为分区内运算规则);
(3)combOp:函数用于合并每个分区中的结果(可以理解为分区间运算规则)。
将数据打乱重组了,有shuffle
作用:
在kv对的RDD中,,按key将value进行分组合并,合并时,将每个value和初始值作为seq函数的参数,进行计算,返回的结果作为一个新的kv对,然后再将结果按照key进行合并,最后将每个分组的value传递给combine函数进行计算(先将前两个value进行计算,将返回结果和下一个value传给combine函数,以此类推),将key与计算结果作为一个新的kv对输出即先在分区内运算再在分区间运算。
需求:
创建一个pairRDD,取出每个分区相同key对应值的最大值,然后相加
import org.apache.spark.{SparkConf, SparkContext}
/**
* 创建一个pairRDD,取出每个分区相同key对应值的最大值,然后相加
* 关键点是数据在哪一个分区里
*/
object aggregateByKeyTest {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("aggregateByKeyTest")
val sc = new SparkContext(conf)
val listRDD = sc.parallelize(List(("a",3),("a",2),("c",4),("b",3),("c",6),("c",8)),2)
//看一下分区分布情况(c,6和8是放在了一个分区里,所以最终结果为12)
// listRDD.glom().collect().foreach(array=>{
// println(array.mkString(","))
// })
//因为要两两计算(scala中计算规则一般都是两两进行运算),要是没有初始值的话第一个key计算不了所以赋初始值(也就是说只有key第一回出现的时候初始值才有用)
val arrRDD = listRDD.aggregateByKey(0)(math.max(_,_),_+_)//math.max(_,_)也可以用List(_,_).max
arrRDD.collect().foreach(println)
}
}
上面listRDD.aggregateByKey(0)(+,+)任然可以实现一个wordcount
2.3.3.6 foldByKey案例
参数:(zeroValue: V)(func: (V, V) => V): RDD[(K, V)]
作用:aggregateByKey的简化操作,seqop和combop相同
aggregateByKey与foldByKey源码对比:
aggregateByKey:
combineByKeyWithClassTag[U](
(v: V) =>
cleanedSeqOp(createZero(), v),//cleanedSeqOp分区内(createZero代表初始值,v代表每一个value)
cleanedSeqOp, //分区内
combOp, //分区间
partitioner)
foldByKey:
combineByKeyWithClassTag[V](
(v: V) =>
cleanedFunc(createZero(), v),
cleanedFunc,
cleanedFunc,
partitioner)
所以从源码可知foldByKey分区内和分区间方法相同,所以所谓的foldByKey其实就是省略了一个参数
2.3.3.7 combineByKey[C] 案例
参数:(createCombiner: V => C, mergeValue: (C, V) => C, mergeCombiners: (C, C) => C)
作用:对相同K,把V合并成一个集合。
参数描述:
(1)createCombiner: combineByKey() 会遍历分区中的所有元素,因此每个元素的键要么还没有遇到过,要么就和之前的某个元素的键相同。如果这是一个新的元素,combineByKey()会使用一个叫作createCombiner()的函数来创建那个键对应的累加器的初始值即创建初始规则
(2)mergeValue: 如果这是一个在处理当前分区之前已经遇到的键,它会使用mergeValue()方法将该键的累加器对应的当前值与这个新的值进行合并
(3)mergeCombiners: 由于每个分区都是独立处理的, 因此对于同一个键可以有多个累加器。如果有两个或者更多的分区都有对应同一个键的累加器, 就需要使用用户提供的 mergeCombiners() 方法将各个分区的结果进行合并。
源码如下:
def combineByKey[C](
createCombiner: V => C,
mergeValue: (C, V) => C,
mergeCombiners: (C, C) => C): RDD[(K, C)] = self.withScope {
combineByKeyWithClassTag(createCombiner, mergeValue, mergeCombiners)(null)
}
其实combineByKey是和aggregateByKey及foldByKey是大同小异的
aggregateByKey和foldByKey中有一个createZero是因为第一个v也要参与运算
但在某些情况下,第一个v不见得要固定写死就用分区内的计算,因为可能第一个v结构不满足。比如想要将一个v(“a”,1)此时这个1的结构不是想要的,没办法作运算,比如想要的是一个元组,就得想办法把1变成指定结构,所以计算规则就得传递进去
需求:
创建一个pairRDD,根据key计算每种key的均值。(先计算每个key出现的次数以及可以对应值的总和,再相除得到结果)
/**
* 创建一个pairRDD,根据key计算每种key的均值。
* (先计算每个key出现的次数以及可以对应值的总和,再相除得到结果)
*/
object combineByKeyTest{
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("combineByKeyTest")
val sc = new SparkContext(conf)
val input = sc.parallelize(Array(("a", 88), ("b", 95), ("a", 91), ("b", 93), ("a", 95), ("b", 98)),2)
val combineByKeyRDD = input.combineByKey(
(_, 1),
(acc: (Int, Int), v) => (acc._1 + v, acc._2 + 1), //前面代表初始值,v代表第二个元素 例子初始值:(88,1)与91关联 (初始值已经变成了一个tuple,第二个值还是一个数)
(acc1: (Int, Int), acc2: (Int, Int)) => (acc1._1 + acc2._1, acc1._2 + acc2._2)
)
//combine.map{case (key,value) => (key,value._1/value._2.toDouble)}
val resRDD = combineByKeyRDD.map {
case (key, value) => (key, value._1 / value._2.toDouble)
}
resRDD.collect().foreach(println)
}
}
比如只是简单的wordcount以下这三种可以实现同样的效果:
rdd.aggregateByKey(0)(+,+)
rdd.foldByKey(0)(+)
上面两种方法初始值赋0,combineByKey中初始值不用变,原来是啥就是啥,直接拿来运算
rdd.combineByKey(x=>x,+,+) 这种写法会报错
rdd.combineByKey(x=>x,(x:Int,y:Int)=>x+y,(x:Int,y:Int)=>x+y) 这种正确
(x:Int,y:Int)=>x+y中写成(x:Int,y)=>x+y也可以,因为可以推断出V的类型,但是推断不出来C的类型(V代表第一个出现的值,C代表转换之后的),所以写成(x,y:Int)=>x+y就报错了 error:missing parameter type for expanded ((x$1,x$2)=>x
1.
1.
1.plus(x$2))
在aggregateByKey和foldByKey中
U是ClassTag泛型,意味着在编译时也不知道具体是什么类型,但是可以在运行过程中反射得到它的类型,因为当给U的时候(初始值)它的类型就能得到了
(加了ClassTag就是运行时推断)
所以以上三种算子中,combineByKey更加灵活,不进行推断,可以灵活变换,想给什么类型就给什么类型,其余两种是更方便但不灵活
2.3.3.8 sortByKey([ascending], [numTasks]) 案例
作用:在一个(K,V)的RDD上调用,K必须实现Ordered接口,返回一个按照key进行排序的(K,V)的RDD(默认为升序)
object sortByKeyTest {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("sortByKeyTest")
val sc = new SparkContext(conf)
val rdd = sc.parallelize(Array((3,"aa"),(6,"cc"),(2,"bb"),(1,"dd")))
rdd.sortByKey(true).collect().foreach(println)
}
}
2.3.3.9 mapValues案例
作用:针对于**(K,V)形式的类型**只对V进行操作(map是对数据进行转换,这里是只对value转换)
object mapValuesTest{
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("sortByKeyTest")
val sc = new SparkContext(conf)
val listRDD = sc.parallelize(Array((1,"a"),(1,"d"),(2,"b"),(3,"c")))
listRDD.mapValues(_+"|||").collect().foreach(println)
}
}
2.3.3.10 join(otherDataset, [numTasks]) 案例
作用:在类型为(K,V)和(K,W)的RDD上调用,返回一个相同key对应的所有元素对在一起的(K,(V,W))的RDD
/**
* 结果:
* (1,(a,4))
* (2,(b,5))
* (3,(c,6))
*/
object joinTest{
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("sortByKeyTest")
val sc = new SparkContext(conf)
val rdd = sc.parallelize(Array((1,"a"),(2,"b"),(3,"c")))
val rdd1 = sc.parallelize(Array((1,4),(2,5),(3,6)))
val joinRDD = rdd.join(rdd1)
joinRDD.collect().foreach(println)
}
}
(join的性能较低)
2.3.3.11 cogroup(otherDataset, [numTasks]) 案例
作用:在类型为(K,V)和(K,W)的RDD上调用,返回一个(K,(Iterable,Iterable))类型的RDD
/**
* 结果:
* (1,(CompactBuffer(a),CompactBuffer(4)))
* (2,(CompactBuffer(b),CompactBuffer(5)))
* (3,(CompactBuffer(c),CompactBuffer(6)))
*/
object cogroupTest{
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("sortByKeyTest")
val sc = new SparkContext(conf)
val rdd = sc.parallelize(Array((1,"a"),(2,"b"),(3,"c")))
val rdd1 = sc.parallelize(Array((1,4),(2,5),(3,6)))
val resRDD = rdd.cogroup(rdd1)
resRDD.collect().foreach(println)
}
}
cogroup与join的区别:
join必须满足key相同,join中相连接的两个rdd如果key不相同就抛弃
而cogroup中即使key不行=相同也会列出来
上图中rdd为val rdd = sc.parallelize(Array((1,“a”),(2,“b”),(3,“c”)))
两者会有一个方位顺序:
2.3.4案例实操