SparkCore
1.RDD的依赖关系
1.1RDD的依赖
RDD和它依赖的父RDD的关系有两种不同的类型,即窄依赖(narrow dependency)和宽依赖(wide dependency)。
●如何区分宽窄依赖
窄依赖:父RDD的一个分区只会被子RDD的一个分区依赖
宽依赖:父RDD的一个分区会被子RDD的多个分区依赖(涉及到shuffle)
●面试题:
子RDD的一个分区依赖多个父RDD是宽依赖还是窄依赖?
不能确定,也就是宽窄依赖的划分依据是父RDD的一个分区是否被子RDD的多个分区所依赖,是,就是宽依赖,或者从shuffle的角度去判断,有shuffle就是宽依赖
为什么要设计宽窄依赖
1.对于窄依赖
Spark可以并行计算
如果有一个分区数据丢失,只需要从父RDD的对应1个分区重新计算即可,不需要重新计算整个任务,提高容错。
2.对于宽依赖
是划分Stage的依据
血统
RDD的Lineage会记录RDD的元数据信息和转换行为,当该RDD的部分分区数据丢失时,它可以根据这些信息来重新运算和恢复丢失的数据分区。
spark任务组件
1-一个Application就是包含多个Job,调用一次action产生的就是一次job
2-一个Job也就是一个DAG包含有很多Stages
4-Stages是根据宽窄依赖划分的,出现宽依赖就可以将Stages进行划分,因为窄依赖可以在Stage内部完成
5-一个Stages包含有很多TaskSet
6-一个TaskSet就是一个RDD,一个RDD有多个分区,每个分区都是由Task线程执行
7-Task是Spark执行任务的最小线程的单元--Task(worker节点的Executor的Task线程中)
RDD的缓存
Spark速度非常快的原因之一,就是在不同操作中可以在内存中持久化或者缓存数据集。当持久化某个RDD后,每一个节点都将把计算分区结果保存在内存中,对此RDD或衍生出的RDD进行的其他动作中重用。这使得后续的动作变得更加迅速。RDD相关的持久化和缓存,是Spark最重要的特征之一。可以说,缓存是Spark构建迭代式算法和快速交互式查询的关键。
RDD缓存方式
RDD通过persist方法或cache方法可以将前面的计算结果缓存,但是并不是这两个方法被调用时立即缓存,而是触发后面的action时,该RDD将会被缓存在计算节点的内存中,并供后面重用。
7.3RDD cache使用
val data = sc.textFile(“hdfs://nn:8020/input”)
data.cache() //实际上是data.persist(StorageLevel.MEMORY_ONLY)
//data.persist(StorageLevel.DISK_ONLY_2)
7.4cache和persist的区别
/** Persist this RDD with the default storage level (`MEMORY_ONLY`). */
def cache(): this.type = persist()
说明是cache()调用了persist(), 想要知道二者的不同还需要看一下persist函数:
/** Persist this RDD with the default storage level (`MEMORY_ONLY`). */
def persist(): this.type = persist(StorageLevel.MEMORY_ONLY)
2.RDD创建的分区详解
makeRDD()或者parallieise()默认分区数
textFile()默认分区数
3.checkPoint机制
Checkpoint的产生就是为了相对而言更加可靠的持久化数据,在Checkpoint的时候可以指定把数据放在本地,并且是多副本的方式,但是在生产环境下是放在HDFS上,这就天然的借助了HDFS高容错、高可靠的特征来完成了最大化的可靠的持久化数据的方式
Spark读取缓存或checpoint的顺序?
1-Spark会从cache机制从内存中读取RDD数据
2-如果数据没有读到,会使用checkpoint机制读取数据。此时如果没有checkpoint机制
3-那么就需要找到父RDD重新计算数据了(Spark的RDD依赖关系,重建依赖关系)
总计:checkpoint是个很重要的容错机制。
SPark提供的缓存机制和checkpoint机制的区别
1-Persist和Cache只是将数据保存在block块中,但是lineage血统的关系是不变的
2-CheckPoint执行完毕后,rdd已经没有依赖关系的存在,切断了依赖关系,Checpoint设置的目录就会保存依赖关系的RDD,checkpoint之后的RDD的依赖改变了。
3-可靠性讲---checpoint更加可靠
Spark的缓存或检查点checkpoint机制什么时候使用?
一定在RDD的依赖关系或血缘关系非常长的时候,建议使用缓存或检查点机制
checkpoint原理机制
当RDD使用cache机制从内存中读取数据,如果数据没有读到,会使用checkpoint机制读取数据。此时如果没有checkpoint机制,那么就需要找到父RDD重新计算数据了,因此checkpoint是个很重要的容错机制。checkpoint就是对于一个RDD chain(链)如果后面需要反复使用某些中间结果RDD,可能因为一些故障导致该中间数据丢失,那么就可以针对该RDD启动checkpoint机制,使用checkpoint首先需要调用sparkContext的setCheckpointDir方法,设置一个容错文件系统目录,比如hdfs,然后对RDD调用checkpoint方法。之后在RDD所处的job运行结束后,会启动一个单独的job来将checkpoint过的数据写入之前设置的文件系统持久化,进行高可用。所以后面的计算在使用该RDD时,如果数据丢失了,但是还是可以从它的checkpoint中读取数据,不需要重新计算。
persist或者cache与checkpoint的区别在于,前者持久化只是将数据保存在BlockManager中但是其lineage是不变的,但是后者checkpoint执行完后,rdd已经没有依赖RDD,只有一个checkpointRDD,checkpoint之后,RDD的lineage就改变了。persist或者cache持久化的数据丢失的可能性更大,因为可能磁盘或内存被清理,但是checkpoint的数据通常保存到hdfs上,放在了高容错文件系统。
课堂图解总结:
一个application里面包含多个job,调用一次action算子,就是一个job。如果rdd的数据要进行复用,可以将rdd的数据进行缓存,使用cache或者persist将数据进行缓存
内存里面的数据可能会过期
persist将数据缓存到本地磁盘文件,磁盘文件有可能会被删掉
对于一些比较珍贵的rdd里面的数据可以使用checkpoint将数据持久化保存起来.
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("Ip_address").setMaster("local[*]")
val sc = new SparkContext(conf)
sc.setLogLevel("WARN")
sc.setCheckpointDir("E:\\WorkSpace\\IdeaWS\\SparkTest\\day02_SaprkCore\\src\\main\\resources\\checkDir")
val rdd1: RDD[String] = sc.textFile("E:\\WorkSpace\\IdeaWS\\SparkTest\\day02_SaprkCore\\src\\main\\resources\\access.log")
val rdd2: RDD[Array[String]] = rdd1.map(_.split(" "))
rdd2.checkpoint()
rdd2.foreach(println(_))
}
4.spark任务调度
Spark的任务调度流程:
第一步:客户端提交jar包,提交一个application
第二步:根据提交的application以及rdd的依赖关系和血统lineage划分DAG
第三步:DAG送给DAGScheduler
第四步:DAGScheduler划分stage,划分taskSet
第五步:将taskSet送给taskScheduler
第六步:taskScheduler接收到taskSet
第七步:taskScheduler将接收到的taskset分解成为一个个的task,等待将task送给executor去执行
以上七部:都是Driver程序负责的职责,Driver程序一般都是运行在worker里面,使用cluster模式
第八步:将分解之后的task与executor进行通信,将task放到executor里面去执行
●为什么要划分Stage? --并行计算
一个复杂的业务逻辑如果有shuffle,那么就意味着前面阶段产生结果后,才能执行下一个阶段,即下一个阶段的计算要依赖上一个阶段的数据。那么我们按照shuffle进行划分(也就是按照宽依赖就行划分),就可以将一个DAG划分成多个Stage/阶段,在同一个Stage中,会有多个算子操作,可以形成一个pipeline流水线,流水线内的多个平行的分区可以并行执行
●如何划分DAG的stage
对于窄依赖,partition的转换处理在stage中完成计算,不划分(将窄依赖尽量放在在同一个stage中,可以实现流水线计算)
对于宽依赖,由于有shuffle的存在,只能在父RDD处理完成后,才能开始接下来的计算,也就是说需要要划分stage
●总结
Spark会根据shuffle/宽依赖使用回溯算法来对DAG进行Stage划分,从后往前,遇到宽依赖就断开,遇到窄依赖就把当前的RDD加入到当前的stage/阶段中
DAGScheduler
DAGScheduler对DAG有向无环图进行Stage划分。
记录哪个RDD或者 Stage 输出被物化(缓存),通常在一个复杂的shuffle之后,通常物化一下(cache、persist),方便之后的计算。
重新提交shuffle输出丢失的stage(stage内部计算出错)给TaskScheduler
将 Taskset 传给底层调度器
– spark-cluster TaskScheduler
– yarn-cluster YarnClusterScheduler
– yarn-client YarnClientClusterScheduler
TaskScheduler
(1)为每一个TaskSet构建一个TaskSetManager 实例管理这个TaskSet 的生命周期
(2)数据本地性决定每个Task最佳位置
(3)提交 taskset( 一组task) 到集群运行并监控
(4)推测执行,碰到计算缓慢任务需要放到别的节点上重试
(5)重新提交Shuffle输出丢失的Stage给DAGScheduler
5.spark运行架构
细化架构
1.Application:指的是*用户编写的Spark应用程序/代码*,包含了Driver功能代码和分布在集群中多个节点上运行的Executor代码。
2.Driver:Spark中的Driver即运*行上述Application的Main()函数*并且创建SparkContext,SparkContext负责和ClusterManager通信,进行资源的申请、任务的分配和监控等
3.Cluster Manager:指的是在集群上获取资源的外部服务,Standalone模式下由Master负责,Yarn模式下ResourceManager负责;
4.Executor:是运行在工作节点Worker上的进程,负责运行任务,并为应用程序存储数据,是执行分区计算任务的进程;
5.RDD:Resilient Distributed Dataset弹性分布式数据集,是分布式内存的一个抽象概念;
6.DAG:Directed Acyclic Graph有向无环图,反映RDD之间的依赖关系和执行流程;
7.Job:作业,按照DAG执行就是一个作业;Job==DAG
8.Stage:阶段,是作业的基本调度单位,同一个Stage中的Task可以并行执行,多个Task组成TaskSet任务集
9.Task:任务,运行在Executor上的工作单元,一个 Task 计算一个分区,包括pipline上的一系列操作
Spark运行基本流程
1.当一个Spark应用被提交时,首先需要为这个Spark Application构建基本的运行环境,即由任务控制节点(Driver)创建一个SparkContext,
2.SparkContext向资源管理器注册并申请运行Executor资源;
3.资源管理器为Executor分配资源并启动Executor进程,Executor运行情况将随着心跳发送到资源管理器上;
4.SparkContext根据RDD的依赖关系构建成DAG图,并提交给DAGScheduler进行解析划分成Stage,并把该Stage中的Task组成的Taskset发送给TaskScheduler。
5.TaskScheduler将Task发放给Executor运行,同时SparkContext将应用程序代码发放给Executor。
6.Executor将Task丢入到线程池中执行,把执行结果反馈给任务调度器,然后反馈给DAG调度器,运行完毕后写入数据并释放所有资源。
6.累加器
通常在向 Spark 传递函数时,比如使用 map() 函数或者用 filter() 传条件时,可以使用驱动器driver程序中定义的变量,但是集群中运行的每个任务都会得到这些变量的一份新的副本,更新这些副本的值也不会影响驱动器中的对应变量。这时使用累加器就可以实现我们想要的效果。
val xx: Accumulator[Int] = sc.accumulator(0)
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("Ip_address").setMaster("local[*]")
val sc = new SparkContext(conf)
sc.setLogLevel("WARN")
val seq: Seq[Int] = Seq(1,2,3)
var count = 0
seq.foreach(x=>count+=x)
println(count)
println("==================================")
var count1 = 0
val rdd1: RDD[Int] = sc.parallelize(seq)
rdd1.foreach(count1+=_)
println(count1)
println("=================================================")
val count2: Accumulator[Int] = sc.accumulator(0)
rdd1.foreach(count2+=_)
println(count2)
val acc: DoubleAccumulator = sc.doubleAccumulator("acc")
}
7.广播变量
广播变量允许程序员将一个只读的变量缓存在每台机器上,而不用在任务之间传递变量。广播变量可被用于有效地给每个节点一个大输入数据集的副本。
假如我们要共享的变量map,1M
在默认的,task执行的算子中,使用了外部的变量,每个task都会获取一份变量的副本。
广播,Broadcast,将大变量广播出去。而不是直接使用。
广播变量的好处,不是每个task一份变量副本,而是变成每个节点的executor才一份副本。这样的话,就可以让变量产生的副本大大减少。
广播变量,初始的时候,就在Drvier上有一份副本。task在运行的时候,想要使用广播变量中的数据,此时首先会在自己本地的Executor对应的BlockManager中,尝试获取变量副本;如果本地没有,BlockManager,也许会从远程的Driver上面去获取变量副本;也有可能从距离比较近的其他节点的Executor的BlockManager上去获取,并保存在本地的BlockManager中;BlockManager负责管理某个Executor对应的内存和磁盘上的数据,此后这个executor上的task,都会直接使用本地的BlockManager中的副本。
val Broadcast: Broadcast[Array[(String, String, String, String)]] = sc.broadcast(ip_info.collect())