RDD

spark把分布式数据的计算逻辑划分为DAG,DAG有多个节点,每个节点相互依赖,单向。每个节点用RDD来表示。每个RDD内保存有每步计算需要的父RDD、函数、数据位置等信息。

1.RDD的起源

  • 官网上说RDD是并行且容错的数据集合。RDD是Spark最核心的概念,搞清楚RDD,才能回答Spark为什么快,为什么有高容错性,才能搞清楚任务提交和调度机制。
  • 分布式计算中,如果不用框架,需要考虑的东西太多:需要把运算逻辑打成jar,上传到各个数据节点,计算完还要取结果,汇总。期间还要考虑容错:节点、进程宕机,进程启停,中间结果处理,shuffle也就是数据的重新分配等问题。而spark和mr将这些过程封装,mr提供了mapreduce的编程模型,spark提供了rdd,使得开发者可以专注于业务逻辑的编写。

 1.1 而spark相对于mr的优势是什么?,其实也就是spark为什么快的问题?6

  • {1} 使用DAG来组织逻辑,提供80多个算子。
    DAG由一系列RDD通过血缘组成,通过job,stage的划分,来实现计算过程的优化。使用mr时,开发者常常花费大量时间考虑如何把操作组合到一起,以减少 MapReduce 的迭代次数。而在 Spark 的DAG可以自动优化,写出一个非常复杂的映射并不见得能比使用很多简单的连续操作获得好很多的性能。
    spark提供了多种算子,算子内部也进行了优化,比如向hdfs存结果,rdd一句话就可以。可以更快的处理业务。而MR模型只有map和reduce2个阶段,很多代码需要自己封装。因此,用户可以用更小的操作来组织他们的程序,这样也使这些操作更容易管理。
  • {2} 粗粒度资源申请 + 基于多线程的任务调度机制
    • [1] spark也使用了mapreduce分而治之的思想。但mr的job基于进程。spark的job会划分为stage,进而切分为task,由Executor进程的线程池执行。进程的切换和开启开销远大于线程。迭代计算时,mr提交一个job就要开一个进程,就会申请资源,而spark的资源在application开始执行时就申请好了,task执行时不需要再重新申请。
    • [2] 单个job的场景下spark优势并不大。但实际场景中的任务都是由很多job组成的,比如一个中等规模的join操作就有可能有10多个job。进行这种迭代计算时,mr执行完一个job就释放资源然后下一个job重新申请,重新开进程。而spark由application统一申请资源,application提交时已经把所有资源申请好,只需要从线程池拿线程执行一个个job即可。
  • {3} Task的执行有优化:推测执行,数据本地化。当然这一点mr也有。
  • {4} 基于内存,迭代计算的中间结果不落盘直接在内存中传递。只有shuffle时或中间结果内存存不下才落盘。而MR的中间结果肯定会落盘。比如运行pageRank或者机器学习的算法,有可能要迭代几百次。这样差距就显现出来了。
  • {5} spark有持久化机制,可以把数据持久化到内存或者磁盘中,减少重复的数据加载,还可以容错。MR也有缓存,但目的是为了提高磁盘IO效率
  • {6} spark的shuffle做了优化
    • [1] sSM的普通机制减少文件的生成
    • [2] sSM的优化机制取消了排序

 1.2 RDD的作用:

  1. 提供操作分布式数据的接口
  2. 将对数据的计算过程切分为多个血缘,这一点使通过RDD的只读不可变的特性实现。使DAG成为可能,分区粒度的容错、缓存、窄依赖内并行计算也都得以实现。
  3. 容错,以分区为单位,重新计算丢失的分区

2.RDD的理解

  • RDD是分布式数据的代理,通过RDD以单机方式来间接操作分布式数据。设置好RDD的操作逻辑后,client提交,driver会把这些操作转成task发到各节点的executor进程执行,使得操作分布式数据就像操作单节点数据一样。就像操作单机list,可以调用list 的方法,操作RDD,也可以调用RDD的算子,所以RDD叫数据集,只不过这个数据集不能存数据,容器的作用无非就是存数据和操作数据,而RDD只有操作数据的功能,这个操作数据也不能像单机list那样有add和remove操作,但可以处理数据。
  • RDD的对分布式数据处理逻辑的封装,那分布式数据有什么处理逻辑的?
  1. 加载数据:从hdfs也就是磁盘记载数据到内存
  2. 分区操作:大数据场景必然要以分区为单位来操作数据,而分区根据是否shuffle分为2种
  3. 计算:
    1. ==》形成DAG
      1. 把计算发送到多个节点执行:
      2. 按能否进行pipeline式计算也就是是否shuffle划分stage,同一stage内的可以多分区并行 + 同一分区pipeline来计算,前后stage之间顺序进行。
  4. 容错
  5. 把逻辑切分封装为算子
  6. 同时RDD又引入了FP的思想,每个RDD就是函数(算子)的输入和输出。而FP的变量都是immutable
    以上6点其实就是RDD的作用,是上面1.2的更细粒度的解释

