spark core

RDD:弹性分布式数据集

RDD类似于java中的io流,RDD中不存储数据,当触发任务计算的时候拿到数据处理完成之后马上交给下一层RDD
    特性:
        不存储数据(RDD中只是封装了数据的处理逻辑)
        不可变(RDD是不可以改变的,想要改变,只能产生新的RDD,在新的RDD里封装逻辑)
        弹性:(存储、容错、计算、分片的弹性)
        分布式(存储在集群的不同节点)
        数据抽象(RDD是一个抽象类,需要子类实现)
    注意:
        所有的RDD算子操作(Transformation转换算子)都是在Executor端执行的,
        RDD算子之外的操作(action行动算子)都是在Driver端执行
        只有遇到行动算子,才会执行RDD的计算操作(延迟计算))
    特点:
        一组分区列表
        作用于每个分区的计算函数
        对于其他的RDD的依赖关系
        分区器
        优先位置

RDD编程:

在pom文件中添加spark-core的依赖和scala的编译插件

<dependencies>
    <dependency>
        <groupId>org.apache.spark</groupId>
        <artifactId>spark-core_2.12</artifactId>
        <version>3.0.0</version>
    </dependency>
</dependencies>

<build>
    <finalName>SparkCoreTest</finalName>
    <plugins>
        <plugin>
            <groupId>net.alchim31.maven</groupId>
            <artifactId>scala-maven-plugin</artifactId>
            <version>3.4.6</version>
            <executions>
                <execution>
                    <goals>
                        <goal>compile</goal>
                        <goal>testCompile</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

RDD创建:
       

 import org.apache.spark.{SparkConf, SparkContext}
  val sc = new SparkContext(new SparkConf().setMaster("local[4]").setAppName("test"))
  /**
    * RDD的创建方式
    *     1、通过本地集合创建
    *     2、通过读取文件创建
    *     3、通过其他RDD衍生
    */

  /**
    * 通过集合创建RDD 【工作中一般用于测试】
    *     sc.makeRDD(集合)
    *     sc.parallelize(集合)
    */
  @Test //@Test注解必须用在class中,标准的方法必须是无返回值的方法
  def createRddByLocalCollection(){

    new $01_RDDCreate

    val list = List(1,4,3,2,5)

    //val rdd = sc.makeRDD(list)
    val rdd = sc.parallelize(list)

    val arr = rdd.collect()

    println(arr.toList)
  }

  /**
    * 通过读取文件创建RDD
    */
  @Test
  def createRddByFile(): Unit ={
    //System.setProperty("HADOOP_USER_NAME","atguigu")
    val rdd = sc.textFile("datas/product.txt")
    //读取HDFS文件
    //val rdd = sc.textFile("hdfs://hadoop102:8020/datas/products.txt")
    println(rdd.collect().toList)
  }

  /**
    * 3、通过其他RDD衍生
    */
  @Test
  def createRddByRdd(): Unit ={
    val rdd = sc.textFile("datas/product.txt")

    val rdd2 = rdd.flatMap(_.split("\t"))

    println(rdd2.collect().toList)
  }

    RDD分区数:    
        根据本地集合创建的RDD的分区数:
            如果有设置numSlices参数,此时RDD的分区数 = 设置的numSlice的值
                    val rdd: RDD[Int] = sc.parallelize(List(1,3,6,5,2,7,9,10),8)        
            工作中通过设置spark.default.parallelism的值来设置默认分区数
                 val sc = new SparkContext(new SparkConf() /*.set("spark.default.parallelism","6")*/ .setMaster("local[4]").setAppName("test"))
            查看RDD的分区数:
                 rdd.getNumPartitions/rdd.partitions.length
        根据读取文件创建RDD的分区数:
            如果有设置minPartitions的值,RDD的分区数>= minPartitions的值
                 如果没有设置minPartitins的值,RDD的分区数>=math.min(defaultParallelism, 2)
            通过读取文件创建RDD的分区数最终由文件的切片数决定,一个切片一个分区
         由其他RDD衍生出的新RDD的分区数 :
             依赖的第一个父RDD的分区数

Transformation转换算子:


***    value值类型:


        map:一对一映射

        

