spark常见面试题

spark面试题

1.spark的RDD是什么,有哪些特性

RDD(Resilient Distributed Dataset)

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

Resilient

弹性的:

  1. RDD中的数据可以存储在内存或者磁盘中。
  2. RDD中的分区是可以改变的。
Distributed

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

Dataset

用于存放数据的集合。

五大特性

1)A list of partitions

一个分区列表,RDD中的数据都存储在一个分区列表中。

2)A function for computing each split

作用在每一个分区中的函数。

3)A list of dependencies on other RDDs

一个RDD依赖于其它多个RDD,这点很重要,RDD的容错机制就是依据这个特性而来的。

4)Optionally,a Partitioner for key-value RDDs(eg:to say that the RDD is hash-partitioned)

可选的,针对kv类型的RDD才有这个特性,作用是决定了数据的来源以及数据处理后的去向。

5)数据本地性,数据位置最优

计算向数据靠拢。

2.概述一下spark中的常用算子区别(map,mapPartitions,foreach,foreachPatition)

map

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

foreach

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

mapPartions

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

foreachPartition

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

ps:一般使用mapPartitions和foreachPartition算子比map和foreach更加高效,推荐使用。

3.谈谈spark中的宽窄依赖

RDD和它的父RDD的关系有两种类型:窄依赖和宽依赖

窄依赖(narrow dependencies)

指的是每个父RDD的Partition最多被子RDD的一个partition使用,是一对一的,也就是父RDD的一个分区数据去到了子RDD的一个分区中,这个过程没有shuffle产生。

输入输出一对一的算子,且结果RDD的分区结构不变。主要是map/flatmap。

输入输出一对一的算子,但结果RDD的分区结构发生了变化,如union/coalesce。

从输入中选择部分元素的算子,如filter、distinct、substract、sample。

宽依赖(wide dependencies)

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

对单个RDD基于key进行重组和reduce,如groupByKey,reduceByKey。

对两个RDD基于key进行join和重组,如join。

经过大量shuffle生成的RDD,建议进行缓存。这样避免失败后重新带来的开销。

ps:reduce是一个action,和reduceByKey完全不同。

4.spark中如何划分stage

概念

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

  1. spark程序中可以因为不同的action触发众多的job,一个程序中可以由很多的job,每一个job是由一个或者多个stage构成的,后面的stage依赖于前面的stage,也就是说只有前面依赖的stage计算完毕后,后面的stage才会运行。
  2. stage的划分标准就是宽依赖:何时产生宽依赖就会产生一个新的stage,例如reduceByKey、groupByKey、join的算子,会导致宽依赖的产生。
  3. 切割规则:从后往前,遇到宽依赖就切割stage。
  4. 计算格式:pipeline管道计算模式,pipeline只是一种计算思想,一种模式。
  5. spark的pipeline管道计算模式相当于执行了一个高阶函数,也就是说来一条数据然后计算一条数据,会把所有的逻辑走完,然后落地,而MapReduce是1+1=2,2+1=3这样的计算模式,也就是计算完落地,然后再计算,然后再落地到磁盘或者内存,最后数据是落在计算节点上,按reduce的hash分区落地。管道计算模式完全基于内存计算,所以比MapReduce快的原因。
  6. 管道中的RDD何时落地:shuffle write的时候,对RDD进行持久化的时候。
  7. stage的task的并行度是由stage的最后一个RDD的分区数来决定的,一般来说,一个partition对应一个task,但最后reduce的时候可以手动改变reduce的个数,也就是改变最后一个RDD的分区数,也就改变了并行度。例如:reduceByKey(+,3)
  8. 优化:提高stage的并行度:reduceByKey(_+_,partition的个数),join(+,partition的个数)

5、DAGScheduler分析:

1)概述

是一个面向stage的调度器。

主要入参:dagScheduler.runJob(rdd, cleanedFunc, partitions, callSite, allowLocal, resultHandler, localProperties.get)

rdd:final RDD;

cleanedFunc:计算每个分区的函数。

resultHander:结果侦听器。

2)主要功能:

  1. 接收用户提交的job。
  2. 将job根据类型划分为不同的stage,记录那些RDD,stage被物化,并在每一个stage内产生一系列的task,并封装成taskset。
  3. 决定每个task的最佳位置,任务再数据所在节点上运行,并结合当前的缓存情况,将taskSet提交给TaskScheduler。
  4. 重新提交shuffle输出丢失的stage给taskScheduler。

ps:一个stage内部的错误不是由shuffle输出丢失造成的,DAGScheduler是不管的,由TaskScheduler负责尝试重新提交task执行。

6、job的生成:

