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端进行一个局部集合。能够减少数据量传输。
- map和mapPartition区别 和flatMap
- 在转换算子操作内部,直接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操作
- 宽依赖关系:父RDD中一个partition被子RDD中的多个partitoin所依赖,这种依赖关系就是宽依赖
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
- RDD与RDD之间依赖关系是如何构建的?
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端拉取数据和执行数据是并行操作。
- writer阶段
- 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回收资源