[spark]一RDD的基本属性|RDD的缓存|RDD的容错机制|spark组件简介|spark运行模式|存储简介|spark job提交流程|SparkShuffle|什么是RDD|弹性|分区

一.概述

1.RDD的基本属性

分区、计算函数、依赖、分区器、首选运行位置。

1)分区

将数据进行分区,一个大的数据集被分成很多小的分区,每个分区包含RDD的一部分数据,大量的分区可以在不同的节点上同时运行。通过对每个分区的数据进行计算,然后对计算的结果进行汇总,从而实现整个数据集的计算。

RDD的计算是以分区为单位进行的,而且同一分区的所有数据都进行相同的计算,对于一个分区的数据而言,要么都执行,要么都不执行,所以RDD的计算是一种粗粒度的计算,分区的数量决定了同时执行的任务的数量。

2)计算函数

 当需要计算rdd2的数据时,rdd2就会调用其计算函数,使用rdd1的数据进行计算。这里需要注意的是:rdd2的计算函数并不是map函数,计算函数是rdd2的一个属性,rdd1通过map()函数生成了rdd2,在该过程中,确定了rdd2的计算函数应该如何执行。rdd2的计算函数就是获取rdd1中对应分区的数据,对该分区的数据中执行map函数传入的匿名函数(_+2),从而得到rdd2对应分区的数据。

RDD的计算函数的返回值都是迭代器类型,该迭代器中能返回RDD某分区的所有数据,RDD的计算函数通过迭代器的形式,避免了分区的数据同时加载到内存,从而避免了大量内存被占用。

3)依赖

RDD根据其依赖关系(宽依赖、窄依赖)和计算函数便可以根据父RDD的数据计算出每个分区的数据。RDD这样的数据转换和依赖称为血统(lineage),即每一个RDD只要根据其lineage,就能够把数据计算出来。

4)分区器

并不是所有的RDD都需要分区器(partitioner),一般只有(key,value)形式的RDD才有分区器,分区器在Shuffle的Map阶段使用,当RDD的计算函数发生shuffle时,分区器根据每条记录的key,判断这个key属于Reduce哪个分区的数据。

5)首选运行位置

每个RDD对于每个分区来说有一组首选运行位置,可以让RDD的每个分区的计算任务直接在指定主机上运行,从而实现移动计算而不移动数据。例如:spark中HadoopRDD能够实现加载数据的任务在相应节点上运行。

2.RDD的缓存

RDD是进行迭代计算的,默认并不会保存中间结果的数据。在计算完成后,中间迭代的结果数据将会全部消失。

当一个RDD经过了复杂的计算过程,经过多次Shuffle生成的数据,如果多次使用其计算结果,对其进行缓存,可以极大地提高程序的执行效率。

因为RDD是分布式的,不同的分区落在不同的节点上,所以RDD的缓存也是分布式的,当对一个RDD进行缓存时,可以直接将每个分区的数据缓存在当前分区的计算节点中,每个分区缓存RDD的一部分,完成整个RDD的缓存。

当一个RDD被设置成可缓存时,每个节点计算RDD的某个分区时,会将这个分区的数据进行缓存。当下一个action操作重新使用这个被缓存的RDD时,会直接使用缓存中的数据,大大提高了计算速度。

RDD可以使用persist()方法对RDD进行缓存,同时可以指定相应的缓存级别。RDD的cache()方法只是persist()的一个特殊情况,它使用MEMORY_ONLY作为存储级别。Spark的不同级别存储旨在提供一个CPU效率和内存使用的折中方案。这个策略由用户指定,一般来说,如果内存足够存放整个RDD的数据,使用MEMORY_ONLY是最佳的选择,可以让CPU以最高效的方式运行。如果这种方式不行,则可以尝试使用MEMORY_ONLY_SER,将数据序列化后进行存储。使用这种方式可以更加节约内存,缺点是中间增加了序列化和反序列化的过程,从而延长了计算时间。一般来说并不推荐将数据缓存到磁盘中,因为在这种情况下,重新计算数据可能比从磁盘中读取数据还要快,除非该RDD的数据经过了非常复杂的计算或从大量的数据中过滤出来一批数据,在这种情况下。缓存到磁盘中可能会提高计算效率。

使用:

rdd1.cache().collect()
rdd1.persist(StorageLevel.MEMORY_ONLY).collect()

3.RDD的容错机制

RDD的容错是通过其Lineage机制实现的,因为一个RDD的数据可以通过其父RDD的转换而来,如果在运行的过程中,某一分区的缓存数据丢失,则重新计算该分区的数据。

当此RDD的依赖是窄依赖时,只需要计算依赖的RDD的一个分区的数据即可,避免了一个几点出错则所有数据都需要重新计算的缺点。

