spark002

本文详细介绍了Spark中核心数据结构RDD的基本概念、属性及其在计算中的应用。包括RDD的宽窄依赖、缓存机制、任务调度流程等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Spark计算模型RDD

1.课程目标

  • 掌握RDD的原理
  • 熟练使用RDD的算子完成计算任务
  • 掌握RDD的宽窄依赖
  • 掌握RDD的缓存机制
  • 掌握划分stage
  • 掌握spark的任务调度流程

2.RDD概述

  • A Resilient Distributed Dataset (RDD):弹性分布式数据集合。并且RDD是spark最基础的数据集合抽象,RDD是不可变的、可分区的、进行并行计算的一个集合。
    
    关键词:
      数据集合:RDD是一个数据集合,就类似Scala中Array List等 RDD也具备map、flateMap等这类操作
      分布式:RDD是一个分布式的数据集合。RDD中数据是可以进行分片的,并且每个分片只包含了RDD的部分数据。RDD可以进行分布式计算
      弹  性:
          存储的弹性:RDD的数据可以在内存和磁盘之间自由切换
          可靠性弹性:RDD在进行计算的时候,如果计算失败,spark会基于该RDD进行一定次数的重试,默认情况下重试4次。RDD丢失数据以后,能够进行自动恢复(容错机制)。
          并行计算的弹性:RDD的分区数据可以根据需求进行自定义修改。RDD执行的并行度是高度弹性的
  • RDD的五大属性
    • A list of partitions
          概念:RDD是一个分区列表。每个分区包含了RDD的部分数据。
          通过textFile读取hdfs上文件,hdfs上文件块的个数跟RDD的partitions 是一一对应的
          RDD如何管理分区列表
              getPartitions: Array[Partition] : RDD是通过数组集合管理partitions
      
      A function for computing each split 
          概念:高级函数操作通过RDD的compute作用在RDD的每个分区之上
          f:是自定义函数
           def map[U: ClassTag](f: T => U): RDD[U] = withScope {
              val cleanF = sc.clean(f)
              //通过参数的形式传递给MapPartitionsRDD
           new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.map(cleanF))
           }
           //最后自定函数f通过compute作用在RDD的每个分片之上
           override def compute(split: Partition, context: TaskContext): Iterator[U] =
          f(context, split.index, firstParent[T].iterator(split, context))
      
      A list of dependencies on other RDDs
          概念:每一个RDD都有一个依赖列表
          以wordcont为例:wordCountRDD 依赖于wordPairRDD,wordPairRDD依赖于wordRDD,wordRDD依赖于lineRDD。
          通过一下代码:可以找到RDD依赖关系
              wordPairRDD.dependencies.foreach(d => println(d+" \t"+d.rdd))
           RDD依赖关系存在于
              dependencies: Seq[Dependency[_]] List
      
      a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned) 
          概念:针对key-value形式的RDD,Spark提供了两个分区器
              HashPartitioner
                  根据key值 求hash值 ,然后取模与分区数
                  针对key-value形式RDD,在进行shuffle操作的时候 默认分区器  HashPartitioner
                  HashPartitioner分区个数:
                      1.spark.default.parallelism
                      2.继承其所依赖的RDD中具有最大分片数的RDD的分片数
      
              RangePartition:
                  基于一定区间的分区器,主要用于排序。排序算法:水桶排序
      
      a list of preferred locations to compute each split on (e.g. block locations for
          an HDFS file)
          概念:HDFS file每一个块 默认3份,RDD在读取HDFS文件每一个blok块的时候,会获取该blok块的位置列表,在运行的时候,RDD会从该列表中选择一个最佳位置进行计算。大数据中数据本地化策略。移动数据不如移动计算的思想。
          RDD是如何定位到最佳位置? 
              preferredLocations(split: Partition): Seq[String]
          RDD在真正进行计算的时候,首先会通过preferredLocations该方法定位到每个分片所在的最佳位置,然后在进行任务分发。

3.RDD的数据来源

  • 可以通过scala集合创建RDD
    • val arr=Array(1,2,3,4)
      val list=List(1,2,3,4)
      valrdd:RDD[Int]=sc.parallelize(arr)
       sc.parallelize(1 to 10)
  • 通过读取文件创建RDD
    • sc.textFile("path")
  • 通过算子操作转换成RDD, map flatMap
    • map flatMap 都会产生新的RDD

