• SparkCore2


    二、RDD编程

    2.5 RDD中的函数传递

    在实际开发中我们往往需要自己定义一些对于RDD的操作,那么此时需要主要的是,初始化工作是在Driver端进行的,而实际运行程序是在Executor端进行的,这就涉及到了跨进程通信,是需要序列化的。下面我们看几个例子:

    2.5.1 传递一个方法

    1.创建一个类

    class Search(s:String){

    //过滤出包含字符串的数据

      def isMatch(s: String): Boolean = {

        s.contains(query)

      }

     

    //过滤出包含字符串的RDD

      def getMatch1 (rdd: RDD[String]): RDD[String] = {

        rdd.filter(isMatch)

      }

     

      //过滤出包含字符串的RDD

      def getMatche2(rdd: RDD[String]): RDD[String] = {

        rdd.filter(x => x.contains(query))

      }

     

    }

    2.创建Spark主程序

    object SeriTest {

     

      def main(args: Array[String]): Unit = {

     

        //1.初始化配置信息及SparkContext

        val sparkConf: SparkConf = new SparkConf().setAppName("WordCount").setMaster("local[*]")

        val sc = new SparkContext(sparkConf)

     

    //2.创建一个RDD

        val rdd: RDD[String] = sc.parallelize(Array("hadoop", "spark", "hive", "atguigu"))

     

    //3.创建一个Search对象

        val search = new Search()

     

    //4.运用第一个过滤函数并打印结果

        val match1: RDD[String] = search.getMatche1(rdd)

        match1.collect().foreach(println)

        }

    }

    3.运行程序

    Exception in thread "main" org.apache.spark.SparkException: Task not serializable

        at org.apache.spark.util.ClosureCleaner$.ensureSerializable(ClosureCleaner.scala:298)

        at org.apache.spark.util.ClosureCleaner$.org$apache$spark$util$ClosureCleaner$$clean(ClosureCleaner.scala:288)

        at org.apache.spark.util.ClosureCleaner$.clean(ClosureCleaner.scala:108)

        at org.apache.spark.SparkContext.clean(SparkContext.scala:2101)

        at org.apache.spark.rdd.RDD$$anonfun$filter$1.apply(RDD.scala:387)

        at org.apache.spark.rdd.RDD$$anonfun$filter$1.apply(RDD.scala:386)

        at org.apache.spark.rdd.RDDOperationScope$.withScope(RDDOperationScope.scala:151)

        at org.apache.spark.rdd.RDDOperationScope$.withScope(RDDOperationScope.scala:112)

        at org.apache.spark.rdd.RDD.withScope(RDD.scala:362)

        at org.apache.spark.rdd.RDD.filter(RDD.scala:386)

        at com.atguigu.Search.getMatche1(SeriTest.scala:39)

        at com.atguigu.SeriTest$.main(SeriTest.scala:18)

        at com.atguigu.SeriTest.main(SeriTest.scala)

    Caused by: java.io.NotSerializableException: com.atguigu.Search

    4.问题说明

    //过滤出包含字符串的RDD

      def getMatch1 (rdd: RDD[String]): RDD[String] = {

        rdd.filter(isMatch)

      }

    在这个方法中所调用的方法isMatch()是定义在Search这个类中的,实际上调用的是this. isMatch()this表示Search这个类的对象,程序在运行过程中需要将Search对象序列化以后传递到Executor端。

    5.解决方案

    使类继承scala.Serializable即可。

    class Search() extends Serializable{...}

    2.5.2 传递一个属性

    1.创建Spark主程序

    object TransmitTest {

     

      def main(args: Array[String]): Unit = {

     

        //1.初始化配置信息及SparkContext

        val sparkConf: SparkConf = new SparkConf().setAppName("WordCount").setMaster("local[*]")

        val sc = new SparkContext(sparkConf)

     

    //2.创建一个RDD

        val rdd: RDD[String] = sc.parallelize(Array("hadoop", "spark", "hive", "atguigu"))

     

    //3.创建一个Search对象

        val search = new Search()

     

    //4.运用第一个过滤函数并打印结果

        val match1: RDD[String] = search.getMatche2(rdd)

        match1.collect().foreach(println)

        }

    }

    2.运行程序

    Exception in thread "main" org.apache.spark.SparkException: Task not serializable

        at org.apache.spark.util.ClosureCleaner$.ensureSerializable(ClosureCleaner.scala:298)

        at org.apache.spark.util.ClosureCleaner$.org$apache$spark$util$ClosureCleaner$$clean(ClosureCleaner.scala:288)

        at org.apache.spark.util.ClosureCleaner$.clean(ClosureCleaner.scala:108)

        at org.apache.spark.SparkContext.clean(SparkContext.scala:2101)

        at org.apache.spark.rdd.RDD$$anonfun$filter$1.apply(RDD.scala:387)

        at org.apache.spark.rdd.RDD$$anonfun$filter$1.apply(RDD.scala:386)

        at org.apache.spark.rdd.RDDOperationScope$.withScope(RDDOperationScope.scala:151)

        at org.apache.spark.rdd.RDDOperationScope$.withScope(RDDOperationScope.scala:112)

        at org.apache.spark.rdd.RDD.withScope(RDD.scala:362)

        at org.apache.spark.rdd.RDD.filter(RDD.scala:386)

        at com.atguigu.Search.getMatche1(SeriTest.scala:39)

        at com.atguigu.SeriTest$.main(SeriTest.scala:18)

        at com.atguigu.SeriTest.main(SeriTest.scala)

    Caused by: java.io.NotSerializableException: com.atguigu.Search

    3.问题说明

      //过滤出包含字符串的RDD

      def getMatche2(rdd: RDD[String]): RDD[String] = {

        rdd.filter(x => x.contains(query))

      }

    在这个方法中所调用的方法query是定义在Search这个类中的字段,实际上调用的是this. querythis表示Search这个类的对象,程序在运行过程中需要将Search对象序列化以后传递到Executor端。

    4.解决方案

    1)使类继承scala.Serializable即可。

    class Search() extends Serializable{...}

    2)将类变量query赋值给局部变量

    修改getMatche2为

      //过滤出包含字符串的RDD

      def getMatche2(rdd: RDD[String]): RDD[String] = {

        val query_ : String = this.query//将类变量赋值给局部变量

        rdd.filter(x => x.contains(query_))

      }

    2.6 RDD依赖关系

    2.6.1 Lineage

    RDD只支持粗粒度转换,即在大量记录上执行的单个操作。将创建RDD的一系列Lineage(血统)记录下来,以便恢复丢失的分区。RDDLineage会记录RDD的元数据信息和转换行为,当该RDD的部分分区数据丢失时,它可以根据这些信息来重新运算和恢复丢失的数据分区。

    1)读取一个HDFS文件并将其中内容映射成一个个元组

    scala> val wordAndOne = sc.textFile("/fruit.tsv").flatMap(_.split(" ")).map((_,1))

    wordAndOne: org.apache.spark.rdd.RDD[(String, Int)] = MapPartitionsRDD[22] at map at <console>:24

    2)统计每一种key对应的个数

    scala> val wordAndCount = wordAndOne.reduceByKey(_+_)

    wordAndCount: org.apache.spark.rdd.RDD[(String, Int)] = ShuffledRDD[23] at reduceByKey at <console>:26

    (3)查看“wordAndOne”的Lineage

    scala> wordAndOne.toDebugString

    res5: String =

    (2) MapPartitionsRDD[22] at map at <console>:24 []

     |  MapPartitionsRDD[21] at flatMap at <console>:24 []

     |  /fruit.tsv MapPartitionsRDD[20] at textFile at <console>:24 []

     |  /fruit.tsv HadoopRDD[19] at textFile at <console>:24 []

    4)查看“wordAndCount”的Lineage

    scala> wordAndCount.toDebugString

    res6: String =

    (2) ShuffledRDD[23] at reduceByKey at <console>:26 []

     +-(2) MapPartitionsRDD[22] at map at <console>:24 []

        |  MapPartitionsRDD[21] at flatMap at <console>:24 []

        |  /fruit.tsv MapPartitionsRDD[20] at textFile at <console>:24 []

        |  /fruit.tsv HadoopRDD[19] at textFile at <console>:24 []

    5)查看“wordAndOne”的依赖类型

    scala> wordAndOne.dependencies

    res7: Seq[org.apache.spark.Dependency[_]] = List(org.apache.spark.OneToOneDependency@5d5db92b)

    (6)查看“wordAndCount”的依赖类型

    scala> wordAndCount.dependencies

    res8: Seq[org.apache.spark.Dependency[_]] = List(org.apache.spark.ShuffleDependency@63f3e6a8)

    注意:RDD和它依赖的父RDDs)的关系有两种不同的类型,即窄依赖(narrow dependency)和宽依赖(wide dependency)。

    2.6.2 窄依赖

    窄依赖指的是每一个父RDDPartition最多被子RDD的一个Partition使用,窄依赖我们形象的比喻为独生子女

    2.6.3 宽依赖

    宽依赖指的是多个子RDDPartition会依赖同一个父RDDPartition,会引起shuffle,总结:宽依赖我们形象的比喻为超生

    2.6.4 DAG

    DAG(Directed Acyclic Graph)叫做有向无环图,原始的RDD通过一系列的转换就就形成了DAG,根据RDD之间的依赖关系的不同将DAG划分成不同的Stage,对于窄依赖,partition的转换处理在Stage中完成计算。对于宽依赖,由于有Shuffle的存在,只能在parent RDD处理完成后,才能开始接下来的计算,因此宽依赖是划分Stage的依据

    2.6.5 任务划分(面试重点)

    RDD任务切分中间分为:ApplicationJobStageTask

    1Application:初始化一个SparkContext即生成一个Application

    2Job:一个Action算子就会生成一个Job

    3Stage:根据RDD之间的依赖关系的不同将Job划分成不同的Stage,遇到一个宽依赖则划分一个Stage

     

    4TaskStage是一个TaskSet,将Stage划分的结果发送到不同的Executor执行即为一个Task

    注意:Application->Job->Stage-> Task每一层都是1n的关系。

    2.7 RDD缓存

    RDD通过persist方法或cache方法可以将前面的计算结果缓存,默认情况下 persist() 会把数据以序列化的形式缓存在 JVM 的堆空间中。

    但是并不是这两个方法被调用时立即缓存,而是触发后面的action时,该RDD将会被缓存在计算节点的内存中,并供后面重用。

     

    通过查看源码发现cache最终也是调用了persist方法,默认的存储级别都是仅在内存存储一份,Spark的存储级别还有好多种,存储级别在object StorageLevel中定义的。

     

    在存储级别的末尾加上“_2”来把持久化数据存为两份 

    缓存有可能丢失,或者存储存储于内存的数据由于内存不足而被删除,RDD的缓存容错机制保证了即使缓存丢失也能保证计算的正确执行。通过基于RDD的一系列转换,丢失的数据会被重算,由于RDD的各个Partition是相对独立的,因此只需要计算丢失的部分即可,并不需要重算全部Partition

    1)创建一个RDD

    scala> val rdd = sc.makeRDD(Array("atguigu"))

    rdd: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[19] at makeRDD at <console>:25

    2)将RDD转换为携带当前时间戳不做缓存

    scala> val nocache = rdd.map(_.toString+System.currentTimeMillis)

    nocache: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[20] at map at <console>:27

    (3)多次打印结果

    scala> nocache.collect

    res0: Array[String] = Array(atguigu1538978275359) 

    scala> nocache.collect

    res1: Array[String] = Array(atguigu1538978282416)

    scala> nocache.collect

    res2: Array[String] = Array(atguigu1538978283199)

    (4)将RDD转换为携带当前时间戳并做缓存

    scala> val cache =  rdd.map(_.toString+System.currentTimeMillis).cache

    cache: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[21] at map at <console>:27

    5)多次打印做了缓存的结果

    scala> cache.collect

    res3: Array[String] = Array(atguigu1538978435705)                                   

    scala> cache.collect

    res4: Array[String] = Array(atguigu1538978435705)

    scala> cache.collect

    res5: Array[String] = Array(atguigu1538978435705)

    2.8 RDD CheckPoint

    Spark中对于数据的保存除了持久化操作之外,还提供了一种检查点的机制,检查点(本质是通过将RDD写入Disk做检查点)是为了通过lineage做容错的辅助,lineage过长会造成容错成本过高,这样就不如在中间阶段做检查点容错,如果之后有节点出现问题而丢失分区,从做检查点的RDD开始重做Lineage,就会减少开销。检查点通过将数据写入到HDFS文件系统实现了RDD的检查点功能。

    为当前RDD设置检查点。该函数将会创建一个二进制的文件,并存储到checkpoint目录中,该目录是用SparkContext.setCheckpointDir()设置的。在checkpoint的过程中,该RDD的所有依赖于父RDD中的信息将全部被移除。对RDD进行checkpoint操作并不会马上被执行,必须执行Action操作才能触发。

    案例实操:

    1)设置检查点

    scala> sc.setCheckpointDir("hdfs://hadoop102:9000/checkpoint")

    2)创建一个RDD

    scala> val rdd = sc.parallelize(Array("atguigu"))

    rdd: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[14] at parallelize at <console>:24

    3)将RDD转换为携带当前时间戳并做checkpoint

    scala> val ch = rdd.map(_+System.currentTimeMillis)

    ch: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[16] at map at <console>:26

    scala> ch.checkpoint

    4)多次打印结果

    scala> ch.collect

    res55: Array[String] = Array(atguigu1538981860336)

    scala> ch.collect

    res56: Array[String] = Array(atguigu1538981860504)

    scala> ch.collect

    res57: Array[String] = Array(atguigu1538981860504)

    scala> ch.collect

    res58: Array[String] = Array(atguigu1538981860504)

    三、键值对RDD数据分区器

    Spark目前支持Hash分区和Range分区,用户也可以自定义分区,Hash分区为当前的默认分区,Spark中分区器直接决定了RDD中分区的个数、RDD中每条数据经过Shuffle过程属于哪个分区和Reduce的个数

    注意:

    (1)只有Key-Value类型的RDD才有分区器的,非Key-Value类型的RDD分区器的值是None
    (2)每个RDD的分区ID范围:0~numPartitions-1,决定这个值是属于那个分区的。

    3.1 获取RDD分区

    可以通过使用RDDpartitioner 属性来获取 RDD 的分区方式。它会返回一个 scala.Option 对象, 通过get方法获取其中的值。相关源码如下:

    def getPartition(key: Any): Int = key match {
      case null => 0
      case _ => Utils.nonNegativeMod(key.hashCode, numPartitions)
    }

    def nonNegativeMod(x: Int, mod: Int): Int = {
      val rawMod = x % mod
      rawMod + (if (rawMod < 0) mod else 0)
    }

    1)创建一个pairRDD

    scala> val pairs = sc.parallelize(List((1,1),(2,2),(3,3)))

    pairs: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[3] at parallelize at <console>:24

    2)查看RDD的分区器

    scala> pairs.partitioner

    res1: Option[org.apache.spark.Partitioner] = None

    3)导入HashPartitioner

    scala> import org.apache.spark.HashPartitioner

    import org.apache.spark.HashPartitioner

    (4)使用HashPartitionerRDD进行重新分区

    scala> val partitioned = pairs.partitionBy(new HashPartitioner(2))

    partitioned: org.apache.spark.rdd.RDD[(Int, Int)] = ShuffledRDD[4] at partitionBy at <console>:27

    5)查看重新分区后RDD的分区器

    scala> partitioned.partitioner

    res2: Option[org.apache.spark.Partitioner] = Some(org.apache.spark.HashPartitioner@2)

    3.2 Hash分区

    HashPartitioner分区的原理:对于给定的key,计算其hashCode,并除以分区的个数取余,如果余数小于0,则用余数+分区的个数(否则加0),最后返回的值就是这个key所属的分区ID

    使用Hash分区的实操

    scala> nopar.partitioner

    res20: Option[org.apache.spark.Partitioner] = None

    scala> val nopar = sc.parallelize(List((1,3),(1,2),(2,4),(2,3),(3,6),(3,8)),8)

    nopar: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[10] at parallelize at <console>:24

    scala>nopar.mapPartitionsWithIndex((index,iter)=>{ Iterator(index.toString+" : "+iter.mkString("|")) }).collect

    res0: Array[String] = Array("0 : ", 1 : (1,3), 2 : (1,2), 3 : (2,4), "4 : ", 5 : (2,3), 6 : (3,6), 7 : (3,8))

    scala> val hashpar = nopar.partitionBy(new org.apache.spark.HashPartitioner(7))

    hashpar: org.apache.spark.rdd.RDD[(Int, Int)] = ShuffledRDD[12] at partitionBy at <console>:26

    scala> hashpar.count

    res18: Long = 6

    scala> hashpar.partitioner

    res21: Option[org.apache.spark.Partitioner] = Some(org.apache.spark.HashPartitioner@7)

    scala> hashpar.mapPartitions(iter => Iterator(iter.length)).collect()

    res19: Array[Int] = Array(0, 3, 1, 2, 0, 0, 0)

    3.3 Ranger分区

    HashPartitioner分区弊端:可能导致每个分区中数据量的不均匀,极端情况下会导致某些分区拥有RDD的全部数据。

    RangePartitioner作用:将一定范围内的数映射到某一个分区内,尽量保证每个分区中数据量的均匀,而且分区与分区之间是有序的,一个分区中的元素肯定都是比另一个分区内的元素小或者大,但是分区内的元素是不能保证顺序的。简单的说就是将一定范围内的数映射到某一个分区内。实现过程为:

    第一步:先重整个RDD中抽取出样本数据,将样本数据排序,计算出每个分区的最大key值,形成一个Array[KEY]类型的数组变量rangeBounds

    第二步:判断keyrangeBounds中所处的范围,给出该key值在下一个RDD中的分区id下标;该分区器要求RDD中的KEY类型必须是可以排序的

    3.4 自定义分区

    要实现自定义的分区器,你需要继承 org.apache.spark.Partitioner 类并实现下面三个方法。

    1numPartitions: Int:返回创建出来的分区数。

    2getPartition(key: Any): Int:返回给定键的分区编号(0numPartitions-1)

    3equals():Java 判断相等性的标准方法。这个方法的实现非常重要,Spark 需要用这个方法来检查你的分区器对象是否和其他分区器实例相同,这样 Spark 才可以判断两个 RDD 的分区方式是否相同。

    需求:将相同后缀的数据写入相同的文件,通过将相同后缀的数据分区到相同的分区并保存输出来实现。

    1)创建一个pairRDD

    scala> val data = sc.parallelize(Array((1,1),(2,2),(3,3),(4,4),(5,5),(6,6)))

    data: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[3] at parallelize at <console>:24

    2)定义一个自定义分区类

    scala> :paste

    // Entering paste mode (ctrl-D to finish)

    class CustomerPartitioner(numParts:Int) extends org.apache.spark.Partitioner{

      //覆盖分区数

      override def numPartitions: Int = numParts

      //覆盖分区号获取函数

      override def getPartition(key: Any): Int = {

        val ckey: String = key.toString

        ckey.substring(ckey.length-1).toInt%numParts

      }

    }

    // Exiting paste mode, now interpreting.

    defined class CustomerPartitioner

    3)将RDD使用自定义的分区类进行重新分区

    scala> val par = data.partitionBy(new CustomerPartitioner(2))

    par: org.apache.spark.rdd.RDD[(Int, Int)] = ShuffledRDD[2] at partitionBy at <console>:27

    4)查看重新分区后的数据分布

    scala> par.mapPartitionsWithIndex((index,items)=>items.map((index,_))).collect

    res3: Array[(Int, (Int, Int))] = Array((0,(2,2)), (0,(4,4)), (0,(6,6)), (1,(1,1)), (1,(3,3)), (1,(5,5)))

    使用自定义的 Partitioner 是很容易的:只要把它传给 partitionBy() 方法即可。Spark 中有许多依赖于数据混洗的方法,比如 join() groupByKey(),它们也可以接收一个可选的 Partitioner 对象来控制输出数据的分区方式。

     

    四、数据读取与保存

     

    Spark的数据读取及数据保存可以从两个维度来作区分:文件格式以及文件系统。

     

    文件格式分为:Text文件、Json文件Csv文件、Sequence文件以及Object文件;

     

    文件系统分为:本地文件系统、HDFS、HBASE以及数据库。

     

    4.1 文件类数据读取与保存

     

    4.1.1 Text文件

     

    1)数据读取:textFile(String)

     

    scala> val hdfsFile = sc.textFile("hdfs://hadoop102:9000/fruit.txt")

     

    hdfsFile: org.apache.spark.rdd.RDD[String] = hdfs://hadoop102:9000/fruit.txt MapPartitionsRDD[21] at textFile at <console>:24

     

    2)数据保存: saveAsTextFile(String)

     

    scala> hdfsFile.saveAsTextFile("/fruitOut")

     

     

     

    4.1.2 Json文件

     

    如果JSON文件中每一行就是一个JSON记录,那么可以通过将JSON文件当做文本文件来读取,然后利用相关的JSON库对每一条数据进行JSON解析。

     

    注意:使用RDD读取JSON文件处理很复杂,同时SparkSQL集成了很好的处理JSON文件的方式,所以应用中多是采用SparkSQL处理JSON文件。

     

    1)导入解析json所需的包

     

    scala> import scala.util.parsing.json.JSON

     

    2)上传json文件到HDFS

     

    [atguigu@hadoop102 spark]$ hadoop fs -put ./examples/src/main/resources/people.json /

     

    3)读取文件

     

    scala> val json = sc.textFile("/people.json")

     

    json: org.apache.spark.rdd.RDD[String] = /people.json MapPartitionsRDD[8] at textFile at <console>:24

     

    4)解析json数据

     

    scala> val result  = json.map(JSON.parseFull)

     

    result: org.apache.spark.rdd.RDD[Option[Any]] = MapPartitionsRDD[10] at map at <console>:27

     

    5)打印

     

    scala> result.collect

     

    res11: Array[Option[Any]] = Array(Some(Map(name -> Michael)), Some(Map(name -> Andy, age -> 30.0)), Some(Map(name -> Justin, age -> 19.0)))

     

    4.1.3 Sequence文件

     

     SequenceFile文件是Hadoop用来存储二进制形式的key-value对而设计的一种平面文件(Flat File)。Spark 有专门用来读取 SequenceFile 的接口。在 SparkContext 中,可以调用 sequenceFile[ keyClass, valueClass](path)

     

    注意:SequenceFile文件只针对PairRDD

     

    1)创建一个RDD

     

    scala> val rdd = sc.parallelize(Array((1,2),(3,4),(5,6)))

     

    rdd: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[13] at parallelize at <console>:24

     

    2)将RDD保存为Sequence文件

     

    scala> rdd.saveAsSequenceFile("file:///opt/module/spark/seqFile")

     

    3)查看该文件

     

    [atguigu@hadoop102 seqFile]$ pwd

     

    /opt/module/spark/seqFile

     

    [atguigu@hadoop102 seqFile]$ ll

     

    总用量 8

     

    -rw-r--r-- 1 atguigu atguigu 108 10月  9 10:29 part-00000

     

    -rw-r--r-- 1 atguigu atguigu 124 10月  9 10:29 part-00001

     

    -rw-r--r-- 1 atguigu atguigu   0 10月  9 10:29 _SUCCESS

     

    [atguigu@hadoop102 seqFile]$ cat part-00000

     

    SEQ org.apache.hadoop.io.IntWritable org.apache.hadoop.io.IntWritableط

     

    4)读取Sequence文件

     

    scala> val seq = sc.sequenceFile[Int,Int]("file:///opt/module/spark/seqFile")

     

    seq: org.apache.spark.rdd.RDD[(Int, Int)] = MapPartitionsRDD[18] at sequenceFile at <console>:24

     

    5)打印读取后的Sequence文件

     

    scala> seq.collect

     

    res14: Array[(Int, Int)] = Array((1,2), (3,4), (5,6))

     

    4.1.4 对象文件

     

    对象文件是将对象序列化后保存的文件,采用Java的序列化机制。可以通过objectFile[k,v](path) 函数接收一个路径,读取对象文件,返回对应的 RDD也可以通过调用saveAsObjectFile() 实现对对象文件的输出。因为是序列化所以要指定类型。

     

    1)创建一个RDD

     

    scala> val rdd = sc.parallelize(Array(1,2,3,4))

     

    rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[19] at parallelize at <console>:24

     

    2)将RDD保存为Object文件

     

    scala> rdd.saveAsObjectFile("file:///opt/module/spark/objectFile")

     

    3)查看该文件

     

    [atguigu@hadoop102 objectFile]$ pwd

     

    /opt/module/spark/objectFile

     

    [atguigu@hadoop102 objectFile]$ ll

     

    总用量 8

     

    -rw-r--r-- 1 atguigu atguigu 142 10月  9 10:37 part-00000

     

    -rw-r--r-- 1 atguigu atguigu 142 10月  9 10:37 part-00001

     

    -rw-r--r-- 1 atguigu atguigu   0 10月  9 10:37 _SUCCESS

     

    [atguigu@hadoop102 objectFile]$ cat part-00000

     

    SEQ!org.apache.hadoop.io.NullWritable"org.apache.hadoop.io.BytesWritableW@`l

     

    4)读取Object文件

     

    scala> val objFile = sc.objectFile[Int]("file:///opt/module/spark/objectFile")

     

    objFile: org.apache.spark.rdd.RDD[Int] = MapPartitionsRDD[31] at objectFile at <console>:24

     

    5)打印读取后的Sequence文件

     

    scala> objFile.collect

     

    res19: Array[Int] = Array(1, 2, 3, 4)

     

    4.2 文件系统类数据读取与保存

     

    4.2.1 HDFS

     

    Spark的整个生态系统与Hadoop是完全兼容的,所以对于Hadoop所支持的文件类型或者数据库类型,Spark也同样支持.另外,由于HadoopAPI有新旧两个版本,所以Spark为了能够兼容Hadoop所有的版本,也提供了两套创建操作接口.对于外部存储创建操作而言,hadoopRDDnewHadoopRDD是最为抽象的两个函数接口,主要包含以下四个参数.

     

    1输入格式(InputFormat): 制定数据输入的类型,TextInputFormat,新旧两个版本所引用的版本分别是org.apache.hadoop.mapred.InputFormatorg.apache.hadoop.mapreduce.InputFormat(NewInputFormat)

     

    2)键类型: 指定[K,V]键值对中K的类型

     

    3)值类型: 指定[K,V]键值对中V的类型

     

    4)分区值: 指定由外部存储生成的RDDpartition数量的最小值,如果没有指定,系统会使用默认值defaultMinSplits

     

    注意:其他创建操作的API接口都是为了方便最终的Spark程序开发者而设置的,是这两个接口的高效实现版本.例如,对于textFile而言,只有path这个指定文件路径的参数,其他参数在系统内部指定了默认值

     

    1.Hadoop中以压缩形式存储的数据,不需要指定解压方式就能够进行读取,因为Hadoop本身有一个解压器会根据压缩文件的后缀推断解压算法进行解压.

     

    2.如果用SparkHadoop中读取某种类型的数据不知道怎么读取的时候,上网查找一个使用map-reduce的时候是怎么读取这种这种数据的,然后再将对应的读取方式改写成上面的hadoopRDDnewAPIHadoopRDD两个类就行了

     

    4.2.2 MySQL数据库连接

     

    支持通过Java JDBC访问关系型数据库。需要通过JdbcRDD进行,示例如下:

     

    1)添加依赖

     

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.27</version>
    </dependency>

     

    2Mysql读取:

     

    package com.atguigu

     

     

     

    import java.sql.DriverManager

     

     

     

    import org.apache.spark.rdd.JdbcRDD

     

    import org.apache.spark.{SparkConf, SparkContext}

     

     

     

    object MysqlRDD {

     

     

     

     def main(args: Array[String]): Unit = {

     

     

     

       //1.创建spark配置信息

     

       val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("JdbcRDD")

     

     

     

       //2.创建SparkContext

     

       val sc = new SparkContext(sparkConf)

     

     

     

       //3.定义连接mysql的参数

     

       val driver = "com.mysql.jdbc.Driver"

     

       val url = "jdbc:mysql://hadoop102:3306/rdd"

     

       val userName = "root"

     

       val passWd = "000000"

     

     

     

       //创建JdbcRDD

     

       val rdd = new JdbcRDD(sc, () => {

     

         Class.forName(driver)

     

         DriverManager.getConnection(url, userName, passWd)

     

       },

     

         "select * from `rddtable` where `id`>=?;",

     

         1,

     

         10,

     

         1,

     

         r => (r.getInt(1), r.getString(2))

     

       )

     

     

     

       //打印最后结果

     

       println(rdd.count())

     

       rdd.foreach(println)

     

     

     

       sc.stop()

     

     }

     

    }

     

    Mysql写入:

     

    def main(args: Array[String]) {
      val sparkConf = new SparkConf().setMaster("local[2]").setAppName("HBaseApp")
      val sc = new SparkContext(sparkConf)
      val data = sc.parallelize(List("Female", "Male","Female"))

      data.foreachPartition(insertData)
    }

    def insertData(iterator: Iterator[String]): Unit = {

     

    Class.forName ("com.mysql.jdbc.Driver").newInstance()
      val conn = java.sql.DriverManager.getConnection("jdbc:mysql://hadoop102:3306/rdd", "root", "000000")
      iterator.foreach(data => {
        val ps = conn.prepareStatement("insert into rddtable(name) values (?)")
        ps.setString(1, data)
        ps.executeUpdate()
      })
    }

     

    4.2.3 HBase数据库

     

    由于 org.apache.hadoop.hbase.mapreduce.TableInputFormat 类的实现,Spark 可以通过Hadoop输入格式访问HBase。这个输入格式会返回键值对数据,其中键的类型为org. apache.hadoop.hbase.io.ImmutableBytesWritable,而值的类型为org.apache.hadoop.hbase.client.

     

    Result

     

    1)添加依赖

     

    <dependency>

     

    <groupId>org.apache.hbase</groupId>

     

    <artifactId>hbase-server</artifactId>

     

    <version>1.3.1</version>

     

    </dependency>

     

     

     

    <dependency>

     

    <groupId>org.apache.hbase</groupId>

     

    <artifactId>hbase-client</artifactId>

     

    <version>1.3.1</version>

     

    </dependency>

     

    2)从HBase读取数据

     

    package com.atguigu

     

     

     

    import org.apache.hadoop.conf.Configuration

     

    import org.apache.hadoop.hbase.HBaseConfiguration

     

    import org.apache.hadoop.hbase.client.Result

     

    import org.apache.hadoop.hbase.io.ImmutableBytesWritable

     

    import org.apache.hadoop.hbase.mapreduce.TableInputFormat

     

    import org.apache.spark.rdd.RDD

     

    import org.apache.spark.{SparkConf, SparkContext}

     

    import org.apache.hadoop.hbase.util.Bytes

     

     

     

    object HBaseSpark {

     

     

     

      def main(args: Array[String]): Unit = {

     

     

     

        //创建spark配置信息

     

        val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("JdbcRDD")

     

     

     

        //创建SparkContext

     

        val sc = new SparkContext(sparkConf)

     

     

     

        //构建HBase配置信息

     

        val conf: Configuration = HBaseConfiguration.create()

     

        conf.set("hbase.zookeeper.quorum", "hadoop102,hadoop103,hadoop104")

     

        conf.set(TableInputFormat.INPUT_TABLE, "rddtable")

     

     

     

        //HBase读取数据形成RDD

     

        val hbaseRDD: RDD[(ImmutableBytesWritable, Result)] = sc.newAPIHadoopRDD(

     

          conf,

     

          classOf[TableInputFormat],

     

          classOf[ImmutableBytesWritable],

     

          classOf[Result])

     

     

     

        val count: Long = hbaseRDD.count()

     

        println(count)

     

     

     

        //hbaseRDD进行处理

     

        hbaseRDD.foreach {

     

          case (_, result) =>

     

            val key: String = Bytes.toString(result.getRow)

     

            val name: String = Bytes.toString(result.getValue(Bytes.toBytes("info"), Bytes.toBytes("name")))

     

            val color: String = Bytes.toString(result.getValue(Bytes.toBytes("info"), Bytes.toBytes("color")))

     

            println("RowKey:" + key + ",Name:" + name + ",Color:" + color)

     

        }

     

     

     

        //关闭连接

     

        sc.stop()

     

      }

     

     

     

    }

     

    3)往HBase写入

     

     

     

     

     

     

     

    def main(args: Array[String]) {

     

    //获取Spark配置信息并创建与spark的连接
      val sparkConf = new SparkConf().setMaster("local[*]").setAppName("HBaseApp")
      val sc = new SparkContext(sparkConf)

     

    //创建HBaseConf
      val conf = HBaseConfiguration.create()
      val jobConf = new JobConf(conf)
      jobConf.setOutputFormat(classOf[TableOutputFormat])
      jobConf.set(TableOutputFormat.OUTPUT_TABLE, "fruit_spark")

     

    //构建Hbase表描述器

     

      val fruitTable = TableName.valueOf("fruit_spark")
      val tableDescr = new HTableDescriptor(fruitTable)
      tableDescr.addFamily(new HColumnDescriptor("info".getBytes))

     

    //创建Hbase
      val admin = new HBaseAdmin(conf)
      if (admin.tableExists(fruitTable)) {
        admin.disableTable(fruitTable)
        admin.deleteTable(fruitTable)
      }
      admin.createTable(tableDescr)

     

    //定义往Hbase插入数据的方法
      def convert(triple: (Int, String, Int)) = {
        val put = new Put(Bytes.toBytes(triple._1))
        put.addImmutable(Bytes.toBytes("info"), Bytes.toBytes("name"), Bytes.toBytes(triple._2))
        put.addImmutable(Bytes.toBytes("info"), Bytes.toBytes("price"), Bytes.toBytes(triple._3))
        (new ImmutableBytesWritable, put)
      }

     

     

     

    //创建一个RDD
      val initialRDD = sc.parallelize(List((1,"apple",11), (2,"banana",12), (3,"pear",13)))

     

     

     

    //RDD内容写到HBase
      val localData = initialRDD.map(convert)

      localData.saveAsHadoopDataset(jobConf)
    }

    五、RDD编程进阶

     

    5.1 累加器

     

    累加器用来对信息进行聚合,通常在向 Spark传递函数时,比如使用 map() 函数或者用 filter() 传条件时,可以使用驱动器程序中定义的变量,但是集群中运行的每个任务都会得到这些变量的一份新的副本,更新这些副本的值也不会影响驱动器中的对应变量。如果我们想实现所有分片处理时更新共享变量的功能,那么累加器可以实现我们想要的效果。

     

    5.1.1 系统累加器

     

    针对一个输入的日志文件,如果我们想计算文件中所有空行的数量,我们可以编写以下程序:

     

    scala> val notice = sc.textFile("./NOTICE")

     

    notice: org.apache.spark.rdd.RDD[String] = ./NOTICE MapPartitionsRDD[40] at textFile at <console>:32

     

     

     

    scala> val blanklines = sc.accumulator(0)

     

    warning: there were two deprecation warnings; re-run with -deprecation for details

     

    blanklines: org.apache.spark.Accumulator[Int] = 0

     

     

     

    scala> val tmp = notice.flatMap(line => {

     

         |    if (line == "") {

     

         |       blanklines += 1

     

         |    }

     

         |    line.split(" ")

     

         | })

     

    tmp: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[41] at flatMap at <console>:36

     

     

     

    scala> tmp.count()

     

    res31: Long = 3213

     

     

     

    scala> blanklines.value

     

    res32: Int = 171

     

    累加器的用法如下所示。

     

    通过在驱动器中调用SparkContext.accumulator(initialValue)方法,创建出存有初始值的累加器。返回值为 org.apache.spark.Accumulator[T] 对象,其中 T 是初始值 initialValue 的类型。Spark闭包里的执行器代码可以使用累加器的 += 方法(在Java中是 add)增加累加器的值。 驱动器程序可以调用累加器的value属性(Java中使用value()setValue())来访问累加器的值。

     

    注意:工作节点上的任务不能访问累加器的值。从这些任务的角度来看,累加器是一个只写变量。

     

    对于要在行动操作中使用的累加器,Spark只会把每个任务对各累加器的修改应用一次。因此,如果想要一个无论在失败还是重复计算时都绝对可靠的累加器,我们必须把它放在 foreach() 这样的行动操作中。转化操作中累加器可能会发生不止一次更新

     

    5.1.2 自定义累加器

     

    自定义累加器类型的功能在1.X版本中就已经提供了,但是使用起来比较麻烦,在2.0版本后,累加器的易用性有了较大的改进,而且官方还提供了一个新的抽象类:AccumulatorV2来提供更加友好的自定义类型累加器的实现方式。实现自定义类型累加器需要继承AccumulatorV2并至少覆写下例中出现的方法,下面这个累加器可以用于在程序运行过程中收集一些文本类信息,最终以Set[String]的形式返回。1

     

    package com.atguigu.spark

    import org.apache.spark.util.AccumulatorV2
    import org.apache.spark.{SparkConf, SparkContext}
    import scala.collection.JavaConversions._

    class LogAccumulator extends org.apache.spark.util.AccumulatorV2[String, java.util.Set[String]] {
      private val _logArray: java.util.Set[String] = new java.util.HashSet[String]()

      override def isZero: Boolean = {
        _logArray.isEmpty
      }

      override def reset(): Unit = {
        _logArray.clear()
      }

      override def add(v: String): Unit = {
        _logArray.add(v)
      }

      override def merge(other: org.apache.spark.util.AccumulatorV2[String, java.util.Set[String]]): Unit = {
        other match {
          case o: LogAccumulator => _logArray.addAll(o.value)
        }

      }

      override def value: java.util.Set[String] = {
        java.util.Collections.unmodifiableSet(_logArray)
      }

      override def copy():org.apache.spark.util.AccumulatorV2[String, java.util.Set[String]] = {
        val newAcc = new LogAccumulator()
        _logArray.synchronized{
          newAcc._logArray.addAll(_logArray)
        }
        newAcc
      }
    }

    // 过滤掉带字母的
    object LogAccumulator {
      def main(args: Array[String]) {
        val conf=new SparkConf().setAppName("LogAccumulator")
        val sc=new SparkContext(conf)

        val accum = new LogAccumulator
        sc.register(accum, "logAccum")
        val sum = sc.parallelize(Array("1", "2a", "3", "4b", "5", "6", "7cd", "8", "9"), 2).filter(line => {
          val pattern = """^-?(d+)"""
          val flag = line.matches(pattern)
          if (!flag) {
            accum.add(line)
          }
          flag
        }).map(_.toInt).reduce(_ + _)

        println("sum: " + sum)
        for (v <- accum.value) print(v + "")
        println()
        sc.stop()
      }
    }

     

    5.2 广播变量(调优策略)

     

    广播变量用来高效分发较大的对象。向所有工作节点发送一个较大的只读值,以供一个或多个Spark操作使用。比如,如果你的应用需要向所有节点发送一个较大的只读查询表,甚至是机器学习算法中的一个很大的特征向量,广播变量用起来都很顺手。 在多个并行操作中使用同一个变量,但是 Spark会为每个任务分别发送。

     

    scala> val broadcastVar = sc.broadcast(Array(1, 2, 3))

     

    broadcastVar: org.apache.spark.broadcast.Broadcast[Array[Int]] = Broadcast(35)

     

     

     

    scala> broadcastVar.value

     

    res33: Array[Int] = Array(1, 2, 3)

     

    使用广播变量的过程如下:

     

    (1) 通过对一个类型 T 的对象调用 SparkContext.broadcast 创建出一个 Broadcast[T] 对象。 任何可序列化的类型都可以这么实现。

     

    (2) 通过 value 属性访问该对象的值(Java 中为 value() 方法)

     

    (3) 变量只会被发到各个节点一次,应作为只读值处理(修改这个值不会影响到别的节点)

    六、扩展

    6.1 RDD相关概念关系

    输入可能以多个文件的形式存储在HDFS上,每个File都包含了很多块,称为Block。当Spark读取这些文件作为输入时,会根据具体数据格式对应的InputFormat进行解析,一般是将若干个Block合并成一个输入分片,称为InputSplit,注意InputSplit不能跨越文件。随后将为这些输入分片生成具体的TaskInputSplitTask是一一对应的关系。随后这些具体的Task每个都会被分配到集群上的某个节点的某个Executor去执行。

    1) 每个节点可以起一个或多个Executor

    2) 每个Executor由若干core组成,每个Executor的每个core一次只能执行一个Task

    3) 每个Task执行的结果就是生成了目标RDD的一个partiton

    注意这里的core是虚拟的core而不是机器的物理CPU核,可以理解为就是Executor的一个工作线程。而 Task被执行的并发度 = Executor数目 * 每个Executor核数。至于partition的数目:

    1) 对于数据读入阶段,例如sc.textFile,输入文件被划分为多少InputSplit就会需要多少初始Task

    2) Map阶段partition数目保持不变。

    3) Reduce阶段,RDD的聚合会触发shuffle操作,聚合后的RDDpartition数目跟具体操作有关,例如repartition操作会聚合成指定分区数,还有一些算子是可配置的。

    RDD在计算的时候,每个分区都会起一个task,所以rdd的分区数目决定了总的的task数目。申请的计算节点(Executor)数目和每个计算节点核数,决定了你同一时刻可以并行执行的task

    比如的RDD100个分区,那么计算的时候就会生成100task,你的资源配置为10个计算节点,每个两2个核,同一时刻可以并行的task数目为20,计算这个RDD就需要5个轮次。如果计算资源不变,你有101task的话,就需要6个轮次,在最后一轮中,只有一个task在执行,其余核都在空转。如果资源不变,你的RDD只有2个分区,那么同一时刻只有2task运行,其余18个核空转,造成资源浪费。这就是在spark调优中,增大RDD分区数目,增大任务并行度的做法。

     

  • 相关阅读:
    生成日期列表的函数.sql
    Archlinux下启用Thinkpad功能键
    使用临时表进行编号重排的处理示例.sql
    行值动态变化的交叉报表处理示例.sql
    工作日处理函数(标准节假日).sql
    字符串在编号查询中的应用示例及常见问题.sql
    分段更新函数.sql
    TypeMembersToIL.cs
    排序规则在拼音处理中的应用.sql
    text与image字段转换处理示例.sql
  • 原文地址:https://www.cnblogs.com/qfdy123/p/13873620.html
Copyright © 2020-2023  润新知