(一)Spark RDD的基础概念

577 阅读15分钟

1. MapReduce和Spark的对比

1.1 什么是MapReduce

MapReduce是一种计算模型,将大型数据分解成很多单个任务在集群中并行执行,然后将计算结果合并起来得到最终的结果。具体关于MapReduce的介绍可以看之前写过的文章(三)通俗易懂地理解MapReduce的工作原理

MapReduce解决了很多大数据中的场景问题,但是它的局限性也很明显:

  1. MapReduce只提供map和reduce两个操作,
  2. 中间结果放在hdfs文件系统中,迭代计算效率低
  3. 适合批量数据处理,对交互式数据处理的支持不够
  4. 需要写很多底层代码,不易上手。

如图是MapReduce计算WordCount:

img

1.2 什么是Spark

Spark实在Hadoop的基础上改进的,基于MapReduce算法实现的分布式计算,他拥有MapReduce所具有的优点。但不同于MapReduce的是Job的中间输出和结果可以保存在内存中,从而不再需要去读取HDFS,因此Spark可以更好的适用数据挖掘和机器学习等需要迭代的MapReduce的算法。

但是Spark仅仅只是设计到计算,并没有设计到数据的存储,所以还是需要去对接外部的数据源,比如HDFS。

发展至今,Spark不仅仅是MapReduce的替换方案,它已经发展成了一个包含众多子项目的Spark的生态,如图,Spark生态可以分为四层:

img

  1. 数据存储层,以HDFS为代表的一些分布式文件存储系统或各种数据库。
  2. 资源管理层,Yarn等资源管理器
  3. 数据处理引擎
  4. 丰富的类库支持,包括SQL,MLbin,GraphX,Spark Streaming,可以无缝的进行组合。

2. Spark的特点

在大数据的存储,计算,资源调度中,Spark主要解决计算问题,及主要替代MapReduce的功能,底层存储和资源调度很多公司仍让是在使用HDFS,Yarn来承载。Spark有什么特点可以让众多企业在Hadoop的生态中选择用Spark作为处理引擎呢?

  1. 速度快。Spark基于内存进行计算。
  2. 容易上手开发。Spark基于RDD的计算模型,比MapReduce的计算模型要更易理解和上手开发,可以实现各种复杂功能。
  3. 强通用性。Spark提供了Spark RDD,Spark SQL,Spark Streaming,Spark MLlib,Spark GraphX等技术组件,可以一站式的完成大数据领域的离线批处理,交互式查询,流式计算,机器学习,图计算等常见任务。
  4. 集成Hadoop。Spark可以完美的继承Hadoop。Hadoop的HDFS,Hive,HBase负责存储,Yarn负责资源调度,Spark负责大数据计算是比较流行的大数据解决方案。
  5. 社区高活跃度。

Spark Code面试题

MapReduce和Spark的都是并行计算,那么他们有什么相同和区别?

A:两者都是用MapReduce来进行并行计算的。

①MapReduce的一个作业称为job,job分为map task和reduce task,每个task都是在自已的进程中运行的,当task结束进程也会结束。关于Spark,用户提交的任务成为application,一个application对应一个sparkcontent,每触发一次action操作就会产生一个job,所以application中存在多个job,这些job可以是并行或串行执行。每个job有多个stage,stage是suffle中DGASchaduler通过RDD间的依赖关系划分job而来的,每个stage里有多个task,这些task组成taskset后由TaskSchaduler分发到各个executor中执行。executor的生命周期和app一样,即使没有job运行也是存在的,所以task可以快速启动读取内存进行计算。

②MapReduce的job只有map和reduce操作,表达能力欠佳而且mr过程中重复的读写hdfs,造成大量的io操作。Spark二点迭代计算都是在内存中进行的,API提供了大量的RDD操作如join,groupby等,而且DAG图可以实现良好的容错性。

3. RDD

RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,是Spark中最基本的数据抽象,它代表一个不可变的,可分区,里面的元素可以并行计算的。

Resilient:表示弹性,RDD的数据是可以保存在内存或者是磁盘中。

Distributed: 对内部的元素进行分布式存储,方便后期进行分布式计算。

Dataset:存储数据的集合

在代码中,每一个方法所对应的结果都是一个RDD,下一个RDD的结果会依赖上一个RDD。RDD的执行过程中会形成DAG图,形成lineage保证容错性。

img

