大数据之Spark面试题(不定时更新,欢迎补充)

 

### Spark有哪些组件

1)master:管理集群和节点,不参与计算。 
2)worker:计算节点,进程本身不参与计算,和master汇报。 
3)Driver:运行程序的main方法,创建spark context对象。 
4)spark context:控制整个application的生命周期,包括dagsheduler和task scheduler等组件。 
5)client:用户提交程序的入口。

### Spark工作机制

用户在client端提交作业后,会由Driver运行main方法并创建spark context上下文。 
执行rdd算子,形成dag图输入dagscheduler,按照rdd之间的依赖关系划分stage输入task scheduler。 task scheduler会将stage划分为task set分发到各个节点的executor中执行。
 

### Spark中的RDD是什么,有哪些特性

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

Resilient:表示弹性的,弹性表示

Destributed:分布式,可以并行在集群计算

Dataset:就是一个集合,用于存放数据的

基本特性

a.分区

每一个 RDD 包含的数据被存储在系统的不同节点上。逻辑上我们可以将 RDD 理解成一个大的数组,数组中的每个元素就代表一个分区 (Partition) 。

b.不可变

不可变性是指每个 RDD 都是只读的,它所包含的分区信息是不可变的。由于已有的 RDD 是不可变的,所以我们只有对现有的 RDD 进行转化 (Transformation) 操作,才能得到新的 RDD ,一步一步的计算出我们想要的结果。

c.并行操作

因为 RDD 的分区特性,所以其天然支持并行处理的特性。即不同节点上的数据可以分别被处理,然后生成一个新的 RDD。

###  RDD、DataFrame、DataSet三者概念

1. RDD:全称Resilient Distributed Dataset,弹性分布式数据集,Spark中最基础的数据抽象,特点是RDD只包含数据本身,没有数据结构。

2. DataFrame:也是一个分布式数据容器,除数据本身,还记录了数据的结构信息,即schema;结构信息便于Spark知道该数据集中包含了哪些列,每一列的类型和数据是什么。

3. DataSet:Spark中最上层的数据抽象,不仅包含数据本身,记录了数据的结构信息schema,还包含了数据集的类型,也就是真正把数据集做成了一个java对象的形式,需要先创建一个样例类case class,把数据做成样例类的格式,每一列就是样例类里的属性。

ps:

(1)DataSet是面向对象的思想,把数据变成了对象的属性。

(2)DataSet是强类型,比如可以有DataSet[Car],DataSet[Person](汽车对象数据集,人对象数据集);DataFrame=DataSet[Row],DataFrame是DataSet的特例。

(3)在后期的Spark版本中,DataSet会逐步取代RDD和DataFrame成为唯一的API接口。

### Spark 中的常用算子区别

Spark 算子大致可以分为以下两类:

a.Transformation 变换/转换算子:这种变换并不触发提交作业,完成作业中间过程处理。

b.Action 行动算子:  这类算子会触发 SparkContext 提交 Job 作业。

map:用于遍历RDD,将函数应用于每一个元素,返回新的RDD(transformation算子)

filter :对元素进行过滤,对每个 元 素 应 用 f 函 数, 返 回 值 为 true 的 元 素 在RDD 中保留,返回值为 false 的元素将被过滤掉。(transformation算子)

flatMap:将原来 RDD 中的每个元素通过函数 f 转换为新的元素,并将生成的 RDD 的每个集合中的元素合并为一个集合(transformation算子)(相对于map 会想数组元素打乱返回一个数组)

foreach:用于遍历RDD,将函数应用于每一个元素,无返回值(action算子)

mapPatitions:用于遍历操作RDD中的每一个分区,返回生成一个新的RDD(transformation算子)

foreachPatition:用于遍历操作RDD中的每一个分区,无返回值(action算子)

collect : 相当于 toArray, toArray 已经过时不推荐使用, collect 将分布式的 RDD 返回为一个单机的 scala Array 数组。(action算子)

总结:一般使用mapPatitions和foreachPatition算子比map和foreach更加高效,推荐使用

详解的图文解释  参照博客 Spark常用算子 

### Spark中的 stage 定义

Job----> 一个或多个stage---> 一个或多个task