4.RDD的算子操作

  • 转换算子(Transform操作)
    • 作用:主要是将一个RDD通过算子操作转换成一个新的RDD
    • 主要算子的区别
      • map和mapPartition区别 和flatMap
        • mapPartition作用于每一个分区相当于一个批量操作,map是作用于每一条数据的。mapPartition 一般情况情况下效率是比map要高的,但是再大数据量的情况下,mapPartition 很可能会引起内存溢出或者GC(垃圾回收)造成数据丢失
      • coalesce和repartition
        • coalesce 主要是用来减少分区的,并且该算子可以有shuffle 也可以没有shuffle ,默认是没有shuffle操作。rdd.fliter.coalesce
        • repartition 可以增加和减少分区个数, 有shuffle操作。
      • groupbyKey和reduceByKey
        • groupbyKey 分组操作,根据key值,将对应的value数据汇总到一个集合中。groupbyKey 当某个key值数据量过的时候,可能会引起OOM(内存溢出)以及GC
        • reduceByKey 聚合操作:根据key值,将对应的value值进行一个累加操作。reduceByKey 会首先在map端进行一个局部集合。能够减少数据量传输。
    • 在转换算子操作内部,直接new了一个新的RDD对象返回,转换算子没有真正意义上的执行,转换算子都是懒加载的。
  • Action算子
    • Action作用:用来触发Spark的job的执行 sc.runJob操作,使得转换算子真正意义上的执行
    • collect saveAsTextFile

5.编程实战

  • PV(PageView)
    • //创建sparkcontext对象
      
      val sc = new SparkContext(new SparkConf().setMaster("local[*]").setAppName("pv"))
      //读取数据
      val linesRDD = sc.textFile("C:\\Users\\tp\\Desktop\\深圳大数据7期\\spark\\spark_day02\\数据\\运营商日志\\access.log")
      val urlRDD = linesRDD.filter(_.split(" ").length>10).map(line => {
        line.split(" ")(10)
      })
      val urlPairRDD = urlRDD.map((_, 1))
      val pvCount = urlPairRDD.reduceByKey(_ + _).sortBy(t=>t._2)
      pvCount.collect().foreach(t => println(t._2 + "\t" + t._1))
  • UV 和TopN
    • package cn.itcast.spark
      
      import org.apache.spark.{SparkConf, SparkContext}
      
      object UV {
        def main(args: Array[String]): Unit = {
          //创建sparkcontext对象
      
          val sc = new SparkContext(new SparkConf().setMaster("local[*]").setAppName("UV"))
          //读取数据
          val linesRDD = sc.textFile("C:\\Users\\tp\\Desktop\\深圳大数据7期\\spark\\spark_day02\\数据\\运营商日志\\access.log")
          val ipRDD = linesRDD.map(line => {
            line.split(" ")(0)
          })
          val ipPairRDD = ipRDD.map((_, 1))
          val uvCount = ipPairRDD.reduceByKey(_ + _).sortBy(t=>t._2,false).take(10)
          uvCount.foreach(t => println(t._2 + "\t" + t._1))
          //topN :UV前10
        }
      }
  • IP热力图
    • 根据IP地址,然后查找IP地址所在的经度和纬度,然后在图上显示出不同区域的人数
    • package cn.itcast.spark
      
      import java.sql.DriverManager
      
      import org.apache.spark.rdd.RDD
      import org.apache.spark.{SparkConf, SparkContext}
      
      import scala.collection.mutable.ListBuffer
      
      object IPTable {
        //todo:将IP地址装换成long   192.168.200.150  这里的.必须使用特殊切分方式[.]
        def ipToLong(ip: String): Long = {
          val ipArray: Array[String] = ip.split("[.]")
          var ipNum = 0L
      
          for (i <- ipArray) {
            ipNum = i.toLong | ipNum << 8L
          }
          ipNum
        }
      
        //todo:通过二分查询,找到对应的ipNum在基站数据中的下标位置
        def binarySearch(ipNum: Long, valueArray: Array[(Long, Long, String, String)]): Int = {
          //todo:口诀:上下循环寻上下,左移右移寻中间
          var start = 0
          var end = valueArray.length - 1
      
          while (start <= end) {
            val middle = (start + end) / 2
      
            if (ipNum >= valueArray(middle)._1.toLong && ipNum <= valueArray(middle)._2.toLong) {
              return middle
            }
      
            if (ipNum < valueArray(middle)._1.toLong) {
              end = middle
            }
      
            if (ipNum > valueArray(middle)._2.toLong) {
              start = middle
            }
      
          }
          -1
        }
      
        def saveToMysql(iterator: Iterator[((String, String), Int)]): Unit = {
          //链接数据库
          Class.forName("com.mysql.jdbc.Driver")
          val connection = DriverManager.getConnection("jdbc:mysql://node-02:3306/test", "root", "root")
          val statement = connection.prepareStatement("insert into iplocation values (?,?,?)")
          while (iterator.hasNext) {
            val ((jingdu, weidu), count) = iterator.next()
            statement.setString(1, jingdu)
            statement.setString(2, weidu)
            statement.setInt(3, count)
            statement.execute()
          }
        }
      
        def main(args: Array[String]): Unit = {
      
          //创建sparkcontext对象
      
          val sc = new SparkContext(new SparkConf().setMaster("local[*]").setAppName("IPTable"))
      
          //第一步:加载日志信息
          val logRDD = sc.textFile("C:\\Users\\tp\\Desktop\\深圳大数据7期\\spark\\spark_day02\\数据\\服务器访问日志根据ip地址查找区域\\20090121000132.394251.http.format")
          //第二步:加载IP库信息
          val ipRDD = sc.textFile("C:\\Users\\tp\\Desktop\\深圳大数据7期\\spark\\spark_day02\\数据\\服务器访问日志根据ip地址查找区域\\ip.txt")
          //拿到IP库中想要的数据 iP起始值和经纬度信息
          val ipArray: Array[(Long, Long, String, String)] = ipRDD.map(ipLine => {
            val ipLineArray = ipLine.split("\\|")
            (ipLineArray(2).toLong, ipLineArray(3).toLong, ipLineArray(13), ipLineArray(14))
          }).collect()
          //广播变量
          val brodacastIpArray = sc.broadcast(ipArray)
          //从日志文件中将IP地址取出来 然后将IP转换成long类型数据
          val ipNumRDD: RDD[Long] = logRDD.map(line => line.split("\\|")(1)).map(ipToLong _)
      
          val locationRDD = ipNumRDD.mapPartitions(iterator => {
            val list = new ListBuffer[(String, String)]()
            while (iterator.hasNext) {
              val ip = iterator.next() //取出每一个ipNum
              val index = binarySearch(ip, brodacastIpArray.value) //返回的是ipArray的下标信息
              //取出经度和纬度信息
              list += ((ipArray(index)._3, ipArray(index)._4))
            }
            list.iterator
          })
          //汇通统计每个经度纬度出现的人数
          val locationCountRDD = locationRDD.map((_, 1)).reduceByKey(_ + _)
          //locationCountRDD.foreach(t => println(t._1 + ":" + t._2))
          //把数据保存到mysql之上
          locationCountRDD.foreachPartition(saveToMysql _)
        }
      }