2.1RDD的几个理解的要点

2.1.1.RDD是抽象的,是个抽象类,说明不能真的存数据,但存的是数据的索引。这些数据可以并行操作。

虽然RDD不存数据,存的是逻辑和元数据,

  • 逻辑包括父RDD到当前RDD的算子
  • 元数据也就是分区的索引,包括分区的位置

有了这些数据,其实也就是保存了数据变换的1个状态,这个状态可以随时恢复

  val rdd1 = sc.textFile("xxx")
  val rdd2 = rdd1.map
  rdd2.first
  • RDD的容错之所以更先进,是因为其容错是以分区为单位的,当某个RDD的某个分区也就是某个block损坏,不需要重新计算整个RDD,而是只重新计算这个分区即可。

2.1.2.算子:operations,其实就是RDD里面的方法,RDD包含了基本的算子,他的子类比如PairRDD的方法更多。想要的操作都有对应的算子,不像MR,需要自己实现。

2.1.2.1.算子分2种:

transfomation的返回值是RDD,action的返回值是具体的值(比如count)或Unit,比如saveAsXxx的返回值就是Unit

reduceByKey不是action是transfomation,但会触发shuffle,为了保证数据不丢失,也会触发前面转换算子的执行

reduce是action

2.1.2.2.转换算子的懒执行

懒:到了不得不干火烧眉毛了的时候才干,使用textFile加载1个不存在的目录不会报错,因为lazy。

2.1.3.弹性:RDD还考虑了容错的情况,使用lineage的概念,所以叫弹性,意思是可复原,可容错,以分区为单元。弹性还有个意思是可大可小,大了就分布式计算。

2.1.3.1.容错靠的是血缘,RDD之间有lineage关系,组成1个DAG,可以任意复原,还可以缓存数据避免重复计算。

2.1.4.分区:RDD处理的是分布式的数据,所以有分区的概念,通过分区才能并行,而且还可以随意修改分区某些情况下只是简单的数据合并,不需要shuffle,有时需要进行数据的重新分配,就需要shuffle,shuffle意味着对全量数据操作,往往要落盘。

有几个分区就意味着Driver会生成几个task,有几个task就说明处理分布式数据的并行度是几。1个task对应1部分数据,有几个task,数据就会被分成几份,处理hdfs上数据时默认RDD的分区数就是block数,一旦修改,比如分区数改的比block小,意味着1个task会处理2个block的数据。

存到hdfs上时,几个task就有几个结果文件

2.1.5.不可变:不能对数据修改,没有add和remove操作,但可以处理数据

2.2. RDD的5个属性

Spark中想要对数据做任何处理都要通过RDD,RDD是Spark提供的数据操作入口。无论是对数据结构的转换,还是保存到指定的位置,都要通过RDD,RDD使得开发者可以以单机的方式操作分布式数据。

RDD封装的逻辑从它的5个属性可以一探究竟:

  • 1) A list of partitions

    RDD有多个分区,1个分区对应一个要计算的数据节点

    spark任务计算是以分区为单位,一个分区就对应上一个task线程。

    通过val rdd1=sc.textFile(文件) 如果这个文件大小的block个数小于等于2,它产生的rdd的分区数就是2 如果这个文件大小的block个数大于2,它产生的rdd的分区数跟文件的block相同。创建RDD时,如果不指定分区数量,当RDD从集合创建时,则默认分区数量为该程序所分配到的资源的CPU核数(每个Core可以承载2~4个 partition),如果是从HDFS文件创建,默认为文件的 Block数。

    分区的索引,其实就是数据的位置,因为是分布式,所以要记录节点-块的位置

  • 2)A function for computing each split

    由一个函数计算每一个分片 比如: rdd2=rdd1.map(x=>(x,1)) ,这里指的就是每个单词计为1的函数

    RDD的逻辑是精确到分区的,某个RDD的某个分区数据丢了,都可以通过函数重新计算出来,而不是计算整个RDD所有分区

  • 3)A list of dependencies on other RDDs

    如何从其他RDD衍生(即计算)出本RDD的相关信息(即Lineage)

    RDD之间的依赖关系。一个rdd会依赖于其他多个rdd,这里就涉及到rdd与rdd之间的依赖关系,后期spark任务的容错机制就是根据这个特性而来。 比如: rdd2=rdd1.map(x=>(x,1)) rdd2的结果是通过rdd1调用了map方法生成,那么rdd2就依赖于rdd1依赖还具体分为宽依赖和窄依赖,但并不是所有的RDD都有依赖。

    窄依赖:1父分区对1子分区

    宽依赖:1父分区对多子分区,子RDD的分区计算依赖于父RDD的所有分区

  • 4)Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned)

    分区器用来shuffle时分配数据

    (可选项) 对于kv类型的pairRDD才会有分区函数(必须要产生shuffle),对于不是kv类型的rdd分区函数是None。 分区函数的作用:它是决定了原始rdd的数据会流入到下面rdd的哪些分区中。 spark的分区函数有2种:第一种hashPartitioner(默认值), 通过 key.hashcode % 分区数=分区号 第二种RangePartitioner,是基于一定的范围进行分区。

  • 5)Optionally, a list of preferred locations to compute each split on (e.g. block locations for an HDFS file)

    调度task时,要先知道数据䣌位置,这样才能把task发送到这个最优位置。

    (可选项) 一组最优的数据块的位置,这里涉及到数据的本地性和数据位置最优。spark后期在进行任务调度的时候,会优先考虑存有数据的worker节点来进行任务的计算。

    RDD计算时,每个分区的代码运行在什么位置是一开始就确定的。对于一个HDFS文件来说,这个列表保存的就是每个Partition对应的块的位置。Spark向Executor发送task的时候,知道数据的位置,才有可能移动计算。