一旦driver程序中出现action,就会生成一个job,比如count等,向DAGScheduler提交job。如果driver程序后面还有action,那么其它action也会生成相应的job。所以,driver端有多少action就会提交多少job。

这可能就是为什么spark将driver程序称为application而不是job的原因。

划分stage

每一个job可能会包含一个或多个stage,最后一个stage生成result。在提交job的过程中,DAGScheduler会首先从后往前划分stage,划分的标准就是宽依赖,一旦遇到宽依赖就划分。

然后先提交没有父阶段的stage,并在提交过程中,计算该stage的task数目以及类型,并提交具体的task,在这些无父阶段的stage提交完之后,依赖该stage的stage才会提交。

7、有向无环图

DAG,有向无环图,简单来说,就是一个有顶点和有方向性的边构成的图中,从任意一个顶点出发,没有任何一条路径会将其带回到出发点的顶点位置。

划分stage

为每个spark job计算具有依赖关系的多个stage任务阶段,通常根据shuffle来划分stage,如reduceByKey、groupByKey等涉及到shuffle的转换(transformation)就会产生新的stage。

划分task

然后将每个stage划分为具体的一组任务,以taskSet的形式提交给底层的任务调度模块来执行,其中不同stage之前的RDD为宽依赖关系,TaskScheduler任务调度模块负责具体启动任务,监控和汇报任务运行情况。

8、RDD是什么以及它的分类

spark所有的操作都围绕弹性分布式数据集(RDD)进行,这是一个有容错机制并可以被并行操作的元素集合,具有只读、分区、容错、高效、无需物化、可以缓存、RDD依赖等特征。

目前有两种类型的基础RDD:

1)并行集合(Parallelized Collections)

接收一个已经存在的scala集合,然后进行各种并行计算。

并行化集合是通过调用SparkContext的parallelize方法,在一个已经存在的scala集合上创建的(一个Seq对象)。

集合的对象将会被拷贝,创建出一个可以被并行操作的分布式数据集。

例如:

val rdd = sc.parallelize(Array(1 to 10))
// 根据能启动的executor的数量来进行切分多个slice,每个slice启动一个task来进行处理
val rdd = sc.parallelize(Array(1 to 10), 5)
// 指定了partition的数量

2)hadoop数据集(Hadoop Datasets)

在一个文件的每条记录上运行函数。只要文件系统是HDFS,或者hadoop支持的任意存储系统即可。

spark可以将任何hadoop所支持的存储资源转换成RDD。如:

  1. 本地文件(需要网络文件系统,所有的节点都必须能访问到)。
  2. hdfs
  3. cassandra
  4. hbase
  5. Amazon S3
  6. spark支持文本文件、sequencefiles和任何hadoop inputFormat格式。

ps:这两种类型的RDD都可以通过相同的方式进行操作,从而获得子RDD等一系列扩展,形成Lineage血统关系图。

9、RDD的操作

RDD支持两类操作:

  1. 转换(transformation):现有的RDD通过转换生成一个新的RDD,转换是延时执行(lazy)的。
  2. 动作(actions):在RDD上运行计算后,返回结果给驱动程序或写入文件系统。

