Spark之RDD
在介绍RDD之前,先从java的IO讲起会比较容易理解
1.Java IO
Java的输入可以分为字节流输入(rar,zip,dot,png,jpg)和字符流输入(txt)
现有创建一个字节流输入:
//文件输入流
InputStream in = new FileInputStream("XXXX");
这样输入很慢很慢,所以就有了下面的缓冲流
InputStream bufferIn = new BufferedInputStream(new FileInputStream("XXXX"));
从输入流到缓冲流体现了java的装饰者设计模式,实际读文件还是FileInputStream对象读,只是通过“包装和装饰”给了它新的功能
那么如果需要按行读取数据,就需要使用字节流(Reader,Writer)
//使用字符流读取一行数据
Reader reader=new BufferedReader(new InputStreamReader(in,"UTF-8"))
需要一个转换流把字节转换成字符,再从Buffer中一行一行读取。
可以看到读取发生的时间是在真正调用的时候才发生,一层一层的往里面发出读数据的请求
2.RDD
2.1 简介
RDD和java的IO很相似的也采用了装饰者的设计模式。比较粗糙的形容就是:依旧是一层叠一层的形式完成对数据的多种操作。
就是向上图这样一层套一层完成对数据的操作,就上上面不论是文件输入流还是缓冲流,使用的都是相同的父类InputStream,RDD也是这个道理。
前面提到了如果在java中,不调用到数据,真正的读数据的操作是不会发生的;对于RDD来说也是一样,如果最后的Collect不发生,读数据的操作依旧不会发生。之前的一层一层的处理都是在封装逻辑,实际是没有把数据代入操作的。所以RDD实际是把数据处理的逻辑进行了封装。
他们之间非常的类似,当然也有区别:RDD是分布式的集群操作,但是IO是无法分布式操作的。
RDD叫做弹性分布式数据集,是spark中最基本的数据(计算)抽象,代码中是一个抽象类,它表示一个不可变,可分区,里面元素可并行计算的集合。
tips:并发指的是多个线程抢占一个CPU的资源,同一时间只有一个线程在真正的运行
2.2 RDD的属性
- 一组分区(Partition),即数据集的基本组成单位
- 一个计算每个分区的函数
- RDD之间的依赖关系
- 一个Partition,即RDD的分片函数
- 一个列表,存储存取每个Partition的优先位置(preferred location)。RDD–移动数据不如移动计算
2.3 RDD特点
RDD表示只读的分区的数据集,对RDD进行改动,只能通过RDD的转换操作,由一个RDD得到一个新的RDD,新的RDD包含了从其他RDD衍生所必须的信息,RDDs之间存在依赖,RDD的执行是按照血缘关系延时计算的。如果血缘关系较长,可以通过持久化来切断血缘关系。
2.4 算子
解决问题的其实就是将问题的初始状态,通过一系列的操作对问题的状态进行转换,然后达到解决的状态,这个操作的概念就叫算子(operate)。
在spark中所有RDD的方法都叫做,但是主要分为两大类—转换算子和行动算子
2.5 RDD的创建
三种方式
[makeRDD、parallelize]、fileText
2.5 RDD的分区
- 对于从内存中创建的RDD来说,默认的分区数是
max(CPU的内核数,2)
,当然也可以自定义:
//自定义分区为2
val listRDD: RDD[Int] = sc.makeRDD(List(1,2,3,4),2)
计算规则如下图:
- 对于文件读取的RDD来说,默认情况下,是min(CPU内核数,2);自定义的情况下,传递的分区参数为最小分区数,不一定是这个分区数,取决于hadoop读取文件时的分片规则(当文件为12个字节,自定义分区数为2,则最后会被分为6,6,1三个分区)。
2.6 RDD的转换
RDD整体上分为Value类型和Key-Value类型
2.6.1 Value类型
2.6.1.1 map算子
作用:返回一个新的RDD,该RDD由每一个输入元素经过fun函数转换后组成
例子:将数组所有元素*2
//map算子
val listRDD: RDD[Int] = sc.makeRDD(1 to 10)
val mapRDD: RDD[Int] = listRDD.map(_*2)
//val mapRDD: RDD[Int] = listRDD.map(x=>x*2)
2.6.1.2 mapPartitions算子
作用:类似于map,但独立地在RDD的每一个分片上运行,因此在类型为T的RDD上运行时,func的哈数类型必须是Iteration[T]=>Iteration[U]。假设有N个元素,有M个分区,那么map的函数将被调用N次,而mapPartitions被调用M次,一个函数一次性处理整个分区的数据。
例子:将数组所有元素*2
//mapPartitions可以对一个RDD中所有分区进行访问
val listRDD: RDD[Int] = sc.makeRDD(1 to 10)
val mapPatitionRDD: RDD[Int] = listRDD.mapPartitions(datas=>{
datas.map(datas=>datas*2)
})
tips:
- map():每次处理一条数据
- mapPartitions():每次处理一个分区的数据,这个分区的数据处理完成后,原RDD中分区的数据才能释放,可能导致OOM。
2.6.1.3 mapPartitionsWithIndex算子
作用:类似于mapPartitions,但func带有一个整数参数表示分片的索引值,因此在类为T的RDD上运行,func的函数类型必须是(Int,Iteration[T]=>Iteration[U]);
例子:输出分区号和对应数据
//mapPartitionsWithIndex
val listRDD: RDD[Int] = sc.makeRDD(1 to 10)
val mPWIRDD: RDD[(Int, String)] = listRDD.mapPartitionsWithIndex {
case (num, datas) => {
datas.map((_,"分区号:"+num))
}
}
2.6.1.4 flapmap算子
作用:类似于map,但是每一个输入元素可以被映射为0或多个输出元素(所以func应该返回一个序列而不是一个单一的元素)
例子:把两个集合扁平化输出
//flapmap
//输出结果会是1,3,3,5
val listRDD: RDD[List[Int]] = sc.makeRDD(Array(List(1,3),List(3,5)))
val flapRDD: RDD[Int] = listRDD.flatMap(datas=>datas)
2.6.1.5 glom算子
作用:将一个分区形成一个数据,形成新的RDD类型RDD[Array[[T]]
例子:把1-16分到四个分区中
//glom算子
//因为不是文件,所以除不尽的余数就会放在最后一个分区里面,不会单独再多一个
val listRDD: RDD[Int] = sc.makeRDD(1 to 16,4)
val glowRDD = listRDD.glom()
glowRDD.collect().foreach(array=>{
println(array.mkString(","))
})
2.6.1.6 groupBy算子
作用:分组,按照传入函数的返回值进行分组,将相同的key对应的值放入一个迭代器
例子:能被2整除的放在一个分组,不能的放在另一个分组
//groupBy算子
val listRDD: RDD[Int] = sc.makeRDD(1 to 16)
val groupByRDD: RDD[(Int, Iterable[Int])] = listRDD.groupBy(i=>i%2)
2.6.1.7 filter算子
作用:一个过滤器,满足要求的数据留下,不满足的就过滤掉
例子:留下偶数
//filter
val listRDD: RDD[Int] = sc.makeRDD(1 to 16)
val filterRDD = listRDD.filter(i=>i%2==0)
2.6.1.8 sample算子
作用:sample(withReplacement,fraction,seed);以指定的随机种子随机抽样出数量为fraction的数据,withReplacement表示是抽出的数据是否放回,true为有放回的抽样,false为无放回的抽样,seed用于指定随机数生成器种子。
放回的算法是泊松,不放回的算法是伯努利(不是正就是反,或者说不是大于标准就是小于标准),而这个标准就是fraction,当fraction=1的时候代表全部满足,fraction=0的时候代表全部都不满足,在0-1之间的数就代表抽取部分。
例子:抽取(不放回)数组中的部分数据
val listRDD: RDD[Int] = sc.makeRDD(1 to 16)
//给每个数打分,超过0.6的就输出,没超过就不输出
val sampleRDD = listRDD.sample(false,0.6,1)
说着是随机,但是上面一样的运行一百遍都是同样的结果,因为这不是真正的随机数,只要一开始的第一个数字是一样的,后面就都会是一样的结果。想要真正的随机数,需要用时间戳当种子。
抽样可以用来查看数据倾斜的具体情况,可以对数据进行重复抽样,如果有些数据一直反复的大量出现,就说明可能需要对这些数据进行一些处理了。
2.6.1.9 distinct算子
作用:distinct(numTask);对源RDD进行去重后返回一个新的RDD。默认情况下,只有8个并行任务来操作,但是可以传入一个可选的numtask来改变它。
例子:去重
//distinct
val listRDD: RDD[Int] = sc.makeRDD(List(1,1,1,2,2,3,3,4,5,6))
val distinctRDD = listRDD.distinct()
可以发现输出没错,但是顺序被改变了,把它输入到文件中,可以发现,这些数据被放到了不同的分区中,所以输出顺序不受控制。
这是因为在distinct的过程中,RDD中一个分区的数据被打乱重组(shuffle)了。
这时候看发现,到目前为止,除了distinct会被shuffle,其他的算子都没有经历这个过程。是因为其他的算子,计算当前分区的数据时不需要其他分区数据的参与,而distinct需要其他分区数据的参与(需要知道其他分区是否有重复数据),这就导致了它一定会被shuffle。
tips:spark中所有没有shuffle的算子,性能比较快。shuffle一定要有打乱和重组的概念在里面,如果没有打乱只有重组,那就不算shuffle。
例子2:去重,因为数据变少,可以改变默认分区的大小,这里改成2
//distinct
val listRDD: RDD[Int] = sc.makeRDD(List(1,1,1,2,2,3,3,4,5,6))
val distinctRDD = listRDD.distinct(2)
2.6.1.10 coalesce算子
作用:coalesce(numPartition);缩减分区数,用于大数据集过滤后,提高小数据集的执行效率。
例子:把4个分区缩减成3个分区
//coalesce
val listRDD: RDD[Int] = sc.makeRDD(1 to 16,4)
println("before:"+listRDD.partitions.size)
val coalesceRDD = listRDD.coalesce(3)
println("after:"+coalesceRDD.partitions.size)
缩减的原理就是合并,上面的例子就是把第四个分区的数据合并到第三个分区中,因为只是合并,所以不存在shuffle。
2.6.1.11 repartition算子
作用:repartition(numPartitions)根据分区数,重新通过网络随机洗牌所有数据,一般用在很大分区数合并成很小分区数的情况,防止发生数据倾斜。
例子:4分区合并成2分区
//rePartition
val listRDD: RDD[Int] = sc.makeRDD(1 to 16,4)
val rePartitionRDD = listRDD.repartition(2)
val glomRDD = rePartitionRDD.glom()
glomRDD.collect().foreach(array=>{
println(array.mkString(","))
})
会产生shuffle
2.6.1.12 sortBy算子
作用:sortBy(func,[ascending],[numTasks]);使用func先对数据进行处理,按照处理后的数据比较结果排序,默认为正序,通过ascending=false可以改成降序。分区默认为当前的分区数量
//sortBy
val listRDD: RDD[Int] = sc.makeRDD(List(1,5,4,7,2))
val sortByRDD = listRDD.sortBy(x=>x,false)
2.6.2 双Value类型
2.6.2.1 union算子
作用:union(otherDataset);对源RDD和参数RDD求集后返回一个新的RDD
//union
val listRDD: RDD[Int] = sc.makeRDD(List(1,5,4,7,2))
val listRDD2 = sc.makeRDD(2 to 5)
val unionRDD = listRDD.union(listRDD2)
2.6.2.2 subtract算子
作用:subtract(otherDataset);计算差的一种函数,去除两个RDD中相同元素,只留下不同的元素,也就是取差集。
//substract
val listRDD: RDD[Int] = sc.makeRDD(List(1,5,4,7,2,5))
val listRDD2 = sc.makeRDD(2 to 5)
val substractRDD = listRDD.subtract(listRDD2)
2.6.2.3 intersection算子
作用:intersection(otherDataset);取交集
//intersection
val listRDD: RDD[Int] = sc.makeRDD(List(1,5,4,7,2,5))
val listRDD2 = sc.makeRDD(2 to 5)
val instersetionRDD = listRDD.intersection(listRDD2)
2.6.2.4 cartesian算子
作用:cartesian(otherDataset);笛卡尔积,尽量不要使用。
//cartesian
val listRDD: RDD[Int] = sc.makeRDD(List(1, 5, 4, 7, 2, 5))
val listRDD2 = sc.makeRDD(2 to 5)
val cartesianRDD = listRDD.cartesian(listRDD2)
2.6.2.5 zip算子
作用:zip(otherDataset);使两个RDD拼接成K-V的形式,这要求两个RDD分区数相等,并且每个分区的数据量也要相等,不然会报错。
//zip
val listRDD: RDD[Int] = sc.makeRDD(List(1, 5, 4),3)
val listRDD2 = sc.makeRDD(List("one","five","four"),3)
val zipRDD = listRDD.zip(listRDD2)
2.6.3 Key-Value类型
2.6.3.1 partitionBy算子
作用:对pairRDD(是RDD的一种隐式转换)进行分区操作,如果原有的partitionRDD和现有的partitionRDD是一直的话就不进行分区,否则会发生shuffleRDD,即会产生shuffle过程。
例子1:创建一个4分区的RDD,对其重新分区。
//partitionBy
val listRDD= sc.makeRDD(List((1,"aaa"),(2,"bbb"),(3,"ccc"),(3,"ddd"),(4,"fff")),4)
//HashPartitioner就是一个根据key的hashcode来划分分区,分区数就是2
val partitionBYRDD = listRDD.partitionBy(new org.apache.spark.HashPartitioner(2))
val glomRDD = partitionBYRDD.glom()
glomRDD.collect().foreach(array=>{
System.out.println(array.mkString(","))
})
System.out.println("分区数:"+partitionBYRDD.partitions.size)
可以看到key的值,1、3被分到一起;2、4被分到一起,这就体现了HashPartitioner的方法。
例子2:自定义分区算法,把key为“a”的分到单独的一个区
object opertwo1 {
def main(args: Array[String]): Unit = {
val config: SparkConf = new SparkConf().setMaster("local[*]").setAppName("newRDD")
val sc = new SparkContext(config)
//partitionBy
val listRDD= sc.makeRDD(List((1,"aaa"),("a","bbb"),(3,"ccc"),(3,"ddd"),(4,"fff")),4)
val partitionBYRDD = listRDD.partitionBy(new MyPartitioner(2))
val glomRDD = partitionBYRDD.glom()
glomRDD.collect().foreach(array=>{
System.out.println(array.mkString(","))
})
System.out.println("分区数:"+partitionBYRDD.partitions.size)
}
}
//申明分区器
//继承partition类
class MyPartitioner(partitions: Int) extends Partitioner {
override def numPartitions: Int = partitions
override def getPartition(key: Any): Int = {
if(key.equals("a")){
0 //分区号是从0开始的,2个分区是0号和1号
}else{
partitions-1
}
}
}
2.6.3.2 groupByKey算子
作用:groupByKey也是对每个key进行操作,但只生成一个sequence
例子:一个wordcount
//groupByKey
val words = Array("one","two","three","one","two","one","one")
val listRDD = sc.makeRDD(words).map(words=>(words,1))
val groupByKeyRDD = listRDD.groupByKey()
val sumRDD = groupByKeyRDD.map(t=>(t._1,t._2.sum))
2.6.3.3 reduceByKey算子
作用:reduceByKey(func,[numTasks]);指定reduce函数,将相同的key值聚合到一起,reduce任务的个数可以通过第二个参数设置。
例子:另一个wordcount
//reduceByKey
val words = Array("one","two","three","one","two","one","one")
val listRDD = sc.makeRDD(words).map(words=>(words,1))
val sumRDD = listRDD.reduceByKey(_+_)
结果和上面是一样的
tips:reduceByKey和groupByKey的区别
- reduceByKey:按照key进行聚合,在shuffle之前有combine(预聚合)操作,返回结果是RDD[k.v]
- groupByKey:按照key进行分组,直接进行shuffle
- reduceByKey比起groupByKey更好一些,但是要考虑到会不会影响到业务逻辑。
2.6.3.4 aggregateByKey算子
作用:例如要完成一个逻辑:分区内相加,分区间相减。用reduceByKey就无法完成,这个就可以完成。
例子:创建一个pairRDD,取出每个分区相同key对应值的最大值,然后分区间如果还有相同的key,就把他们相加,如下图的就是红色框相加,蓝色框相加。
对上面分区进行处理
//aggregateByKe
val listRDD= sc.makeRDD(List(("a",1),("a",2),("b",1),("b",2),("a",4),("b",5)),2)
//math(_,_)为分区内的处理逻辑,_+_为分区间的处理逻辑
//初始值是因为第一个数据进来没有两个值可以进行逻辑处理的,所以需要给一个初值
val aggRDD = listRDD.aggregateByKey(0)(math.max(_,_),_+_)
//这又是一个新的wordcount
//val aggRDD = listRDD.aggregateByKey(0)(_+_,_+_)
2.6.3.5 foldeByKey算子
作用:aggregateByKey的简化操作,seqop和combop相同,也就是分区内和分区间的操作逻辑来说一样的,所以省略。
例子:叒一个wordcount
//foldByKe
val listRDD= sc.makeRDD(List(("a",1),("a",2),("b",1),("b",2),("a",4),("b",5)),2)
//初始值是因为第一个数据进来没有两个值可以进行逻辑处理的,所以需要给一个初值
val foldRDD = listRDD.foldByKey(0)(_+_)
2.6.3.6 combineByKey算子
作用:对相同K,把V合并成一个集合。它的底层实现和上面两个算子基本一致,只是对于第一个参数的处理做了改变。上面两个算子对第一个参数的处理是:给一个初始值,并且处理逻辑就和分区内的处理逻辑一致;而combineByKey对第一个值的处理逻辑是可以重新自定义的,不需要和分区内的处理逻辑一致。
例子:计算相同key的平均值。
思路:对于数据:List(("a",1),("a",2),("b",1),("b",2),("a",4),("b",5))
。之前我们可以做到把相同key的值相加foldByKey(_+_)
,但是无法得到总共加了几次这个信息,现在可以对第一个值进行处理,把它的value变成(1,1)
,然后加上第二个("a",2)
,变成(3,2)
,最后加上第三个("a",4)
,变成(7,3)
。然后平均值就用7/3
.
//combineByKey
val listRDD= sc.makeRDD(List(("a",1),("a",2),("b",1),("b",2),("a",4),("b",5)),2)
val combineRDD = listRDD.combineByKey((_,1),(acc:(Int,Int),v)=>(acc._1+v,acc._2+1),(acc1:(Int,Int),acc2:(Int,Int))=>(acc1._1+acc2._1,acc1._2+acc2._2))
combineRDD.glom().collect().foreach(array=>{
println(array.mkString(","))
})
val result = combineRDD.map {
case (key,value) => (key,value._1 / value._2.toDouble)
}
这个算子同样也可以用来计算wordcount:
listRDD.combineByKey(x:Int,y)=>x+y,(x:Int,y:Int)=>x+y)
需要注意的是第一个x一定要写出数据类型,因为这个是不会自动获取的。
2.6.3.7 sortByKey算子
作用:sortByKey([ascending],[numTasks]);在一个(K,V)的RDD上调用,K必须实现Ordered接口,返回一个按照key进行排序的(K,V)的RDD
例子:按照key的正序和倒序进行排序
//sortByKey
val listRDD= sc.makeRDD(List((1,"red"),(21,"blue"),(13,"yellow"),(8,"pink"),(10,"black"),(5,"white")))
//正序
val sortByKeyRDD = listRDD.sortByKey()
//倒序
val sortByKeyRDD2 = listRDD.sortByKey(false)
2.6.3.8 mapValues算子
作用:针对于(K,V)形式的类型,只对V进行操作。
例子:给value增加字符串“|||”
/mapValue
val listRDD= sc.makeRDD(List((1,"red"),(21,"blue"),(13,"yellow"),(8,"pink"),(10,"black"),(5,"white")))
val mapValueRDD = listRDD.mapValues(_+"|||")
2.6.3.9 join算子
作用:在类型为(K,V)和(K,W)的RDD上调用,返回一个相同的key对应的所有元素对在一起的(K,(V,W))的RDD。性能较低
例子:
//join
val listRDD= sc.makeRDD(List((1,"red"),(21,"blue"),(13,"yellow"),(8,"pink"),(10,"black"),(5,"white")))
val listRDD1= sc.makeRDD(List((1,4),(21,5),(13,2),(8,88),(10,15),(5,3),(4,11)))
val joinRDD = listRDD.join(listRDD1)
2.6.3.10 cogroup算子
作用:在类型为(K,V)和(K,W)的RDD上调用,返回一个(K,(Iterable< V>,Iterable< W>))类型的RDD
例子:连接
//cogroup
val listRDD= sc.makeRDD(List((1,"red"),(21,"blue"),(13,"yellow"),(8,"pink"),(10,"black"),(5,"white")))
val listRDD1= sc.makeRDD(List((1,4),(21,5),(13,2),(8,88),(10,15),(5,3),(4,11)))
val cogroupRDD = listRDD.cogroup(listRDD1)
tips:cogroup和join的差别在于红色框出的部分,join只合并两个RDD中有key相同的数据,而cogroup不论另一个RDD中是否有相同的key,都会保留下来,一边为空而已。
2.6.4 一个案例
需求:统计每个省份中,广告点击数量最多的TOP3的广告编号
思路:
- 首先合并的结果应该是:省份-广告编号-点击次数;这样的格式
- 这要求K值应该是(省份+广告编号),V值应该是(1),然后计数。
val input = sc.textFile("D:\\input\\agent.txt")
//按空格拆分数据,并且把第一列和第四列合并成key值,value=1
val input2 = input.map(x => {
val fields: Array[String] = x.split(" ")
((fields(1) , fields(4)), 1)
})
//key值相同的进行相加,得到的是同一省份同一广告的记数和
val reduceRDD = input2.reduceByKey(_+_)
//把省份单独拆分出来作为主键,以便后面的分组
val input3 = reduceRDD.map(x=>(x._1._1,(x._1._2,x._2)))
//把主键相同的分为一组,即省份相同的分到一起
val groupRDD = input3.groupByKey()
//对value值进行操作:得到点击量前三的信息
val resultRDD = groupRDD.mapValues(x => {
x.toList.sortWith((x, y) => {
x._2 > y._2
}).take(3)
})
resultRDD.collect().foreach(println)
2.6.5 行动算子
2.6.5.1 reduce算子
作用:reduce(func);通过func函数集RDD中的所有元素,先聚合分区内的数据,再聚合分区间的数据。使用起来没什么特别的地方。
2.6.5.2 collect()
作用:在驱动程序中,以数组的形式返回数据集的所有元素。
2.6.5.3 count()
作用:返回RDD中元素的个数
2.6.5.4 first()
作用:返回RDD中的第一个元素
2.6.5.5 take(n)
作用:返回一个由RDD的前n个元素组成的数组
2.6.5.6 takeOrdered(n)
作用:返回该RDD排序后的前n个元素组成的数组
2.6.5.7 aggregate
作用:aggregate函数将每个分区里面的元素通过区间内合并函数和初始值进行聚合,然后用combine函数将每个分区的结果和初始值进行combine操作,这个函数最终返回的类型不需要和RDD中元素类型一致。
它比较特别的一点是,分区内做计算的时候会加上初始值,分区间做计算的时候还会加上初始值。aggregateByKey在分区间做计算的时候是不会加上初始值的。
2.6.5.8 fold(num)(func)
作用:折叠操作,aggregate的简化操作,区间内的操作和分区间的操作是一样的。
2.6.5.9 saveAsTextFile(path)
作用:将数据集的元素以textfile的形式保存到HDFS文件系统或者其他支持的文件系统。
2.6.5.10 saveAsSequenceFile(path)
作用:将数据集的元素以Sequence的形式保存到指定目录下,可以是HDFS或者其他Hadoop支持的文件系统。
2.6.5.11 saveAsObjectFile(path)
作用:用于将RDD中的元素序列化成对象,存储到文件中。
2.6.5.12 countByKey()
作用:针对(K,V)类型的RDD,返回一个(K,Int)的map,表示每一个key对应的元素个数。
2.6.5.12 foreach(func)
作用:在数据集的每个元素上,运行函数func进行更新
有executor的基本上都要经过网络传输,所以只有driver过程的处理一定是比较快的
2.6.6 算子总结
在spark中,RDD被表示为对象,通过对象上的方法调用来对RDD进行转换。经过一系列的transformations定义RDD之后,就可以调用action触发RDD的计算,action可以是向应用程序返回结果(count,collect等),或者是向存储系统保存数据(saveAsTextFile等)。在spark中,只有遇到action,才会执行RDD的计算(即延迟计算),这样在运行时可以通过管道的方式传输多个转换。
要使用spark,开发者需要编写一个Driver程序(也就是创建上下文对象的那个类),它被提交到集群以调度运行worker(只有standalone才有worker)。
算子的一些tips:
-
需要提取
val listRDD: RDD[Int] = sc.makeRDD(List(1,2,3,4))
的最大值,因为是分布式存储的,所以不能直接使用list取最大值的函数来处理。这时候就可以使用glom来把数据放到一个分区中,并且变成一个array数组,处理起来就容易很多。 -
coalesce的合并过程是不确定的,例如:10个分区合并成2个,并不是前五个合并成一个,后五个合并成1个,这个过程是不确定的。而且容易造成数据倾斜。
-
coalesce重新分区,可以选择是否进行shuffle的过程,由参数
shuffle:Boolean=false/true
决定。 -
repartition底层调用的就是coalesce,只是把shuffle的默认值改成了true而已。
2.7 Driver和Executor
- 所有RDD算子的计算功能都是由executor来做的
- 当RDD算子内部用到了Driver中定义的变量或者其他什么对象,那么很可能就要进行网络传输,因为Driver和executor不一定在一台机子上的,这就需要这个变量(或者其他)一定是要能序列化的。
2.8 Partitions和Tasks
- 最基本的原则是:一个分区一个任务,一个任务会被分配到不同的executor上去
- 对于不要shuffle的算子来说,没有读和写的过程,那么在同一个分区中,从前一个RDD到后一个RDD一个task就可以完成。如果是需要shuffle的算子来说,需要读写,那么前后之间的RDD就不能使用一个task,需要分别在读和写的过程中启动新的task,所以task的数量是不要shuffle的算子的两倍。
2.9 Task执行序列化的问题
对于新建的自定义类,需要对其进行序列化,才能传递到executor端。不然传递方法和属性的时候都会报错。
同样功能的两个方法:
//方法1
def getMatche2(rdd: RDD[String]): RDD[String] = {
rdd.filter(x => x.contains(query))
}
//方法2
//过滤出包含字符串的RDD
def getMatche2(rdd: RDD[String]): RDD[String] = {
val query_ : String = this.query//将类变量赋值给局部变量
rdd.filter(x => x.contains(query_))
}
如果都不序列化,那么方法二是不报错的,方法1会报错。这其中要搞清楚哪部分是在Driver中执行的,哪部分是在executor中执行的。方法1直接把query这个对象传递到算子里面,等于直接从Driver传递到executor,所以需要query这个类有序列化。但是方法2是把query赋值给String类型的val,再传递到executor中,String类型本来就是可以直接传递的,不需要序列化。所以不会报错
2.10 RDD的依赖
窄依赖和宽依赖
- 每一个父RDD的partition最多被子RDD的一个partition使用。只要不打乱重组即可,一个分区的数据还在一块。
- 除了窄依赖都叫做宽依赖,多个子RDD的partition会依赖同一个父RDD的Partition,会引起shuffle。
DAG图形
- DAG叫做有向无环图,RDD的转换就形成了DAG,不能产生环状依赖。用于划分阶段,一个阶段没有完成是不能进行下一个阶段的。一个shuffle的前后就是两个阶段,需要等前面的处理完写入到磁盘中去,才能到下一个阶段进行操作 。没有shuffle的都在同一个阶段中进行。
2.11任务划分
RDD任务切分中间分为:Application、Job、Stage和Task
1)Application:初始化一个SparkContext即生成一个Application
2)Job:一个Action算子就会生成一个Job
3)Stage:根据RDD之间的依赖关系的不同将Job划分成不同的Stage,遇到一个宽依赖则划分一个Stage。
-
Application->Job->Stage-> Task每一层都是1对n的关系。
-
整个job的阶段=1+shuffle的个数,1就是
ResultStage
,即整个作业当成一个完整的阶段 -
这样计算shuffle的个数,也就是stage的个数
-
分区和task之间的关系,一个分区对应一个task
2.12 RDD缓存
RDD通过persist方法或cache方法可以将前面的计算结果缓存,默认情况下 persist() 会把数据以序列化的形式缓存在 JVM 的堆空间中。
但是并不是这两个方法被调用时立即缓存,而是触发后面的action时,该RDD将会被缓存在计算节点的内存中,并供后面重用。
总的来说,缓存机制会记住RDD的整个流程中比较重要(靠后)的部分,如果出错了需要重新计算,可以直接从这里开始算起,就不用从头再来一遍了。 -
cache()底层实现就是无参的persist(),persist的是可以传参数的。
缓存有可能丢失,或者存储存储于内存的数据由于内存不足而被删除,RDD的缓存容错机制保证了即使缓存丢失也能保证计算的正确执行。通过基于RDD的一系列转换,丢失的数据会被重算,由于RDD的各个Partition是相对独立的,因此只需要计算丢失的部分即可,并不需要重算全部Partition。
- 例子:
给字符串增加时间戳:
无cache
val rdd = sc.makeRDD(Array("us"))
val nocache =rdd.map(_.toString+System.currentTimeMillis)
nocache.collect
nocache.collect
nocache.collect
输出:
Array[String] = Array(us1538978275359) Array[String] = Array(us1538978282416) Array[String] = Array(us1538978283199)
有cache的情况
val cache = rdd.map(_.toString+System.currentTimeMillis).cache
cache.collect
cache.collect
cache.collect
输出:
Array[String] = Array(us1538978435705)
Array[String] = Array(us1538978435705)
Array[String] = Array(us1538978435705)
2.13 CheckPoint缓存
Spark中对于数据的保存除了持久化操作之外,还提供了一种检查点的机制,检查点(本质是通过将RDD写入Disk做检查点)是为了通过lineage做容错的辅助,lineage过长会造成容错成本过高,这样就不如在中间阶段做检查点容错,如果之后有节点出现问题而丢失分区,从做检查点的RDD开始重做Lineage,就会减少开销。检查点通过将数据写入到HDFS文件系统实现了RDD的检查点功能。
为当前RDD设置检查点。该函数将会创建一个二进制的文件,并存储到checkpoint目录中,该目录是用SparkContext.setCheckpointDir()设置的。在checkpoint的过程中,该RDD的所有依赖于父RDD中的信息将全部被移除。对RDD进行checkpoint操作并不会马上被执行,必须执行Action操作才能触发。
- 检查点的使用,除了申明语句,还需要设定一下检查点的保存目录
sc.setCheckpointDir("hdfs://hadoop102:9000/checkpoint")
。
在需要的地方:Rdd.checkpoint