Spark之RDD

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的广告编号
在这里插入图片描述
思路:

  1. 首先合并的结果应该是:省份-广告编号-点击次数;这样的格式
  2. 这要求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
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值