1)转换

  1. map(func):返回一个新的分布式数据集,由每一个输入元素经过func函数转换后组成。
  2. filter(func):返回一个新数据集,由经过func函数计算后返回值为true的输入元素组成。
  3. flatMap(func):类似map,但是每一个输入元素可以被映射为0或多个输出元素(因此func应该返回一个序列,而不是单一元素)
  4. mapPartitions(func):类似于map,但独立的在RDD的每一个分块上运行,因此在类型为T的RDD上运行时,func的函数类型必须是Iterator[T] => Iterator[U]。mapPartitions将会被每一个数据集分区调用一次。每个数据集分区的全部内容将作为顺序的数据流传入函数func的参数中,func必须返回另一个Iterator[T]。被合并的结果自动转换成为新的RDD。
  5. mapPartitionsWithIndex(func) :与mapPartitions相似,但是输入函数func提供了一个正式的参数,可以用来表示分区的编号。
  6. sample(withReplacement, fraction, seed) :从数据中抽样,withReplacement表示是否有放回,withReplacement=true表示有放回抽样,fraction为抽样的概率(0<=fraction<=1),seed为随机种子。 当withReplacement=True时,相当于采用的是泊松抽样;当withReplacement=False时,相当于采用伯努利抽样。fraction=0.2并不是说明要抽出100个数字中20%的数据作为样本,而是每个数字被抽取为样本的概率为0.2。
  7. union(otherDataset) :并集操作,将源数据集与union中的输入数据集取并集,默认保留重复元素(如果不保留重复元素,可以利用distinct操作去除,下边介绍distinct时会介绍)。
  8. intersection(otherDataset) :交集操作,将源数据集与intersection中的输入数据集取交集,并返回新的数据集。
  9. distinct([numTasks]) :去除数据集中的重复元素。
  10. groupByKey([numTasks]):在一个(K,V)对的数据集上调用,返回一个(K,Seq[V])对的数据集。注意:默认情况下,只有8个并行任务来做操作,但是你可以传入一个可选的numTasks参数来改变它的。如果分组是用来计算聚合操作(如sum或average),那么应该使用reduceByKey或combineByKey来提供更好的性能。
  11. reduceByKey(func,[numTasks]):在一个(K,V)对的数据集上调用时,返回一个(K,V)对的数据集,使用指定的reduce函数,将相同key的值聚合到一起。类似groupByKey,reduce任务个数是可以通过第二个可选参数进行配置的。
  12. aggregateByKey(zeroValue, seqOp, comOp, [numTasks]) :在于键值对(K, V)的RDD中,按key将value进行分组合并,合并时,将每个value和初始值作为seqOp函数的参数,进行计算,返回的结果作为一个新的键值对(K, V),然后再将结果按照key进行合并,最后将每个分组的value传递给comOp函数进行计算(先将前两个value进行计算,将返回结果和下一个value传给comOp函数,以此类推),将key与计算结果作为一个新的键值对(K, V)输出。
  13. sortByKey([ascending],[numTasks]):在一个(K,V)对的数据集上调用,K必须实现Ordered接口,返回一个按照key进行排序的(K,V)对数据集。升序或降序由ascending布尔参数决定。
  14. join(otherDataset,[numTasks]):在类型为(K,V)和(K,W)类型的数据集上调用,返回一个相同key对应的所有元素对在一起的(K,(V,W))数据集。
  15. cogroup(otherDataset, [numTasks]) :作用于键值对(K, V)和(K, W)上,返回元组 (K, (Iterable, Iterable))。这一操作可叫做groupWith。
  16. cartesian(otherDataset) :笛卡尔乘积,作用于数据集T和U上,返回(T, U),即数据集中每个元素的两两组合。
  17. pipe(command, [envVars]) :将驱动程序中的RDD交给shell处理(外部进程),例如Perl或bash脚本。RDD元素作为标准输入传给脚本,脚本处理之后的标准输出会作为新的RDD返回给驱动程序。
  18. coalesce(numPartitions) :将RDD的分区数减小到numPartitions个。当数据集通过过滤规模减小时,使用这个操作可以提升性能。
  19. repartition(numPartitions) :重组数据,数据被重新随机分区为numPartitions个,numPartitions可以比原来大,也可以比原来小,平衡各个分区。这一操作会将整个数据集在网络中重新洗牌。
  20. repartitionAndSortWithinPartitions(partitioner) :根据给定的partitioner函数重新将RDD分区,并在分区内排序。这比先repartition然后在分区内sort高效,原因是这样迫使排序操作被移到了shuffle阶段。

2)动作

  1. Collect():在驱动程序东,以数组的形式,返回数据集的所有元素。这通常会在使用filter或者其它操作并返回一个足够小的数据子集后再使用会比较有用。
  2. reduce(func):使用函数func(两个输入参数,返回一个值)对数据集中的元素做聚集操作。函数func必须是可交换的,并且是相关联的,从而能够正确的进行并行计算。
  3. count():返回数据集的元素的个数。
  4. countByKey() :只能作用于键值对(K, V)形式的RDDs上。按照Key进行计数,返回键值对(K, int)的哈希表。
  5. first():返回数据集中的第一个元素,相当于take(1)。
  6. take():以数组形式返回数据集中前n个元素。**注意:**这一action并不是在多个节点上并行执行,而是在driver程序所在的机器上单机执行,会增大内存的压力,使用需谨慎。
  7. takeSample(withReplacement, num, [seed]) :以数组形式返回从数据集中抽取的样本数量为num的随机样本,有替换或者无替换的进行采样。可选参数[seed]可以允许用户自己预定义随机数生成器的种子。
  8. takeOrdered(n, [ordering]) :返回RDD的前n个元素,可以利用自然顺序或者由用户执行排序的comparator。
  9. foreach(func):在数据集的每一个元素上,运行函数func进行更新。这通常用于边缘效果,例如更新一个累加器,或者和外部存储系统进行交互,例如Hbase。
  10. saveAsTextFile(path):将数据集的元素,以textfile的形式,保存到本地文件系统,hdfs或者任何其它hadoop支持的文件系统。对于每个元素,spark将会调用toString方法,将它转换为文件中的文本行。
  11. saveAsSequenceFile(path) :将数据集中的元素以Hadoop SequenceFile的形式写入本地文件系统,或者HDFS,或者其他Hadoop支持的文件系统的指定路径path下。RDD的元素必须由实现了Hadoop的Writable接口的key-value键值对组成。在Scala中,也可以是隐式转换为Writable的键值对(Spark包括了基本类型的转换,例如Int,Double,String等等)
  12. saveAsObjectFile(path) :利用Java序列化,将数据集中的元素以一种简单的形式进行写操作,并能够利用SparkContext.objectFile()加载数据。(适用于Java和Scala)