3.1 RDD的五大特性

  1. A list of partitions:一个分区(Partition)列表,组成了该RDD的数据。
  2. A function for computing each split:每个分区的计算函数都算是一个RDD
  3. A list of dependencies on other RDDs:一个rdd会依赖于其他多个rdd
  4. Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned):针对key-value类型的RDD才有分区函数
  5. Optionally, a list of preferred locations to compute each split on (e.g. block locations for an HDFS file):计算任务的位置优先为存储每个Partition的位置

3.2 RDD的创建

通过已经存在的scala集合去构建:

val rdd1=sc.parallelize(List(1,2,3,4,5))
val rdd2=sc.parallelize(Array("zookeeper","kafka","spark"))
val rdd3=sc.makeRDD(List(1,2,3,4))

加载外部的数据源去构建:

val rdd1=sc.textFile("/words.txt")

从已经存在的RDD进行转换生成一个新的RDD:

val rdd2=rdd1.flatMap(_.split(" "))
val rdd3=rdd2.map((_,1))

3.3 RDD 的算子分类

3.3.1 transformation(转换)

根据已经存在的RDD转换生成一个新的RDD, 它是延迟加载,它不会立即执行。如wordCount中用到的 map,flatMap,reduceByKey。

3.3.2 action (动作)

它会真正触发任务的运行。将RDD的计算的结果数据返回给Driver端,或者是保存结果数据到外部存储介质中。如wordCount中用到的 collect,saveAsTextFile 等。

3.4 RDD 的常见算子

3.4.1 transformation算子

map(func)返回一个新的RDD,该RDD由每一个输入元素经过func函数转换后组成
filter(func)返回一个新的RDD,该RDD由经过func函数计算后返回值为true的输入元素组成
flatMap(func)类似于map,但是每一个输入元素可以被映射为0或多个输出元素(所以func应该返回一个序列,而不是单一元素)
mapPartitions(func)类似于map,但独立地在RDD的每一个分片上运行,因此在类型为T的RDD上运行时,func的函数类型必须是Iterator[T] => Iterator[U]
mapPartitionsWithIndex(func)类似于mapPartitions,但func带有一个整数参数表示分片的索引值,因此在类型为T的RDD上运行时,func的函数类型必须是(Int, Interator[T]) => Iterator[U]
union(otherDataset)对源RDD和参数RDD求并集后返回一个新的RDD
intersection(otherDataset)对源RDD和参数RDD求交集后返回一个新的RDD
distinct([numTasks]))对源RDD进行去重后返回一个新的RDD
groupByKey([numTasks])在一个(K,V)的RDD上调用,返回一个(K, Iterator[V])的RDD
reduceByKey(func, [numTasks])在一个(K,V)的RDD上调用,返回一个(K,V)的RDD,使用指定的reduce函数,将相同key的值聚合到一起,与groupByKey类似,reduce任务的个数可以通过第二个可选的参数来设置
sortByKey([ascending], [numTasks])在一个(K,V)的RDD上调用,K必须实现Ordered接口,返回一个按照key进行排序的(K,V)的RDD
sortBy(func,[ascending], [numTasks])与sortByKey类似,但是更灵活
join(otherDataset, [numTasks])在类型为(K,V)和(K,W)的RDD上调用,返回一个相同key对应的所有元素对在一起的(K,(V,W))的RDD
cogroup(otherDataset, [numTasks])在类型为(K,V)和(K,W)的RDD上调用,返回一个(K,(Iterable,Iterable))类型的RDD
coalesce(numPartitions)减少 RDD 的分区数到指定值。
repartition(numPartitions)重新给 RDD 分区
repartitionAndSortWithinPartitions(partitioner)重新给 RDD 分区,并且每个分区内以记录的 key 排序

3.4.2 action算子

reduce(func)reduce将RDD中元素前两个传给输入函数,产生一个新的return值,新产生的return值与RDD中下一个元素(第三个元素)组成两个元素,再被传给输入函数,直到最后只有一个值为止。
collect()在驱动程序中,以数组的形式返回数据集的所有元素
count()返回RDD的元素个数
first()返回RDD的第一个元素(类似于take(1))
take(n)返回一个由数据集的前n个元素组成的数组
takeOrdered(n, [ordering])返回自然顺序或者自定义顺序的前 n 个元素
saveAsTextFile(path)将数据集的元素以textfile的形式保存到HDFS文件系统或者其他支持的文件系统,对于每个元素,Spark将会调用toString方法,将它装换为文件中的文本
saveAsSequenceFile(path)将数据集中的元素以Hadoop sequencefile的格式保存到指定的目录下,可以使HDFS或者其他Hadoop支持的文件系统。
saveAsObjectFile(path)将数据集的元素,以 Java 序列化的方式保存到指定的目录下
countByKey()针对(K,V)类型的RDD,返回一个(K,Int)的map,表示每一个key对应的元素个数。
foreach(func)在数据集的每一个元素上,运行函数func
foreachPartition(func)在数据集的每一个分区上,运行函数func