@Test
  def map(): Unit ={

    val rdd = sc.parallelize(List(1,3,2,4,5,6,8,10))

    val rdd2 = rdd.map(x=> {
      println(s"${Thread.currentThread().getName} --${x}")
      x * 10
    })

    println(rdd2.collect().toList)
  }


        mapPatitions:(func:Iterator[RDD元素类型] => Iterator[B])(原RDD一个分区计算得到新RDD一个分区)
            有多少个分区就操作多少次
            应用场景:一般用于mysql、Redis、hbase等存储介质查询数据,此时如果直接将建立连接提到map方法外,但是由于connect没有序列化方法会报错,可以减少资源连接创建与销毁时间提高效率
***            :迭代器iterator只能用一次,使用后就失效
        map与mapPartitions的区别:
            1、函数针对的对象不同(map是每个分区中每个元素,mappartitions是针对每个分区)
            2、函数返回值不同:(map是返回新元素结果,mapPatitions是针对每个分区所有数据的迭代器操作,要求返回一个新的迭代器,新的迭代器是一个分区的所有数据,所以新RDD个数不一定等于原RDD元素个数)
            3、对象内存回收的时间不一样(map是针对元素操作,元素操作完成之后就内存回收了,而mapPartitions是对分区的迭代器,所以必须等到该迭代器中所有数据都操作完成才能整体回收。此时如果RDD一个分区数据量特别大,可能出现内存溢出,此时可以使用map代替)
        mapPartitionsWithIndex(Int,Iterator[RDD元素类型]=>Iterator[B])
         mapPartitions与mapParititonsWithIndex的区别:
                 mapPartitionWithIndex里面的函数相比mapPartitions里面的函数多了个分区号的参数
        
        flatMap:扁平化数据(func:RDD元素类型=>集合)

  @Test
  def flatMap(): Unit ={

    val rdd = sc.parallelize(List("hello java spark","spark hadoop flume","flume kafka spark","spark hadoop"))

    val rdd2 = rdd.flatMap(x=> x.split(" "))

    println(rdd2.collect().toList)
  }


        filter:过滤数据,返回值为true时过滤
        groupBy:
            有shuffle操作(判断条件:是否将各个分区的数据聚合,父RDD的数据被多个子RDD使用,只作用于有k-v键值对的数据)
        distinct:去重
            rdd1.distinct(rdd ) -> 有shuffle操作
        coalesce:改变分区数
            减少分区:rdd.coalesce(分区数) ->   (*没有shuffle操作,父RDD的数据直接写到一个子RDD中)
            增加分区:rdd.coalesce(分区数,true) ->(有shuffle操作,调用了hashpartition)
        repartition:重分区
            增加/减少分区:rdd.repartition() -> 都有shuffle操作
        repartition与coalesce区别:
            coalesce默认只能减少分区数,想增大需要shuffle=true,会产生shuffle(一般和filter搭配过滤减少分区数)
            repartition可增可减,但是都会有shuffle(一般用于增大分区数)
        sortBy(func:集合元素类型=>K):按照指定字段排序
            sortBy是按照函数的返回值对元素进行排序,有shuffle操作
            升序:rdd1.sortBy(rdd)
            降序:rdd1.sortBy(rdd,false)->设置ascending参数为false
    双value值类型:
        intersection:交集  rdd3 = rdd1.intersection(rdd2)(两次shuffle,rdd1数据到rdd3,rdd2到rdd3)
        subtract:差集(两次shuffle)
        union:并集(无shuffle)不去重
        zip:拉链(无shuffle)要求元素个数与分区数一致
    key-value键值对类型:
        partitionBy:按照key值重新分区
            