10、RDD缓存

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

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

StorageLevel

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

  1. 如果你的RDDs可以很好的与默认的存储级别(MEMORY_ONLY)契合,就不需要做任何修改了。这已经是CPU使用效率最高的选项,它使得RDDs的操作尽可能的快。
  2. 如果不行,试着使用MEMORY_ONLY_SER并且选择一个快速序列化的库使得对象在有比较高的空间使用率的情况下,依然可以较快被访问。
  3. 尽可能不要存储到硬盘上,除非计算数据集的函数,计算量特别大,或者它们过滤了大量的数据。否则,重新计算一个分区的速度,和与从硬盘中读取基本差不多快。
  4. 如果你想有快速故障恢复能力,使用复制存储级别(例如:用Spark来响应web应用的请求)。所有的存储级别都有通过重新计算丢失数据恢复错误的容错机制,但是复制存储级别可以让你在RDD上持续的运行任务,而不需要等待丢失的分区被重新计算。
  5. 如果你想要定义你自己的存储级别(比如复制因子为3而不是2),可以使用StorageLevel 单例对象的apply()方法。
  6. 在不会使用cached RDD的时候,及时使用unpersist方法来释放它。

11、RDD共享变量

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

1)广播变量(Broadcast Variables)

  1. 广播变量缓存到各个节点的内存中,而不是每个 Task。
  2. 广播变量被创建后,能在集群中运行的任何函数调用。
  3. 广播变量是只读的,不能在被广播后修改。
  4. 对于大数据集的广播, Spark 尝试使用高效的广播算法来降低通信成本

2)累加器

累加器只支持加法操作,可以高效地并行,用于实现计数器和变量求和。

Spark 原生支持数值类型和标准可变集合的计数器,但用户可以添加新的类型。

只有驱动程序才能获取累加器的值。

12、spark-submit的时候如何引入外部jar包

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

  1. –driver-class-path 外部jar包
  2. –jars 外部jar包

13、spark如何防止内存溢出

1)driver端的内存溢出

可以增大driver的内存参数:spark.driver.memory(default 1G)。

ps:

这个参数用来设置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)

ps:面对这种问题,不能使用rdd.coalesce方法,因为他只能减少分区,不能增加分区,不会有shuffle的过程。

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

数据不平衡除了有可能导致内存溢出外,也有可能导致性能的问题。

解决办法:

  • 调用repartition重新分区。

4)shuffle内存溢出

一般都是shuffle后,单个文件过大导致的。

HashPartitioner

在spark中,join、reduceByKey这一类型的过程,都会有shuffle的过程,在shuffle的使用,需要传入一个partitioner,大部分spark中的shuffle操作,默认的partitioner都是hashpartitioner,默认值是父RDD中最大的分区数,这个参数通过spark.default.parallelism控制(在spark-sql中用spark.sql.shuffle.partitions)。

spark.default.parallelism参数只对hashpartitioner有效,所以如果是别的partitioner或者自己实现的partitioner就不能使用spark.default.parallelism这个参数来控制shuffle的并发量了。

其它Partitioner

如果是别的partitioner导致的shuffle内存溢出,就需要从partitioner的代码增加partitions的数量。

5)cache内存溢出

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

rdd.cache()和rdd.persist(Storage.MEMORY_ONLY)是等价的。

cache

在内存不足的时候,rdd.cache()的数据会丢失,再次使用时会重算。

persist

而rdd.persist(StorageLevel.MEMORY_AND_DISK_SER)在内存不足时会存储在磁盘,避免重算,只是消耗I/O时间。

14、spark中cache和persist的区别

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

15、spark分布式集群搭建的步骤

1)下载解压。

2)重命名spark-defaults.conf.template为spark-defaults.conf,并在文件末尾追加如下语句。

spark.eventLog.enabled	true
spark.eventLog.dir		hdfs://master:9000/spakLogs
spark.eventLog.compress	true
spark.yarn.historyServer.address	master:18080

3)编辑slaves

slave1
slave2

4)最后将master节点配置好的整个spark文件夹拷贝至各slave节点

scp -r spark hadoop@slave1:~
scp -r spark hadoop@slave2:~

ps:spark引入zookeeper搭建高可用的spark集群(HA)