4.RDD的血缘

RDD是分布式的java对象(这里的对象就是指数据的封装,不一定是java)的集合,所以RDD是有泛型的。
action后,RDD需要先把数据从磁盘加载到内存,转成内存中的java对象,经过一系列转换,在内存中转成想要的结果,然后输出到hdfs或拉取到driver。

理想状态下,一个分区对应1个split/block,一个分区1个task,发过去执行完再把结果拉回来即可。这种计算只需要1个stage,全是窄依赖。
现实中很多算子需要用到不同分区的数据(分区分布在不同节点),把数据重新分配到不同的分区,也就是重新分区,并且重新分区的过程中需要拉取数据(如果不拉取数据,比如collace算子可以设置shuffle为false,就是窄依赖),比如groupByKey,需要把分布在不同节点的相同key的数据拉取到同1节点变成1组,然后再进行窄依赖的计算,此时就需要2个stage。
所谓shuffle,就是节点间互传数据,根据某个依据重新分配数据。

在同一stage内,rdd之间全是窄依赖。各个RDD之间转换时不需要从别的节点拉取数据,也就不需要节点之间通信。stage之间是宽依赖,需要把别的节点的数据拉取过来放到内存中变成内存中的java对象,然后才能进行操作。

4.1 每个RDD都是计算的1个逻辑的封装,计算的特点就是前1个输出是后1个的输入,多个RDD通过5个属性形成了1个有顺序、可追溯的DAG。也就意味着可缓存、可复原、可切断。他们之间的关系叫依赖,也叫血缘。

  1. 如果1个RDD出错,正常需要重新计算它前面的所有RDD,如果设置了checkpoint,则不需要
  2. 如果1个RDD被多个子RDD依赖,可以缓存,这样就不用多次重复计算了

4.2 依赖分2种,划分依据就是父RDD的分区拆不拆到多个子RDD分区,本质就是子分区要不要从多个节点拉取数据,也就是要不要shuffle。要不要等所有父分区计算完子RDD再计算,能不能实现pipeline计算?

须知:

  1. 分区意味着分布在多个节点,否则分区就没意义了。可以这么说:1个RDD对应1个hdfs文件,1个分区对应一个block,1个分区对应1个节点,RDD有多少个分区,RDD就对应多少个节点。
  2. 一个父RDD拆分,1个父分区一对多个子分区,子RDD的分区需要从多个父RDD分区,也就是多个节点拉取数据。
  3. join是pairRDD专有的算子

分类

在这里插入图片描述

  1. 窄:父RDD不拆,即父RDD1个分区的数据只对应子RDD的1个分区
    每个父RDD的分区都至多被一个子RDD的分区使用,即为OneToOneDependecies;
    这里2个关键点要注意:
    1. 每个父RDD,对于1个子RDD,直接的父RDD可以有多个
    2. 至多被:这也就意味着每个父RDD的分区是不需要拆分的,也就不需要节点间传输数据
  2. 宽:父RDD拆,即父RDD1个分区的数据重新分配到子RDD多个分区
    多个子分区依赖一个父分区,即为OneToManyDependecies。
    父分区数据经过shuffle过程的hash分区器(也可自定义分区器)分拆划分到子RDD。例如GroupByKey,reduceByKey,join,sortByKey等操作。

4.3 区别:

区分这两种依赖很有用。
首先,窄依赖允许在一个集群节点上以流水线的方式(pipeline)计算所有父分区。例如,逐个元素地执行map、然后filter操作;而宽依赖则需要首先计算好所有父分区数据,然后在节点之间进行Shuffle,这与MapReduce类似。
第二,窄依赖能够更有效地进行失效节点的恢复,即只需重新计算丢失RDD分区的父分区,而且不同节点之间可以并行计算;而对于一个宽依赖关系的Lineage图,单个节点失效可能导致这个RDD的所有祖先丢失部分分区,因而需要整体重新计算。

 {1} RDD窄依赖之间的数据传输一般在单个节点即可完成,而宽依赖则是多个节点间推送拉取

  [1] 窄依赖的每个父RDD的分区和子RDD的分区映射的数据block是相同的,父RDD分区只对应一个子RDD分区中。这些可以在一个节点内完成转换。无非就是对1个数据先map后filter而已。
  [2] 宽依赖需要在计算过程中将1个父RDD的分区数据推送到多个子RDD分区中,必有shuffle,而子RDD分区都是分布在不同节点的,中间会涉及不同节点之间的数据传输;

 {2} stage是可以pipeline式计算的。比如filter和map,map的RDD不需要等filterRDD执行完,而是以pipeline的形式,充分利用时间线的重叠。而wide只能顺序进行,因为1个子RDD分区要等所有父RDD所有分区计算完才能获取数据

在这里插入图片描述

 {3} 容错时,重新计算以分区为单位。窄依赖1个损坏的分区只需要计算1个父RDD的分区,而宽依赖因为依赖的父RDD分区有多个,需把计算父RDD所有分区都计算一次

  1. 对于窄依赖,由于父RDD的一个分区只对应一个子RDD分区,这样只需要重算和子RDD分区对应的父RDD分区即可,所以这个重算对数据的利用率是100%的;
  2. 对于宽依赖,重算的父RDD分区对应多个子RDD分区,这样实际上父RDD 中只有一部分的数据是被用于恢复这个丢失的子RDD分区的,另一部分对应子RDD的其它未丢失分区,这就造成了多余的计算;更一般的,宽依赖中子RDD分区通常来自多个父RDD分区,极端情况下,所有的父RDD分区都要进行重新计算。
  3. 如下图所示,b1分区丢失,则需要重新计算a1,a2和a3,这就产生了冗余计算(a1,a2,a3中对应b2的数据)。
    在这里插入图片描述

4.4 能形成宽依赖的算子

 {1} join

 须知:
  1. 2个要join的RDD,A和B。1个结果RDD,C。
  2. 1个RDD上1个分区对应1个task,这个task是要发送到数据节点执行的,如果2个RDD的2个task对应同1个数据节点,那么join时,不需要进行数据传输,也就不会有shuffle
  3. 英文单词前面加co前缀,表示共同、一起、联合的意思。
  4. 分区就是建立数据和节点的映射,而分区策略相同意味着相同key的数据分布在相同的节点上。
 对于join操作有两种情况:

在这里插入图片描述

  1. 窄依赖型:
    下图的这种情况,不要被迷惑,看起来像是子RDD从2个节点拉取数据,在这里插入图片描述
    实际上因为2个父RDD在join之前已经进行过repartition了,已经shuffle过了,并且使用了相同的分区策略。分区策略相同就意味着:

    • ARDD的分区和BRDD的分区只要key相同,则指向的节点相同。

    所以2个父RDD实际上在1个节点, 自然就没有数据传输了。

  2. 宽依赖型
    如果A中key1的分区和B中key1的分区不在同一节点,Akey1分区的task要从Bkey1的DN拉取数据,则必然有shuffle,则是wide;
    在这里插入图片描述

summary
  1. 窄依赖不仅包含一对一的窄依赖,还包含多对一的窄依赖,只要这个算子没发生数据拉取,不管父RDD有几个,都是窄依赖。
  2. 宽依赖就是多对多

 2. groupByKey,partitionBy,reduceByKey,sortByKey等需要把父RDD分区拆分的算子,所谓拆分就是根据key重新分配,

5.spark的app提交流程

.DataFrame和DataSet

.RDD的API

RDD在spark中是个abstract class,其子类有49个,其中一些也是抽象类。RDD包含基本操作,RDD的算子有80多个,封装了一些常用操作,比如向hdfs存数据。主要分成如下6部分:

  1. 5个主要的方法,Spark中的所有调度和执行都是基于这些方法完成的。开发者可以通过覆盖这些函数来实现自定义RDDs(例如,从新的存储系统中读取数据)
  2. 关于缓存
  3. 关于lineage,包括获取dependencies集合,设置checkpoint
    包括计算、获取宽窄依赖
  4. 关于分区
  5. saveAsXxxx
  6. 基本算子,
    • 交并差cartesian
    • sample
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值