a.Stage 的 task 的数量 由输入文件的切片个数来决定的。在HDFS中不大于128m的文件算一个切片(默认128m)。通过算子修改了某一个rdd的分区数量,task数量也会同步修改。

b.Stage 中的并行度取决于 application任务运行时使用的executor拥有的cores的数量。

c.Spark任务会根据RDD之间的依赖关系,形成一个DAG有向无环图,DAG会提交给DAGScheduler,DAGScheduler会把DAG划分相互依赖的多个stage,划分依据就是宽窄依赖,遇到宽依赖就划分stage,每个stage包含一个或多个task,然后将这些task以taskSet的形式提交给TaskScheduler运行,stage是由一组并行的task组成

  - Stage 的划分标准就是宽依赖
  - 切割规则:从后往前,遇到宽依赖就切割stage;

### Spark 中的宽窄依赖

宽依赖:指的是多个子RDD的Partition会依赖同一个父RDD的Partition,关系是一对多,父RDD的一个分区的数据去到子RDD的不同分区里面,会有shuffle的产生。例如GroupByKeyreduceByKeyjoinsortByKey等操作。

窄依赖:指的是每一个父RDD的Partition最多被子RDD的一个partition使用,是一对一的,也就是父RDD的一个分区去到了子RDD的一个分区中,这个过程没有shuffle产生,如map 、filter。

shuffle可理解为数据的从原分区打乱重组到新的分区

总结:如果父RDD的一个Partition被一个子RDD的Partition所使用就是窄依赖,否则的话就是宽依赖。

### Job的生成:

一旦driver程序中出现action,就会生成一个job,比如count等,向DAGScheduler提交job,如果driver程序后面还有action,那么其他action也会对应生成相应的job,所以,driver端有多少action就会提交多少job,这可能就是为什么spark将driver程序称为application而不是job 的原因。每一个job可能会包含一个或者多个stage,最后一个stage生成result,在提交job 的过程中,DAGScheduler会首先从后往前划分stage,划分的标准就是宽依赖,一旦遇到宽依赖就划分,然后先提交没有父阶段的stage们,并在提交过程中,计算该stage的task数目以及类型,并提交具体的task,在这些无父阶段的stage提交完之后,依赖该stage 的stage才会提交。

### 有向无环图

DAG,有向无环图,简单的来说,就是一个由顶点和有方向性的边构成的图中,从任意一个顶点出发,没有任意一条路径会将其带回到出发点的顶点位置,为每个spark job计算具有依赖关系的多个stage任务阶段,通常根据shuffle来划分stage,如reduceByKey,groupByKey等涉及到shuffle的transformation就会产生新的stage ,然后将每个stage划分为具体的一组任务,以TaskSets的形式提交给底层的任务调度模块来执行,其中不同stage之前的RDD为宽依赖关系,TaskScheduler任务调度模块负责具体启动任务,监控和汇报任务运行情况。
 

### RDD 的基础数据类型

并行集合(Parallelized Collections):接收一个已经存在的Scala集合,然后进行各种并行计算。
Hadoop数据集(Hadoop Datasets):在一个文件的每条记录上运行函数。只要文件系统是HDFS,或者hadoop支持的任意存储系统即可。这两种类型的RDD都可以通过相同的方式进行操作,从而获得子RDD等一系列拓展,形成lineage血统关系图。 
 

814213-20191115213513703-274388531.pnguploading.4e448015.gif正在上传…重新上传取消814213-20191115213513703-274388531.pnguploading.4e448015.gif正在上传…重新上传取消

### RDD 缓存

Spark可以使用 persist 和 cache 方法将任意 RDD 缓存到内存、磁盘文件系统中。缓存是容错的,如果一个 RDD 分片丢失,可以通过构建它的 transformation自动重构。被缓存的 RDD 被使用的时,存取速度会被大大加速。一般的executor内存60%做 cache, 剩下的40%做task。

Spark中,RDD类可以使用cache() 和 persist() 方法来缓存。cache()是persist()的特例,将该RDD缓存到内存中。而persist可以指定一个StorageLevel。StorageLevel的列表可以在StorageLevel 伴生单例对象中找到。