**        自定义分区:继承partitioner抽象类,重写numpartitions和getpartition方法
            class UserDefinedPartitioner(num:Int) extends Partitioner{
              //spark后续内部调用获取重分区的分区数
              override def numPartitions: Int = if(num<5) 5 else num
              //根据key获取分区号,后续shuffle的时候该key的数据放入分区号对应的分区中
              override def getPartition(key: Any): Int = key match {
                 case "aa" =>0
                    case "dd" =>1
                 case x =>
                      val r = x.hashCode() % num
                      if(r <0)   r+num
                      else        r

        groupByKey:按照key进行分组
            返回的元素类型是kv键值对,K是原RDD元素的key(此时k里是唯一一条数据),V是集合,里面是K对应的原RDD所有value值
        
***        reduceByKey(func:(value,value)=>value):按照key分组并且对key对应的value值聚合,相比于map和groupBy效率要高
            rdd1.reduceByKey((a, b) => a + b)
            
**        combineByKey:转换结构和分区间操作,针对相同k,将v合并成一个集合
            rdd.combineByKey(createCombiner,mergeValue,mergeCombiners)
            createCombiner:转换数据结构,对每个组第一个value值转换  Value值类型=>B类型
            mergeValue:分区内聚合combiner阶段    (B,value类型)=>B
            mergeCombiners:分区间聚合reducer阶段      (B,B)=>B
        
        foldByKey:和reduceByKey的区别就是聚合初始值为默认值0,reduce为第一个元素
            rdd1.foldByKey(默认值)((a,b)=>{a+b})
        aggregateByKey:和reduceByKey的区别就是聚合初始值为默认值(0,0),reduce为第一个元素
            rdd1.aggregateByKey(默认值)((a,b)=>{a+b})
        四种ByKey的区别:
            reduceByKey:combiner与reducer计算逻辑一样,用的同一个计算函数
            combineByKey:两个阶段计算逻辑可以自定义,可以不一致
        sortByKey:按照key排序,和sortBy原理一样
    案例处理:
        1、读取数据->2、是否过滤、列裁剪、去重

    action行动算子:


        collect:用来收集RDD每个分区数据并将数据用数组封装之后返回给Driver
            如果RDD所有分区数据量过大,collect数据是返回放在Driver内存中,内存默认为1G,所以,工作中Driver一般为5-10G,通过bin/spark-submit --driver-memory 10G设置。
        take:获取RDD前N个元素     rdd.take(N)
            take是首先从0号分区尝试获取N个元素,如果0号分区没有N个元素,会再启动一个job获取剩余的元素(可能启动两个job)
        countByKey:统计RDD中每个Key的个数    
**        foreach:对RDD每个元素遍历(func:RDD元素类型=>Unit)
            调用Scala中的foreach方法,多线程并行
***        foreachPartition:对分区的所有元素的迭代器处理
            如果不需要返回值,一般用于将数据保存在mysql/hbase/redis等位置。也可以用来减少连接创建和销毁的次数
        


 RDD序列化:

Spark算子里面的代码是在Executor中的task执行的,Spark算子外面的代码是在Driver执行的,如果spark算子使用了Driver定义的对象(类似于闭包)
        就必须要求Driver将该对象序列化之后传递给task才能使用
        spark序列化分为两种:
            java序列化:序列化将类的继承信息,全类名,属性名,属性类型,属性值的信息全部序列化
            Kry序列化:序列化只将类的全类名,属性名,属性类型,属性值序列化
            Kryo序列化要快十倍左右,但是spark默认使用java序列化,Kryo需要设置
***        配置Kryo序列化:(工作一般要配置)
            在创建sparkconf的时候设置spark默认的序列化方式:new SparkConf().set("spark.serializer","ora.apache.spark.serializer.KryoSerializer")
    

RDD依赖关系:

血统:指从第一个RDD到当前RDD的链条,使用toDebugString可以查看血统
        父子RDD的关系:
            通过dependencies查看
            有shuffle的称为宽依赖、没有shuffle的称为窄依赖
***        Application:应用,一个sparkContext称之为一个应用
            Job:任务(一般一个action算子产生一个job)
                stage:阶段(一个job中stage的个数 = job中shuffle分数 + 1),job中前面的stage先执行,后面的后执行
                    stage切分:根据最后一个RDD的依赖从后往前一次查找,一直找到第一个RDD为止,在查询的过程中遇到宽依赖则切分
                    task:task并行的前提是task之间不能存在依赖(不能存在shuffle),所以task有串行与并行两种模式,一个stage中task个数 = stage最后一个RDD的分区个数
    为什么RDD是惰性的
        RDD是迭代器不存数据,封装的是运算的逻辑,当collect的时候才一层层往前请求数据
      

RDD持久化:

        问题场景1、RDD在多个job中重复使用
            此时默认该RDD之前的处理步骤是每个job都会执行,数据重复处理影响效率
            解决方案:将RDD数据保存下来供其他job执行,则不用重复执行了
        2、当一个job依赖链条很长
            如果依赖料条太长,某个环节计算出错需要重新计算得到数据,浪费时间
            解决方案:将RDD数据保存,后续计算出错拿出保存数据计算结果,减少计算时间
        如何持久化:
            
            方案1:缓存
                保存位置:保存所在分区的内存或磁盘中( rdd.cache()或rdd.persist() ,两个方法区别:cache全部保存在内存中,persist根据设置的存储级别分别保存在内存或磁盘中(rdd.persist(StorageLevel.级别))
                persist常用存储级别:MEMORY——ONLY(只保存在磁盘,一般用于数据量小场景)、MEMORY_AND_DISK(一部分保存在内存,一部分保存在磁盘,一般用于大数据量场景)
                数据保存时机:在执行job的时候就保存
            方案2:checkpoint
                场景:缓存是将数据保存在服务器本地磁盘/内存中,如果服务器宕机数据丢失,后续job执行的时候需要根据RDD的关系重新执行得出数据
                    所以需要将数据保存到可靠的存储介质hdfs中,不会出现数据丢失的问题
                配置:1、设置checkpoint数据保存路径  ->  sc.setCheckpoint(path) 
                    2、保存rdd数据 ->  rdd.checkpoint()
                数据保存时机:在执行完job之后,另外单独启动一个job,计算checkpoint的到rdd的数据保存
                    checkpoint操作会单独触发一个job执行得到数据再保存,此时该checkpoint  rdd之前的数据处理会重复执行一次,所以为了避免重复执行,会先rdd.cache,在本地内存保存,然后再rdd.checkpoint,就可以直接读取本地缓存的数据
                    所以checkpoint一般是配合cache使用
            shuffle算子相当于rdd.persist(StorageLevel.DISK),shuffle会落盘,已经缓存一次了
 

RDD数据分区:

HashPartitioner
            分区规则:key.hashCode % 分区数
            弊端:可能导致每个分区数据量不均匀 
            rdd2 = rdd1.partitionBy(new HashPartitioner( 3 ))
        RangePartitioner( sortBy底层分区就是用的Rangepartitioner )
            分区规则:1、对RDD所有数据的key抽样,通过采样的结果确定分区数-1个key
                2、通过这些采样的key确定RDD的每个分区的边界
                3、后续拿到数据的key之后与分区边界对比,如果可以处于分区边界范围内则将数据放入该分区中
            rdd2 = rdd1.partitionBy( new Rangepartitioner( 3 ,rdd1 ) )

***    累加器:

应用场景:只能用于聚合场景并且聚合之后数据量不是特别大的场景
        好处  ->  使用累加器可以避免shuffle操作(shuffle少,速度快,避免写磁盘)
        原理:现在每个分区中对数据累加,然后将累加的结果发给Driver汇总
        默认累加器:
         

    val sc = new SparkContext(new SparkConf().setAppName("test").setMaster("local[4]"))
                val acc = sc.longAccumulator("acc_sum")
                val rdd = sc.parallelize(List(1, 2, 3, 4, 5, 6))
                rdd.foreach(x => acc.add(x))
                println(acc.value)


        
        自定义累加器:
            1、创建class继承AccumulatorV2 [IN,OUT]    
                IN:代表task中累加的元素类型
                OUT:代表driver汇总之后的最终结果类型
            2、重写抽象方法
                自定义add方法和merge方法
            3、使用自定义累加器:
                1、创建自定义累加器对象:val acc = new XXXX
                2、将创建的对象注册到sparkcontext中:sc.register(acc,"累加器名称“)
                3、将executor中的数据累加rdd.foreach(x=>acc.add(x))
                4、返回累加器结果:acc.value

***    广播变量:

    场景:1、spark行动算子使用到Driver数据    ->  工作中的RDD的分区数一般设置为任务总CPU个数的2-3倍,当task需要使用driver中的数据时,driver需要向所有task发送数据,占用内存过大。
            所以需要一个广播变量,将Driver的数据广播给Executor,后续task需要数据时从Executor获取使用即可,减小内存空间占用
            2、大表join小表  ->  好处:默认会产生shuffle操作,此时小表广播后可避免
        用法:1、广播driver数据   ->  val bc = sc.broadcast (数据)
            2、获取广播变量使用  ->  bc.value
        
                

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值