16、spark中的数据倾斜现象、原因、后果

1)数据倾斜的现象

多数task执行速度较快,少数task执行时间非常长,或者等待很长时间后提示你内存不足,执行失败。

2)数据倾斜的原因

数据问题
  1. key本身分布不均衡(包括大量key为空)
  2. 分区器不能均衡的给数据分区。
spark使用问题
  1. shuffle的并发度不够
  2. 计算方式有误。

3)数据倾斜的后果

  1. spark中的stage的执行时间受限于最后那个执行完成的task,因此运行缓慢的任务会拖垮整个程序的运行速度(分布式程序运行的速度是由最慢的那个task决定的)。
  2. 过多的数据在同一个task中运行,将会把executor撑爆。

17、spark数据倾斜的处理

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

1)数据问题造成数据倾斜

分析

如果任务长时间卡在最后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、2情况,直接对数据进行过滤即可(因为该数据对当前业务不会产生影响)

对于3情况,一般可以如下处理:

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

假设说有倾斜的Key,我们给所有的Key加上一个随机数,然后进行reduceByKey操作。

此时同一个Key会有不同的随机数前缀,在进行reduceByKey操作的时候原来的一个非常大的倾斜的Key就分而治之变成若干个更小的Key,不过此时结果和原来不一样,怎么破?

进行map操作,目的是把随机数前缀去掉,然后再次进行reduceByKey操作。(当然,如果你很无聊,可以再次做随机数前缀)。

这样我们就可以把原本倾斜的Key通过分而治之方案分散开来,最后又进行了全局聚合。

ps:

注意1:如果此时依旧存在问题,建议筛选出倾斜的数据单独处理。最后将这份数据与正常的数据进行union即可。

注意2: 单独处理异常数据时,可以配合使用Map Join解决。

2)spark使用不当造成的数据倾斜

提高shuffle并行度
  1. dataFrame和sparkSql可以设置spark.sql.shuffle.patitions参数控制shuffle的并发度,默认为200。
  2. rdd操作可以设置spark.default.parallelism控制并发度,默认参数由不同的cluster manager控制。
  3. 局限性:只是让每个task执行更少的不同的key。无法解决个别key特别大的情况造成的倾斜,如果某些key的大小非常大,即使一个task单独执行它,也会收到数据倾斜的困扰。
使用map join代替reduce join
  1. 在小表不是特别大(取决于你的executor大小)的情况下使用,可以是程序避免shuffle的过程,自然也就没有数据倾斜的困扰了。
  2. 局限性:因为是先将小数据发送到每个executor上,所以数据量不能太大。

17、spark中map-side-join关联优化

将多份数据进行关联是数据处理过程非常普遍的用法,不过在分布式计算系统中,这个问题往往会变得非常麻烦。

reduce-side-join

框架提供的join操作一般会将所有数据根据key发送到所有的reduce分区中去,也就是shuffle的过程,造成大量网络以及磁盘IO消耗,运行效率及其低下,这个过程一般被称为reduce-side-join。

reduce-side-join的缺陷在于会将key相同的数据发送到同一个partition中进行运算,大数据集的传输需要长时间的IO,同时任务并发度收到限制,还可能造成数据倾斜。

如果其中有张小表的化,我们可以自己实现在map端实现数据关联,跳过大量数据进行shuffle的过程,运行时间得到大量缩短,根据不同数据可能会有几倍到数十倍的性能提升。

map-side-join

将少量的数据转化为map进行广播,广播会将此map发送到每个节点中,如果不进行广播,每个task执行时都会去获取该数据,造成了性能浪费。

对大数据进行遍历,使用mapPartition而不是map,因为mappartition是在每个partition中进行操作,因此可以减少遍历时新建broadCastMap.value对象的空间消耗,同时匹配不到的数据也不会返回。

何时使用

在海量数据中匹配少量特定数据时。

19、kafka整合sparkStreaming问题

在kafka0.10版本之前有2种方式于sparkStreaming整合一种时receiver,一种是directt。

receiver

采用kafka高级api,利用receiver接收器来接受kafka topic中的数据。

从kafka接收来的数据会存储在spark的executor中,之后spark streaming提交的job会处理这些数据。

kafka中topic的偏移量是保存在zk中的。

缺点:
  1. 可以保证数据不丢失,但是无法保证只被处理一次,WAL实现的是At-least-once语义(至少被处理一次)。即如果在写入到外部存储的数据还没有将offset更新到zookeeper就挂掉,这些数据将会被反复消费。
  2. 同时,降低了程序的吞吐量。