但是如果丢失数据的RDD的依赖就是宽依赖,那么这个分区的数据可能依赖父RDD的所有分区的数据。在这种情况下就必须重新计算父RDD的所有分区的数据,从而完成数据恢复。

4.Spark组件简介

1)Application

一个Application就是用户编写的一个应用程序,在一个应用程序中,可能会多次获取计算结果(action算子)。

2)Job

用户的应用程序每获取一次计算结果,即对RDD进行一个action操作,就会生成一个Job,因此用户的一个应用程序可能会包含一个或多个Job。

3)Task

每个Job被划分为多个Task并行执行。Task是任务执行的基本单位,每个Task负责执行RDD的一个分区或者计算窄依赖中一组RDD对应的分区,所以Task数量与RDD分区域的数量相等。

4)Stage

在Application中,RDD之间的转换并不完全是窄依赖关系,多个RDD转换时,可能出现Shuffle的情况,在这种情况下,必须先将Map阶段的数据计算出来,再执行Reduce拉取Map阶段生成的数据。因此出现Shuffle的地方,实际上是分两步进行操作的,每一步都是用一组Task进行计算,即Map端使用一组Task进行计算,计算完成后,将计算结果临时存储,再运行一批Task拉取上一步Map输出结果进行Reduce计算,Task这样的分组被称为Stage,同一个Stage中所有Task计算逻辑相同。

5)Driver

每个应用程序对应一个Driver进程,Driver根据用户编写的RDD形成一个或多个Job,并将Job划分为多个Stage,将Stage划分为多个Task,将Task提交到Executor中执行。同时Driver负责将各个Executor中的运行结果进行汇总,形成最终的计算结果。

6)Executor

每个Application都会有自己独立的一批Executor,Executor负责运行Driver分配的Task,并将执行结果返回给Driver,不同应用程序间的Executor互不影响。

理论上每个Executor分配的CPU越多,Executor的数量越多,Spark任务就会计算得越快。

7)Worker Node

任何运行Executor的节点都可以称为Worker Node(工作节点),如果一个节点中资源充足,也可能会在一个工作节点中运行多个Executor.

5.Spark的运行模式

运行Spark的应用程序,其实仅仅需要两种角色:Driver和Executor。Driver负责将用户的应用程序划分为多个job,划分为多个Stage,划分为多个Task,将Task提交到Executor中运行。

Execuor负责运行这些Task并将运行的结果返回给Driver。Driver和Executor实际上并不关心是在哪里运行的,只要能够启动Java进程,将Driver和Executor运行起来,并使Driver和Executor进行通信即可,所以根据Driver和Executor运行位置的不同划分出了多种部署模式。

Spark任务的运行模式分为两大类:本地运行和集群运行。

Spark的本地模式一般在开发测试时使用,该模式在本地同时运行一个Driver和Executor进程。

在集群中运行时,Spark当前可以在YARN等集群中运行,每一个Spark的Application都会有一个Driver和多个Executor,在集群中运行时,多个Executor一定是在集群中运行的。而Driver可以在集群中运行,也可以在集群之外运行,即提交Spark任务的机器上运行。当Driver在集群中时,称为cluster模式,当Driver在集群之外时,称为client模式。

6.存储简介

在Spark中有很多需要存储的地方,如对RDD进行缓存,shuffle时map阶段数据的存储。广播变量时各节点对变量的存储等,这些数据的存储都离不开spark的存储模块:spark的存储模块将所有需要存储的数据进行了抽象,只要是需要存储的数据都成为block。

每个block都有一个唯一的id进行标识,并且存储模块提供多种不同级别的存储,如内存存储,磁盘存储等。只根据blockId即可获取该block的数据,如果数据不存在还可以通过网络从其他远程节点获取,Spark的存储模块也是分布式的,Driver端运行了存储模块的主节点BlockManagerMaster,在Driver端和Executor运行了从节点BlockManager 。BlockManagerMaster中存储了BlockId和Block所在节点的对应关系。当其他节点需要获取数据时,都可以通过BlockManagerMaster获取数据所在的节点。从节点负责对本节点的数据的存储和读取,并将存储的Block信息会报至BlockManagerMaster节点。

Spark的存储模块将RDD的每个分区都视为一个Block,对RDD缓存就是RDD的每一个分区的数据进行缓存。由于RDD是分布式的,所以RDD的缓存也是分布式的,当对RDD进行缓存时,会将RDD的每个分区的数据缓存到当前Executor的BlockManager中,如果指定了缓存数据有多少副本,BlockManager会负责将当前节点的数据向其他节点的BlockManager中复制,并实现多副本机制。