面试题

  1. 下面哪个不是 RDD 的特点 (C )

    A. 可分区 B 可序列化 C 可修改 D 可持久化

  2. RDD的弹性表现在哪几点?

  • 自动的进行内存和磁盘的存储切换;

  • 基于Lingage的高效容错;

  • task如果失败会自动进行特定次数的重试;

  • stage如果失败会自动进行特定次数的重试,而且只会计算失败的分片;

  • checkpoint和persist,数据计算之后持久化缓存

  • 数据调度弹性,DAG TASK调度和资源无关

  • 数据分片的高度弹性,很多碎片可以合并成大的分片

  1. RDD有哪些缺陷?

  • 不支持细粒度的写和更新操作(如网络爬虫),spark写数据是粗粒度的。所谓粗粒度,就是批量写入数据,为了提高效率。但是读数据是细粒度的也就是

  • 说可以一条条的读。

  • 不支持增量迭代计算,Flink支持

  1. RDD创建有哪几种方式?

  • 使用程序中集合创建RDD
  • 使用本地文件系统创建RDD
  • 使用hdfs创建RDD
  • 基于数据库创建RDD
  • 基于Nosql创建RDD,如hbase
  • 基于s3创建RDD
  • 基于数据流,如socket创建RDD

4. Spark的架构

img

Spark集群中由driver端创建SparkContext,SparkContext负责协调各个Worker Node上的Executor,根据用户的输入产生若干个worker,worker节点运行若干个executor,一个executor是一个进程,运行各自的task,每个task执行相同的代码段处理不不同的数据。

img

5. Lineage

img

RDD的lineaga会记录RDD的元数据信息和转换行为,lineaga保存了RDD的依赖关系,当RDD的部分分区数据丢失时,它可以根据这些信息来重新运算和恢复丢失的数据分区。值得注意的是,并不需要人为干预分区数据的恢复,程序自身就可以帮我们根据RDD的血统关系自行恢复。

5.DAG

DAG(Directed Acyclic Graph) 叫做有向无环图,原始的RDD通过一系列的转换就形成了DAG。

5.1 窄依赖和宽依赖

窄依赖施主每一个父RDD的partition最多被一个RDD的partition使用,比如map/flatmap/filter/union等,且窄依赖不会产生shuffle。

宽依赖是指多个子RDD的partition依赖同一个父RDD的partition,比如reduceByKey/softByKey/groupBy/groupKeyBy/join等,宽依赖会产生suffle。

5.2 stage的类型

ShuffleMapStage: 最后一个shuffle之前的所有变换叫ShuffleMapStage,它对应的task是shuffleMapTask

ResultStage: 最后一个shuffle之后的操作叫ResultStage,它是最后一个Stage.它对应的task是ResultTask

5.3 为啥我们要划分stage

根据RDD之间的依赖关系将DGA划分成不同的Stage(调度阶段)。对于窄依赖,partition的转化处理在一个stage中完成;对于宽依赖,由于有shuffle的存在,只能在parent RDD处理完之后才能开始处理接下来的计算。

由于划分完stage后,在同一个stage只有窄依赖,没有宽依赖,可以实现流水线计算,stage的每一个分区对应一个task,在同一个stage中就有很多可以并行运行的task。

5.4 怎么划分stage

划分stage的依据就是宽依赖:

  1. 首先根据RDD的算子操作顺序生成DAG有向无环图,从最后一个RDD往前推,创建一个新的stage,把该RDD加入到该stage中,此时这个就是最后一个stage。
  2. 在往前推的过程中运行遇到了窄依赖就把该RDD加入到了本stage中,如果遇到了宽依赖就从宽依赖的位置切开,那么最后一个stage也被划分出来了。
  3. 重新创建一个stage,按照第二个步骤继续往前推,一直到最开始的RDD,整个划分stage的过程也接受了。

5.5 stage与stage之间的关系

img

划分完stage后,每一个stage中有很多可以并行运行的task,后期把每一个stage的task们封装在一个taskset集合中,最后把一个一的staskset提交到wroker节点的executor上运行。