ps:

  1. spark中的partition和kafka中的partition并不是相关的,所以如果加大每个topic的partition数量,仅仅是增加线程来处理由单一receiver消费的主题。
  2. 对于不同的group和topic,可以使用多个receiver创建不同的Dstream来并行接收数据,之后使用union统一成一个Dstream。
  3. 在默认配置下,在receiver已经通知zookeeper数据接收完成但是还没有处理的时候executor突然挂了(或是driver通知executor关闭),缓存在其中的数据就会丢失。
  4. 为了做到高可靠,可以启用WAL(Write Ahead Logs,spark.streaming.receiver.writeAheadLog.enable=true),该机制会同步的将接收到的kafka数据写入分布式文件系统(如hdfs)上的预写日志中,所以即使底层节点出现了失败,也可以使用预写日志中的数据进行恢复。
  5. 设置:storagelevel需要设置为StorageLevel.MEMORY_AND_DISK_SER,也就是KafkaUtils.createStream(…, StorageLevel.MEMORY_AND_DISK_SER)。

direct

在spark1.3之后,引入了Direct方式。不同于receiver方式,direct方式没有receiver这一层。

其会周期性的获取kafka中每个topic的每个partition中的最新offsets,之后根据设定的maxRatePerPartition来处理每个batch。

(设置spark.streaming.kafka.maxRatePerPartition=10000。限制每秒钟从topic的每个partition最多消费的消息条数)。

特点
  1. 相比receiver模式,能够确保机制更加健壮。
  2. 区别receiver来被动接收数据,Direct模式会周期性的主动查询kafka,来获得每个topic+partition的最新的offset,从而定义每个batch的offset的范围。
  3. 当处理数据的job启动时,就会使用kafka的简单consumer api来获取kafka指定的offset范围的数据。
优点
1)简化并行读取

如果要读取多个partition,不需要创建多个输入Dstream,然后对他们进行union操作。

spark会创建跟kafkapartition一样多的RDD partition,并且会并行从kafka中读取数据。

所以kafka partition和RDD partition之间,有一个一对一的映射关系。

2)高性能

只要kafka中做了数据的副本,那么就可以通过kafka的副本进行恢复。

避免了receiver的WAL复制数据(分布式文件系统的预写日志)的步骤。

3)一次仅且一次的事务机制

使用kafka的简单api,sparkStreaming自己就负责追踪消费的offset,并保存在checkpoint中。

spark自己一定是同步的,因此可以保证数据是消费一次且仅消费一次。

不过需要自己完成将offset写入zk的过程,在官方文档中有相应介绍。

ps:

sparkStreaming程序自己消费完成后,自己主动去更新zk上面的偏移量。

也可以将zk中的偏移量保存在mysql或者redis数据库中,下次重启的时候,直接读取mysql或者redis中的偏移量,获取到上次的偏移量,接着读取数据。

20、spark master正在使用zookeeper进行HA时,有哪些元数据保存在zookeeper?

spark通过这个参数spark.deploy.zookeeper.dir指定master元数据在zookeeper中保存的位置。

包括worker,master,application,executors.standby节点要从zk中获得元数据信息,恢复集群运行状态,才能继续对外提供服务、作业提交资源申请等,在恢复前时不能接收请求的。

注意

  1. 在master切换的过程中,所有的已经在运行的程序皆正常运行。因为spark application在运行前就已经通过cluster manager获得了计算资源,所以在运行时job本身的调度和处理master是没有任何关系的。
  2. 在master的切换过程中唯一的影响是不能提交新的job,一方面不能提交新的应用程序给集群,因为只有Active master才能接受新的程序的提交请求,另外一方面,已经运行的程序也不能action操作触发新的job提交请求。

21、spark master HA主从切换过程不会影响集群已有的作业运行,为什么?

因为程序在运行之前,已经向集群申请过资源,这些资源已经提交给driver了,也就是说已经分配好资源了。

这是粗粒度分配,一次性分配好资源后不需要再关心资源分配,在运行时让driver和executor自动交互。弊端是如果资源分配太多,任务运行完不会很快释放,造成资源浪费。

这里不适合细粒度分配的原因是因为提交太慢。

22、什么是粗粒度,什么是细粒度,各自的优缺点是什么?

1)粗粒度

启动时就分配好资源,程序启动,后序具体使用就使用分配好的资源,不需要再分配。

好处:作业特别多时,资源复用率较高。使用粗粒度。

缺点:容易浪费资源,如果job有1000个task,完成999个,还有一个没完成,就会有999个资源闲置在那里,会造成资源大量浪费。

2)细粒度

用资源的时候分配,用完了就立即回收资源。

启动会麻烦一点,启动一次就分配一次,申请资源会比较麻烦。