Spark的不同StorageLevel ,目的满足内存使用和CPU效率权衡上的不同需求。我们建议通过以下的步骤来进行选择:

·如果你的RDDs可以很好的与默认的存储级别(MEMORY_ONLY)契合,就不需要做任何修改了。这已经是CPU使用效率最高的选项,它使得RDDs的操作尽可能的快。

·如果不行,试着使用MEMORY_ONLY_SER并且选择一个快速序列化的库使得对象在有比较高的空间使用率的情况下,依然可以较快被访问。

·尽可能不要存储到硬盘上,除非计算数据集的函数,计算量特别大,或者它们过滤了大量的数据。否则,重新计算一个分区的速度,和与从硬盘中读取基本差不多快。

·如果你想有快速故障恢复能力,使用复制存储级别(例如:用Spark来响应web应用的请求)。所有的存储级别都有通过重新计算丢失数据恢复错误的容错机制,但是复制存储级别可以让你在RDD上持续的运行任务,而不需要等待丢失的分区被重新计算。

·如果你想要定义你自己的存储级别(比如复制因子为3而不是2),可以使用StorageLevel 单例对象的apply()方法。

在不会使用cached RDD的时候,及时使用unpersist方法来释放它。
 

### spark中cache和persist的区别

  • cache:缓存数据,默认是缓存在内存中,其本质还是调用persist
  • persist:缓存数据,有丰富的数据缓存策略。数据可以保存在内存也可以保存在磁盘中,使用的时候指定对应的缓存级别就可以了。

 ### RDD共享变量

在应用开发中,一个函数被传递给Spark操作(例如map和reduce),在一个远程集群上运行,它实际上操作的是这个函数用到的所有变量的独立拷贝。这些变量会被拷贝到每一台机器。通常看来,在任务之间中,读写共享变量显然不够高效。然而,Spark还是为两种常见的使用模式,提供了两种有限的共享变量:广播变量和累加器。

(1). 广播变量(Broadcast Variables)

– 广播变量缓存到各个节点的内存中,而不是每个 Task

– 广播变量被创建后,能在集群中运行的任何函数调用

– 广播变量是只读的,不能在被广播后修改

– 对于大数据集的广播, Spark 尝试使用高效的广播算法来降低通信成本

    val broadcastVar = sc.broadcast(Array(1, 2, 3))方法参数中是要广播的变量

(2). 累加器

累加器只支持加法操作,可以高效地并行,用于实现计数器和变量求和。Spark 原生支持数值类型和标准可变集合的计数器,但用户可以添加新的类型。只有驱动程序才能获取累加器的值

11.spark-submit的时候如何引入外部jar包:

在通过spark-submit提交任务时,可以通过添加配置参数来指定 

    –driver-class-path 外部jar包
    –jars 外部jar包
 

### Spark如何防止内存溢出

    1.driver端的内存溢出 

        可以增大driver的内存参数:spark.driver.memory (default 1g)
        这个参数用来设置Driver的内存。在Spark程序中,SparkContext,DAGScheduler都是运行在Driver端的。对应rdd的Stage切分也是在Driver端运行,如果用户自己写的程序有过多的步骤,切分出过多的Stage,这部分信息消耗的是Driver的内存,这个时候就需要调大Driver的内存。

    2.map过程产生大量对象导致内存溢出 

        这种溢出的原因是在单个map中产生了大量的对象导致的,例如:rdd.map(x=>for(i <- 1 to 10000) yield i.toString),这个操作在rdd中,每个对象都产生了10000个对象,这肯定很容易产生内存溢出的问题。针对这种问题,在不增加内存的情况下,可以通过减少每个Task的大小,以便达到每个Task即使产生大量的对象Executor的内存也能够装得下。具体做法可以在会产生大量对象的map操作之前调用repartition方法,分区成更小的块传入map。例如:rdd.repartition(10000).map(x=>for(i <- 1 to 10000) yield i.toString)。 