7.Spark Job提交流程

1)用户编写的Spark应用程序都是以new SparkContext()开始的,当SparkContext执行完毕,就开始执行用户编写的程序。

2)根据RDD算子类型的不同,可以分为两类:transformation算子,action算子。

transformations操作并没有真正地触发Spark Job的执行,只是对RDD的转换进行了记录,一直到用户的应用程序执行到action操作的时候,才触发spark job运行。

3)SparkContext将job提交至DAGScheduler,DAGSchedular获取job中执行action操作的RDD,将最后执行action操作的RDD划分到最后的ResultSatge中,然后递归遍历该RDD依赖和所有父RDD的依赖,每遇到宽依赖就将两个RDD划分到两个不同的Stage中,如果遇到窄依赖则将窄依赖的多个RDD划分到同一个Stage中,经过这个过程一个Job就会被划分为多个依赖关系的Stage,其中最后的Stage为ResultStage其他依赖的Stage为ShuffleMapStage,在每个Stage中,所有RDD之间都是窄依赖关系,Stage之间的RDD为宽依赖关系。DAGScheduler将最初被依赖的Stage提交,计算该Stage中的RDD数据,计算完成后,再将后续的Stage进行提交,直到运行到最后的ResultStage,则整个Job计算完成。

4)Spark使用DAGScheduler实现了对Stage的Task的划分。同一个Stage的多个Task会封装到一个TaskSet中,最终一个Stage会生成一个TaskSet,每个TaskSet包含多个Task,这些Task计算的RDD和计算逻辑完全相同,只是计算的分区不同。DAGScheduler会将TaskSet提交至TaskScheduler中进一步处理,ResultStage运行完成后,会将其运行结果返回至Driver端,ShuflleMapTask运行完成后,会将运行的结果直接保存在当前Executor的BlockManager中。

8.SparkShuffle

1)如果在val rdd2=rdd1.groupByKey()操作中

rdd1使用groupByKey()算子将rdd1转换为rdd2,此时rdd2对rdd1的依赖为宽依赖,即ShuffleDependency,在ShuffleDependency中包含了在shuffle过程中使用的几个重要的组件。

a)Partitioner

用于将Key进行分组,判断哪个Key应该分到哪一组中.Partitioner确定了所有key一共能分为多少组,这些分组的数量也决定了下游Reduce任务的分区数量。

b)Aggregator

用于将一个key的两个value进行聚合,也可以将两个聚合后的值进行聚合。在Reduce端使用Aggregator将同一个key的所有value进行聚合,如果定义了在map端进行聚合,在执行Map过程的时候,也会调用Aggregator首先在Map端进行聚合。

c)ShuffleHandle

用于在Map端获取写入器(ShuffleWriter),将分区的数据写入到文件中。在Reduce端用于获取分区的读取器(ShuffleReader)。

d)此外,ShuffleDependency中还包含了,是否对Key进行排序,是否在Map端进行聚合等。

2)当有ShuffleMapStage运行完成时,都会将数据分组存储到当前节点的BlockManager的文件中,等待下一个Stage来拉取结果。

3)ShuffleMapTask会被序列化到Executor节点中进行执行,在运行ShuffleMapTask时,首先会反序列化ShuffleMapTask。

4)ShuffleWriter根据ShuffleDependency中的Parititoner对每条记录的key进行分组,如果ShuffleDependency中定义了需要在Map端进行聚合,还会调用ShuffleDependency中的Aggregator组件将相同的key进行聚合。聚合方式取决于用户执行的算子中定义的聚合方式。最终ShuffleWriter将分组的数据写入文件中。

5)根据Partitioner可获取分区的数量,该数量决定了上游Map任务中间该数据分为多少组,也决定了在Reduce阶段的RDD一共有多少个分区。由于下游中的RDD的分区数量和上游Map端按照key分组的数量使用同一个ShuffleDependency中的Paritioner所以保证了上游的每个分区数据的分组数量和下游的RDD的分区数量一定是相同的。 

6)通过ShuffleManger获取ShuffleWriter。将Map端的数据写入BlockManager管理的文件中,存在两种不同的ShuffleManager,分别是HashShuffleManager和SortShuffleManager。在Spark2.0之后HashShuffleManager退出了历史舞台,HashShuffleWriter也不再使用。

在Map端每个ShuffleMapTask执行时,都会获取一个ShuffleWriter。HashShuffleWriter在写入Map端数据的时候,会对迭代器中的数据使用Partitioner进行分组,为每个分组生成一个文件,将分组中的数据写到文件中。假如在Map的Task数量为10000,在Reduce端的Task数量为1000,那么在集群中,Map端的过程将会形成10000x1000=100万个文件,由此可见,使用HashShuffleManager将会产生大量的文件,会对系统I/O造成巨大的压力。而且在对文件读写时需要打开文件的输出流,打开大量的文件将会消耗大量的内存,使Executor端的内存产生很大的压力。