23、driver的功能是什么

  1. 一个spark作业运行时包括一个driver经常,也就是作业的主进程,具有main函数,并且有sparkContext的实例,是程序的入口。
  2. 负责向集群申请资源。向master注册信息。
  3. 负责作业调度和解析。生成stage并调度task到executor上。包括DAGScheduler、TaskScheduler。

24、spark有几种部署模式,每种模式特点?

1)本地模式

Spark不一定非要跑在hadoop集群,可以在本地,起多个线程的方式来指定。将Spark应用以多线程的方式直接运行在本地,一般都是为了方便调试,本地模式分三类

  1. local:只启动一个executor
  2. local[k]:启动k个executor
  3. local:启动跟cpu数目相同的 executor

2)standalone模式

分布式部署集群,自带完整的服务。

资源管理和任务监控是spark自己监控。

这个模式也是其它模式的基础。

3)spark on yarn模式

分布式部署集群,资源和任务监控交给yarn管理。

但是目前仅支持粗粒度资源分配方式。

包含cluster和client运行模式。

  1. cluster适合生产,driver运行在集群子节点,具有容错功能。
  2. client适合调试,driver运行在客户端。

4)spark on mesos模式

官方推荐这种模式(当然,原因之一是血缘关系)。

正是由于spark开发支出就考虑到支持mesos,所以目前而言,spark运行在mesos上会比yarn上更加灵活,更加自然。

用户可以选择两种调度模式之一运行自己的应用程序:

  1. 粗粒度模式(Coarse-grained Mode):每个应用程序的运行环境由一个driver和若干个executor组成。其中每个executor占用若干资源,内部可运行多个task(对应多少个“slot”)。应用程序的各个任务真实运行之前,需要将运行环境中的资源全部申请好,且运行过程中要一直占用这些资源。即使不使用,也只有程序运行结束后,才能回收这些资源。
  2. 细粒度模式(Fine-grained Mode):将于粗粒度模式会造成大量资源浪费,spark on mesos还提供了另外一种调度模式:细粒度模式。类似于云计算,思想是按需分配。

25、spark技术栈有哪些组件,每个组件都有什么功能,适合什么应用场景?

1)spark core

是其他组件的基础,spark的内核,主要包含:优先循环图、RDD、Lingage、Cache、broadcat等,并封装了底层通讯框架,是spark的基础。

2)sparkStreaming

一个对实时数据流进行高通量、容错处理的流式处理系统。

可以对多种数据源(如kafka、flume、twitter、zero、和tcp套接字)进行类似map、reduce和join等复杂操作。

将流式计算分解成一系列短小的批处理作业。

3)spark sql

shark是sparkSQL的前身,spark sql的一个重要特点是其能够统一处理关系表和RDD,使得开发人员可以轻松的使用sql命令进行外部查询,同时进行更复杂的数据分析。

4)blinkDB

一个用于海量数据上运行交互式sql查询的大规模并行查询引擎,运行用户通过权衡数据精度来提升查询相应时间,其数据的精度被控制在运行的误差范围内。

5)MLBase

spark生态圈的一部分,专注于机器学习,让机器学习的门槛更低,让一些可能并不了解机器学习的用户也能方便的使用MLBase。MLBase分为四部分:MLlib、MLI、ML Optimizer和MLRuntime。

6)GraphX

spark中用于图并行计算的组件。

26、spark中worker的主要工作式什么?

主要功能:

  1. 管理当前节点内存、cpu的使用情况。
  2. 接受master发送过来的资源指令,通过executorRunner启动程序分配任务。
  3. 管理分配新进程,做计算的服务,相当于process服务。

ps:

  1. worker会不会汇报当前信息给master?worker心跳给master的主要只有workid,不会以心跳的方式发送资源信息给master,这样master就知道worker是否存活,只有故障的时候才会发送资源信息。
  2. worker不会运行代码,具体运行的是executor,可以运行具体的application写的业务逻辑代码,操作代码的节点。

27、简单说一下hadoop和spark的shuffle相同和差异?

1)从逻辑(high-level)上看

两者并没有大的差别。

都是将mapper(spark中式ShuffleMapTask)的输出进行partition,不同的partition送到不同的reducer(spark里reducer可能是下一个stage里的shufflemaptask,也可能是resulttask)。

reducer以内存作缓冲区,边shuffle边aggregate数据,等到数据aggregate好以后进行reduce()(spark里可能是后续的一系列操作)。

2)从具体(low-level)上看

两者差别不小。

hadoop mapreduce是sort-based,进入combine()和reduce()的records必须先sort。这样的好处在于combin/reduce可以处理大规模的数据,因为其输入数据可以通过外部排序得到(mapper对每段数据先做排序,reducer的shuffle对排好序的每段数据做归并)。