面对这种问题注意,不能使用rdd.coalesce方法,这个方法只能减少分区,不能增加分区, 不会有shuffle的过程。

    3.数据不平衡导致内存溢出 

        数据不平衡除了有可能导致内存溢出外,也有可能导致性能的问题,解决方法和上面说的类似,就是调用repartition重新分区。这里就不再累赘了。

    4.shuffle后内存溢出 

        shuffle内存溢出的情况可以说都是shuffle后,单个文件过大导致的。在Spark中,join,reduceByKey这一类型的过程,都会有shuffle的过程,在shuffle的使用,需要传入一个partitioner,大部分Spark中的shuffle操作,默认的partitioner都是HashPatitioner,默认值是父RDD中最大的分区数,这个参数通过spark.default.parallelism控制(在spark-sql中用spark.sql.shuffle.partitions) , spark.default.parallelism参数只对HashPartitioner有效,所以如果是别的Partitioner或者自己实现的Partitioner就不能使用spark.default.parallelism这个参数来控制shuffle的并发量了。如果是别的partitioner导致的shuffle内存溢出,就需要从partitioner的代码增加partitions的数量。

    5.standalone模式下资源分配不均匀导致内存溢出

        在standalone的模式下如果配置了–total-executor-cores 和 –executor-memory 这两个参数,但是没有配置–executor-cores这个参数的话,就有可能导致,每个Executor的memory是一样的,但是cores的数量不同,那么在cores数量多的Executor中,由于能够同时执行多个Task,就容易导致内存溢出的情况。这种情况的解决方法就是同时配置–executor-cores或者spark.executor.cores参数,确保Executor资源分配均匀。

    使用rdd.persist(StorageLevel.MEMORY_AND_DISK_SER)代替rdd.cache()

        rdd.cache()和rdd.persist(Storage.MEMORY_ONLY)是等价的,在内存不足的时候rdd.cache()的数据会丢失,再次使用的时候会重算,而rdd.persist(StorageLevel.MEMORY_AND_DISK_SER)在内存不足的时候会存储在磁盘,避免重算,只是消耗点IO时间。

 ### Spark数据倾斜的处理

发现数据倾斜的时候,不要急于提高executor的资源,修改参数或是修改程序,首先要检查数据本身,是否存在异常数据。

1.找出异常的key

  • 如果任务长时间卡在最后最后1个(几个)任务,首先要对key进行抽样分析,判断是哪些key造成的。 选取key,对数据进行抽样,统计出现的次数,根据出现次数大小排序取出前几个。
  • 比如: df.select(“key”).sample(false,0.1).(k=>(k,1)).reduceBykey(+).map(k=>(k._2,k._1)).sortByKey(false).take(10)
  • 如果发现多数数据分布都较为平均,而个别数据比其他数据大上若干个数量级,则说明发生了数据倾斜。

经过分析,倾斜的数据主要有以下三种情况: 

  • 1、null(空值)或是一些无意义的信息()之类的,大多是这个原因引起。
  • 2、有效数据,业务导致的正常数据分布。
  • 3、无效数据,大量重复的测试数据或是对结果影响不大的有效数据。

1.1.解决办法

    a.第1,2种情况,直接对数据进行过滤即可(因为该数据对当前业务不会产生影响)。

    b.第3种情况则需要进行一些特殊操作,常见的有以下几种做法 

   (1) 隔离执行,将异常的key过滤出来单独处理,最后与正常数据的处理结果进行union操作。
   (2) 对key先添加随机值,进行操作后,去掉随机值,再进行一次操作。
   (3) 使用reduceByKey 代替 groupByKey(reduceByKey用于对每个key对应的多个value进行merge操作,最重要的是它能够在本          地先进行merge操作,并且merge操作可以通过函数自定义.)
   (4) 使用map join。
 

2.Spark使用不当造成的数据倾斜

提高shuffle并行度

  • dataFrame和sparkSql可以设置spark.sql.shuffle.partitions参数控制shuffle的并发度,默认为200。
  • rdd操作可以设置spark.default.parallelism控制并发度,默认参数由不同的Cluster Manager控制。
  • 局限性: 只是让每个task执行更少的不同的key。无法解决个别key特别大的情况造成的倾斜,如果某些key的大小非常大,即使一个task单独执行它,也会受到数据倾斜的困扰。

    使用map join 代替reduce join

参考博客 https://www.jianshu.com/p/6411fff954cf

发布了57 篇原创文章 · 获赞 26 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/BoomLee/article/details/105400255