6.RDD依赖关系

  • 两种依赖关系:
    • 窄依赖:父RDD中一个partition最多被子RDD中的一个partition所依赖,那么这种关系就是窄依赖关系
  • 窄依赖算子:map fliter flatMap mapPartitoin union 等等
    • 宽依赖关系:父RDD中一个partition被子RDD中的多个partitoin所依赖,这种依赖关系就是宽依赖
      • 宽依赖算子:groupByKey、reduceByKey等等
    • 宽窄依赖的划分:当前算子会不会产生shuffle操作
  • join算子既可以是宽依赖也可以是窄依赖。如果在join操作之前已经产生过shuffle操作,则此时join操作就是窄依赖,否则就是宽依赖关系

    • RDD与RDD之间依赖关系是如何构建的?
      • rdd在每次进行transform操作的时候都会将其本身的引用传递给新的rdd。也就是说新的rdd中包含了其父rdd的引用,最后将rdd的依赖关系 保存到list中
        
        def map[U: ClassTag](f: T => U): RDD[U] = withScope {
          val cleanF = sc.clean(f)
          new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.map(cleanF))
        }
    • RDD与RDD之间依赖关系
      • 宽依赖:ShuffleDependency
      • 窄依赖:OneToOneDependency、RangeDependency

8.RDD的血统机制

  • RDD与RDD之间存在依赖关系,并且依赖关系存在于 Seq[Dependency],spark在进行计算的时候,会根据最后一个rdd进行回溯,找到数据来源的RDD,才开始真正的去执行RDD中业务逻辑。
    由于RDD血统机制的存在,在计算过程中,如果RDD的数据丢失,则会根据其所以来的父RDD中恢复数据。
    窄依赖丢失数据:
      窄依赖是父RDD中一个parititon被子rdd中一个partition所依赖,所以如果子RDD中一个partition数  据丢失的话,则需要重算其所以来的父RDD中一个partition数据。
    宽依赖数据丢失:
      如果子RDD中一个分片数据丢失,则会重算其所依赖的父RDD中的所有分片数据。 此时容错会造成大量的冗余计算,所以在开发过程中尽量避免使用宽依赖算子,也就说尽量避免使用shuffle算子。