目前spark默认选择的是hash-based,通常使用hashmap来对shuffle来的数据进行aggregate,不会对数据进行提前排序。如果用户需要经过排序的数据,那么需要自己调用类似的sortByKey()的操作。

如果你是spark1.1的用户,可以将spark.shuffle.manager设为sort,则会对数据进行排序。在spark1.2中,sort将作为默认的shuffle实现。

3)从实现角度来看

两者也有不少差别。

mapreduce将处理流程划分出明显的几个阶段:map、spill、merge、shuffle、sort、reduce等。

每个阶段各司其职,可以按照过程式的编程思想来逐个实现每个阶段的功能。

在spark中,没有这样明确的阶段,只有不同的stage和一系列的transformation,所以spill、merge、aggregate等操作需要蕴含在transformation中。

ps:

如果将map端划分数据、持久化数据的过程称为shuffle write,而将reducer读入数据、aggregate数据的过程称为shuffle read。

那么在spark中,问题就变为怎么在job的逻辑或者物理执行图中加入shuffle write和shuffle read的处理逻辑?以及两个处理逻辑应该怎么高效实现?

shuffle write由于不要求数据有序,shuffle write的任务很简单,将数据partition好,并持久化。

之所以要持久化,一方面是要减少内存存储空间压力,另一方面也是为了fault-tolerance。

28、mapreduce和spark都是并行计算,那么它们有什么相同和区别?

相同

两者都是用mr模型来进行并行计算。

区别

  1. hadoop的一个作业称为job,job里面分为map task和reduce task,每个task都是在自己的进程中运行的,当task进程也会结束。
  2. spark用户提交的任务称为application,一个application对应一个sparkcontext,app中存在多个job,没触发一次action操作就会产生一个job。这些job可以并行或串行执行,每个job中有多个stage,stage是shuffle过程中DAGScheduler通过RDD之间的依赖关系划分job而来的。每个stage中有多个task,组成taskset由taskscheduler分发到各个executor中执行,executor的生命周期是和app一样的,即使没有job运行也是存在的,所以task可以快速启动读取内存进行计算。
  3. hadoop的job只有map和reduce操作,表达能力比较欠缺,而且在mr过程中会重复的读写hdfs,造成大量的io操作,多个job需要自己管理关系。
  4. spark的迭代计算都是在内存中进行的,api中提供了大量的rdd操作,如join、groupby等,而且通过DAG图可以实现良好的容错。

29、RDD机制?

RDD中文意思是弹性分布式数据集,简单的理解成一种数据结构,是spark框架上的通用货币。

所有算子都是基于rdd来执行的,不同的场景会有不同的rdd实现类,但是都可以进行互相转换。

rdd执行过程中会形成dag图,然后形成lineage(血缘)保证容错性等。

从物理的角度来看rdd存储的是block和node之间的映射。

30、spark有哪些组件?

1)master

管理集群和节点,不参与计算。

2)worker

计算节点,进程本身不参与计算,给master汇报存活状态(workerid)。

3)driver

运行程序的main方法,创建sparkcontext对象。

4)sparkcontext

控制整个application的生命周期,包括DAGScheduler和taskscheduler等组件。

5)client

用户提交程序的入口。

31、spark工作机制?

用户在client端提交作业后,会由driver运行main方法并创建sparkcontext上下文。

执行rdd算子,形成dag图输入dagscheduler,按照rdd之间的依赖关系划分stage输入taskscheduler。

taskscheduler会将stage划分为taskset分发到各个节点的executor中执行。

32、spark的优化怎么做?

spark调优比较复杂,但是大体可以分为三个方面来进行。

1)平台层面的调优

防止不必要的jar包分发,提高数据的本地性,选择高效的存储格式,如parquet。

2)应用程序层面的调优

  1. 过滤操作符的优化降低过多小任务。
  2. 降低单条记录的资源开销。
  3. 处理数据倾斜。
  4. 复用rdd进行缓存。
  5. 作业并行化执行等。。。

3)jvm层面的调优

设置合适的资源量,设置合理的jvm,启用高效的序列化方法如kyro,增大offhead内存等。。。

ps:

  1. 序列化在分布式系统中扮演着重要的角色,优化spark程序时,首当其冲的就是序列化方式的优化。
  2. Java serialization:默认的序列化方式。
  3. kryo serialization:相较于Java serialization的方式,速度更快,空间占用更小,但并不支持所有的序列化格式,同时使用的时候需要注册class。spark-sql中默认使用的是kyro的序列化方式。
  4. spark不将kyro设置为默认的序列化方式是因为它需要对类进行注册,官方强烈建议在一些网络数据传输很大的应用中使用kyro序列化。

你可能感兴趣的:(面试,大数据运算,软件管理,分布式,大数据,spark,面试)