7. RDD 的缓存机制

把RDD的数据缓存起来,后面其他job需要用到该RDD的结果数据,可以直接从缓存中获得,避免重复计算,缓存是加快后续对该数据的访问操作。

RDD通过设置persist和cache方法把前面的计算结果缓存,但需要注意的并不是这两个方法被调用时就立刻缓存,而是当触发后面的action时,该RDD会被缓存在内存中,并供后面调用。

persist和cache的区别:cache默认是把数据放在内存中,其本质就是调用peisist。而persist可以把数据存在内存中或是磁盘,有丰富的缓存级别。

7.1 使用缓存的时机

img

当需要得到RDD3时,会从RDD1开始计算,经过算子操作后得到RDD2,RDD2计算后得到RDD3。同样RDD4也是从RDD1重新开始计算。

默认情况下会对这个RDD之前的父RDD重新计算一次,在实际开发中也经常遇到,但是我们一定要避免一个RDD重复计算多次,否则会导致性能急剧降低。

img

当获取一个RDD的结果数据是经过了大量的算子操作或者计算逻辑时,我们可以设置缓存,把多次使用到的RDD进行持久化,提升效率。

7.2 清除缓存数据

自动清除:一个application应用程序结束之后,对应的缓存数据也就自动清除

手动清除:调用unpersist方法

虽然我们可以对RDD的数据进行缓存,保存在内存或者是磁盘中,但注意也不是特别安全。

cache直接把数据保存在内存中,如果服务器挂掉或者是进程终止,那就会导致数据的丢失。

persist可以把数据保存在本地磁盘中,如果磁盘损坏也是有可能导致数据的丢失。

8.RDD的checkpoint机制

checkpoint提供了一种更加可靠的数据持久化方式。把数据保存在分布式文件系统如HDFS,利用高容错性(多副本)来最大程度的保证数据的安全性。

8.1 设置checkpoint

1.在 HDFS 上设置一个 checkpoint 目录

sc.setCheckpointDir("hdfs://node1:9000/checkpoint") 

2.对需要做 checkpoint 操作的rdd调用 checkpoint 方法

val rdd1=sc.textFile("/words.txt")
rdd1.checkpoint
val rdd2=rdd1.flatMap(_.split(" ")) 

3.最后需要有一个 action 操作去触发任务的运行,一个action对应一个job。

8.2 cache、persist、checkpoint 三者区别

cache和persist

cache默认数据缓存在内存中,persist可以把数据保存在内存或者磁盘中,后续要触发 cache 和 persist 持久化操作,需要有一个action操作,它不会开启其他新的任务,一个action操作就对应一个job。

它不会改变RDD的依赖关系,程序运行完成后,对应的缓存数据就自动消失。

checkpoint

可以把数据持久化写入到 HDFS 上,后续要触发checkpoint持久化操作,需要有一个action操作,后续会开启新的job执行checkpoint操作。

它会改变RDD的依赖关系,后续数据丢失了不能够在通过血统进行数据的恢复(因为它判断你已经持久化到 HDFS 中所以把依赖关系删除了),程序运行完成后对应的checkpoint数据就不会消失。

9. 为什么说Spark 擅长迭代计算

MapReduce 进行 pagerank 算法的一次迭代过程,需要注意的是灰色的部分都是需要存储到磁盘的数据:

img

Spark 执行 pageRank 算法的一次迭代过程,相较于 MapReduce 做了很多改进:

img

首先在内存足够的情况下 Spark 允许用户将常用的数据缓存到内存中,加快了系统的运行速度;

其次 Spark 对数据之间的依赖关系有了明确的划分,根据宽依赖与窄依赖关系进行任务的调度,可以实现管道化操作,使系统灵活性得以提高。

MapReduce 进行 pagerank 算法的二次迭代:

img

Spark 进行 pagerank 算法的二次迭代:

img

如图所示 Spark 可以将具有窄依赖关系的 RDD 分区分配到一个任务中,进行管道化操作,任务内部数据无需通过网络传输且任务之间互不干扰,因此 Spark 两次迭代只有三次 shuffle。

在一次迭代过程中,MapReduce 与 Spark 在性能上可能并没有很大的差别,但是随着迭代次数的增加,两者的差距逐渐显现出来。Spark 根据依赖关系采用的任务调度策略使得 shuffle 次数相较于 MapReduce 有了显著降低,因此 Spark 的设计十分适合进行迭代运算。