9.RDD缓存机制

  • RDD的缓存机制是spark中比较重要的容错机制,rdd.cache或者rdd.persist方法将rdd进行缓存,缓存以后的RDD可以被后续的job任务重用。rdd的缓存也是懒加载的,需要action算子触发才能够真正的执行。一般情况下缓存的数据能够提升后续的job任务10倍左右。 cache和persist实际上调用的是同一个方法,默认缓存级别是MEMORY_ONLY。
    
     缓存级别:
      //不缓存
      val NONE = new StorageLevel(false, false, false, false)
      //使用磁盘缓存
      val DISK_ONLY = new StorageLevel(true, false, false, false)
      //使用磁盘缓存 并且数据备份2份
      val DISK_ONLY_2 = new StorageLevel(true, false, false, false, 2)
      //使用内存缓存数据 默认缓存级别   ---常用
      val MEMORY_ONLY = new StorageLevel(false, true, false, true)
      //使用内存缓存数据 数据备份2份
      val MEMORY_ONLY_2 = new StorageLevel(false, true, false, true, 2)
      //使用内存缓存数据 然后进行序列化
      val MEMORY_ONLY_SER = new StorageLevel(false, true, false, false)
      //使用内存缓存数据 然后进行序列化 数据备份2份
      val MEMORY_ONLY_SER_2 = new StorageLevel(false, true, false, false, 2)
      //使用内存和磁盘缓存数据   ---常用
      val MEMORY_AND_DISK = new StorageLevel(true, true, false, true)
      //使用内存和磁盘缓存数据 数据备份2份
      val MEMORY_AND_DISK_2 = new StorageLevel(true, true, false, true, 2)
      //使用内存和磁盘缓存数据 数据序列化  --常用
      val MEMORY_AND_DISK_SER = new StorageLevel(true, true, false, false)
      //使用内存和磁盘缓存数据 数据序列化 数据备份2份
      val MEMORY_AND_DISK_SER_2 = new StorageLevel(true, true, false, false, 2)
      //使用堆外内存
      val OFF_HEAP = new StorageLevel(true, true, true, false, 1)
    
    RDD是分布式数据集合,缓存的时候也是分布式的,每个节点值缓存RDD当前节点上的分片数据
    
    RDD的缓存机制缺点:
      缺点:内存不安全,容易丢失数据,磁盘容易被误操作删除。

10.checkpoint机制

  • Spark提供另外一种容错机制,能够将RDD的数据缓存到分布式文件系统中HDFS,HDFS中天然的备份机制能够最大限度的保证数据安全。
  • 使用checkpoint机制
    • 首先设置缓存路径:sc.setCheckpointDir(hdfs路径)
    • RDD.checkpoint()
  • checkpoint 也是懒加载的,也是通过action算子触发的
  • checkpoint特点:
    • checkpoint也触发了job,该job是在action算子触发之后才开始的
    • 会打破rdd的血统机制,当checkpoint触发的job执行完毕以后,会删除当前RDD的依赖关系
  • 缓存机制以及checkpoint机制应用场景
    • 缓存机制一般使用在shuffle算子操作之后
    • 如果rdd在后续计算过程中被多次使用的时候,需要缓存
    • 如果计算链条特别长,最好再中间结果的时候缓存RDD
    • 如果读取数据耗时特别长的情况下,读取完数据后,最好将数据缓存
    • 如果某一个算子或者算法计算特别耗时,此时需要缓存计算结果数据
    • 一般在checkpoint之前 使用rdd缓存操作。