SortShuffleWriter不仅支持Map端的聚合操作,还支持按照Key在Map端排序的操作。SortShuffleWriter最终会将所有的数据的Key按照分区排序写入到一个数据文件中,并生成相应的index文件,如果Map端需要聚合,SortShuffleManager会按照key使用ShuffleDependency中的Aggregator将数据聚合,如果需要按照key排序,使用SorterShuffleWriter写入数据时,每个key在同一个分区内有序。

SortShuffleManager在Map端最终都将数据写到了一个文件中,避免了大量文件的生成,减缓了Shuffle过程中磁盘的I/O压力。将Key按照分区进行排序,依次将不同的分区数据序列化后写入同一个文件中,再使用一个index小文件记录每个分区的数据在文件中的索引即可。

每个Task最终在Map端生成了两个文件:一个文件用于保存按分区排序的数据,另一个文件用于保存每个分区的索引信息。如果一个Task最终生成了3个分区,3个分区的大小分别为300字节,200字节和400字节。在生成的索引(index)文件中,仅仅写了分区数+1个Long类型的数值,分别记录每个分区的偏移量,如图。3个分区中,index文件将会写入4个Long类型的数值,分别为0、300、500、900。在读取分区n的数据时,只需要读取索引中第n个数值即可得到该分区的起始位置,读取n+1可得到分区的结束位置,根据起始和结束位置,即可读取该文件中指定部分的数据。

 7)在拉取数据时,由于ShuffleWriter写入数据时,将所有分区的数据写入了一个文件和一个index文件,在读取数据时,首先会解析index文件,获取对应的分区在文件中的偏移量,根据偏移量读取文件的一部分,返回至调用的ShuffleReader中。

9.什么是RDD

RDD是弹性分布式数据集,是Spark中最基本的数据的抽象。

代码中是一个抽象类,它代表一个弹性的,不可变,可分区,里面元素可以并行计算的数据集。

10.弹性

1)存储的弹性:内存与磁盘的自动切换

spark不但能计算内存中的数据,也能计算磁盘中的数据,当运行程序时内存不足,spark会将在内存中的数据切换到磁盘存储。

2)容错的弹性:数据丢失可以自动修复

当第N个RDD丢失,会计算第N-1个RDD,从而得到丢失的RDD。

3)计算的弹性:计算失败重试机制

Task如果计算失败会自动进行特定次数的重试(默认4次)

Stage如果计算失败会自定进行特定次数的重试(可以只运行计算失败的Stage或只运行计算失败的数据切片)。

4)分片的弹性:可根据需要重新分片

函数:repartition、coalesce

11.分区

local[2]
val list=sc.parallelize(1 to 10)
这个list是两个分区
[0] 1 2 3 4 5 
[1] 6 7 8 9 10

1)并行度:程序在同一时间最大能够执行的线程数,在spark job中一个线程对应一个正在执行的task,一个task对应一个正在执行的partition,因此spark中的并行度就是同一时间能够最大执行的task个数。

注意:并行度并不是只有多少个分区,假如加载外部数据,产生的partition有100个,但是程序中设置的master为local[2],则并行度为2.

影响并行度的参数:spark.default.parallelize代表程序的并行度或rdd的分区个数。如果rdd是通过shuffle操作得到的,比如reduceByKey、join那么其并行度就是上游rdd最大的分区数。

2)如果rdd操作是没有父rdd的操作,例如textFile.parallelize,那么其并行度取决于local模式我们指定的线程个数,例如local[2]。

3)yarn模式:Math.max(2,所有Executor上面的cpu核数),假如有3个Executor,每个Executor上有4个核:Math.max(2,12)所以为12 

4)rdd.reduceByKey(_+_,4)如果指定了分区数,则执行完该函数的分区数为该分区数,如果没指定,则为父RDD中最大的分区数。

5)(k,v).reduceByKey(3)这种用的是Hash的分区方式没用k对3取余。结果为0到0号分区,结果为1到1号分区,为2到2号分区。

6)原始数据集例如val list=sc.parallelize(1 to 10,4);

[0]  1 2

[1] 3 4 5

[2] 6 7

[3] 8 9 10

采用的是Range(范围)分区的方式

可以通过rdd的transformation算子传Parition对象来完成rdd的数据的分区,spark提供的分区器有:

HashPartitioner(默认的)

RangePartitioner

对可排序的集合做sample抽样,生成多个Range。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值