11.SparkJob任务执行过程

  • DAG的构建
    • DAG:有向无环图,根据spark应用程序对数据处理的过程,spark框架为每一个job构建一个DAG图
    • 在wordcount案例中DAG图:textFile->flatMap->map->reduceByKey
    • DAG是高层调度器DAGScheduler的调度依据
  • DAGScheduler 是高层调度器
    • 划分Stage,并且提交Stage,DAGScheduler 划分Stage的依据就是宽窄依赖,如果是宽依赖则创建新的stage,如果是窄依赖呢,则将当前操作加入到当前Stage
    • DAGScheduler 是如何划分Stage的,Stage0和Stage1会是什么样的东西?
      • action算子提交job,最终通过sc.runJob方法将dag图交给DAGScheduler,DAGScheduler.sumitJob方法,首先将Job封装到JobSubmitted对象中,然后将JobSubmitted提交到任务池中,然后调用DAGScheduler.handleJobSubmitted进行stage的划分。首先创建ResultStage,然后根据最后一个RDD进行回溯,在回溯过程中,首先获取到所有的宽依赖关系,然后根据宽依赖关系创建ShuffleMapStage,并将ShuffleMapStage放在List中返回给ResultStage。此时ResultStage中包含了其所以来的所有的Stage。最后将ResultStage 通过DAGScheduler.submitStage方法提交。在整个应用程序中ResultStage只存在一个,ShuffleMapStage存在多个的。Stage与Stage之间是串行操作
    • Stage划分Task流程
      • 根据stage根据RDD的partition,获取partitionId值,然后根据Partition的Id找到每个partition对应的位置信息列表Map[Int, Seq[TaskLocation]] 。然后根据partition划分Task,ShuffleMapStage划分成了ShuffleMapTask,ResultStage划分成了ResultTask,最后将划分好的Task封装到了Seq[Task[]],最后将Seq[Task[]] 序列封装在TaskSet对象中,提交个底层调度器TaskScheduler。
  • TaskScheduler如何进行工作?
    • TaskScheduler接受TaskSet对象,进而将TaskSet封装到TaskSetManager对象中,TaskSetManager是用来监控所有的Task的,将TaskSetManager存放到了公平竞争的队列中,通过SchedulerBackend对象将Task发送到Executor
  • Worker如何运行Task的?
    • Executor是一个进程,在启动过程中会启动一个Java线程池 newCacheThreadPool,Execuor接收到Task以后,会对Task进一步封装TaskRuner,将TaskRuner提交给java线程池,通过线程并发执行和线程复用的方式执行Task。Task在执行过程中如果执行失败,则会进行一定次数的重试,重试4次。如果Task执行速度过慢,Spark会根据其他task的执行速度,推测当前Task执行时间,如果Task执行时间超过推测出来的时间,Spark会在其他节点上重新启动一个新的Task,其中一个Task返回数据后,会终止掉另外一个Task,Task的推测执行机制。
  • Stage的重试机制
    • 如果Stage执行失败,也会进行一定次数的重试,默认也是4次,如果4次重试全部失败,则意味着真个Job执行失败。

12.Spark当中Shuffle机制

  • HashShuffleManager(弃用)
    • 会根据key进行shuffle操作,HashShuffleManager会产生大量的小文件 map端10个并行度 reduce端有20个并行度 小文件个数:10*20=200个, 基于HashShuffleManager合并机制,将一些小文件进行合并 CUP核数x reduce并行度,依然会产生大量的小文件 。小文件容易丢失数据、很容易引起OOM异常。
  • SortedShuffleManager

    • writer阶段
      • map端会将数据缓存到内存中,如果内存不足,则将数据溢写到磁盘,在溢写磁盘过程中,会对数据进行按照key值排序,排序完成会在磁盘上形成小文件,spark会启动守护线程,合并小文件,并形成两个文件一个是索引文件,一个数据文件
    • reader阶段:
      • 首先会根据文件所在的问题,创建连接获取文件数据,首先读取的索引文件,根据索引文件提供的位置信息,找到其所需要的数据,进行拉取。在reduce端拉取数据和执行数据是并行操作。
  • HashShuffleManager 在小数据量的情况下运行效率要高于SortedShuffleManager,但是在大数据量的情况下HashShuffleManager的性能很差。
  • byPass机制 (实际上就是hashshuffle) 在小数据量情况下自动开启 200 partition

13.整个Spark应用程序执行流程

  • spark-submit 提交应用程序,并且传递资源参数,进而初始化SparkContext对象。
  • SparkContext在初始化过程中会向master节点申请资源(对应第一步操作),master节点接收到消息后会向woker节点发送资源请求,worker接收到master消息后会向master汇报资源信息(对应的第二步操作),最后将资源信息汇报给SparkContext对象。
  • SparkContext会通过高层调度DAGScheduler划分Stage和Task,然后通过底层调度器TaskScheduler封装Task,最终将Task发送Worker(对应第三步操作)
  • worker会启动Executor进程,并且在该进程中启动java线程池接受Task,将Task进一步封装成TaskRunner,最终线程并发和线程复用的方式执行Task任务。
  • 当整个spark应用程序执行完成后,SparkContext通过sc.stop()会通知master回收资源

转载于:https://www.cnblogs.com/jeasonchen001/p/11192033.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值