Spark 经典面试题汇总《一》

1你是怎么理解Spark,它的特点是什么?
Spark是一个基于内存的,用于大规模数据处理(离线计算、实时计算、快速查询(交互式查询))的统一分析引擎。

它内部的组成模块,包含SparkCore,SparkSQL,Spark Streaming,SparkMLlib,SparkGraghx等。

Spark的主要特点包括:

快:Spark计算速度是MapReduce计算速度的10-100倍,Spark使用内存计算技术,以及基于弹性分布式数据集(RDD)的计算模型,可以在内存中对数据进行高效处理,从而比传统的基于磁盘的计算系统更快速。
容错性:Spark可以在节点故障时重新计算丢失的数据,从而避免了数据丢失的问题,保证了任务的可靠性。
多语言支持:Spark提供了多种编程语言API,包括Java、Scala、Python和R等,使得开发者可以使用自己熟悉的语言进行数据处理任务。
数据处理能力:Spark可以处理各种类型的数据,包括结构化数据、半结构化数据和非结构化数据等,并且支持各种数据源的读写操作,如HDFS、Hive、MySQL等。
可扩展性:Spark可以在大规模集群上运行,支持自动分区和并行化处理,从而可以处理PB级别的数据。
总的来说,Spark具有高效的性能、容错性、多语言支持、强大的数据处理能力和良好的可扩展性,适用于各种大规模数据处理任务,如机器学习、图像处理、数据挖掘、日志分析等。

2Spark有几种部署方式,请分别简要论述?
Spark有三种常见的部署方式,分别是本地模式、单例模式和Yarn模式。

1) Local:运行在一台机器上,通常是练手或者测试环境。

2)Standalone:构建一个基于Mster+Slaves的资源调度集群,Spark任务提交给Master运行。是Spark自身的一个调度系统。

3)Yarn: Spark客户端直接连接Yarn,不需要额外构建Spark集群。有yarn-client和yarn-cluster两种模式,主要区别在于:Driver程序的运行节点。

3Spark提交作业的参数
因为我们Spark任务是采用的Shell脚本进行提交,所以一定会涉及到几个重要的参数,而这个也是在面试的时候容易被考察到的“细节”。

executor-cores —— 每个executor使用的内核数,默认为1,官方建议2-5个,我们企业是4个
num-executors —— 启动executors的数量,默认为2
executor-memory —— executor内存大小,默认1G
driver-cores —— driver使用内核数,默认为1
driver-memory —— driver内存大小,默认512M
在使用Spark提交作业时,可以使用以下参数:

--class:指定要运行的主类名。
--master:指定要连接的Spark集群的URL。例如,--master yarn将连接到YARN集群。
--deploy-mode:指定作业的部署模式,可选值为client或cluster。client模式是在客户端启动Driver程序,cluster模式则是在集群中启动Driver程序。
--executor-memory:指定每个Executor可用的内存量。例如,--executor-memory 4g将为每个Executor分配4GB内存。
--num-executors:指定要启动的Executor数量。例如,--num-executors 10将启动10个Executor。
--executor-cores:指定每个Executor可用的CPU核心数量。例如,--executor-cores 2将为每个Executor分配2个CPU核心。
--conf:用于设置Spark配置属性。例如,--conf spark.shuffle.compress=true将启用Shuffle压缩。
--jars:用于指定需要在作业中使用的JAR文件。例如,--jars /path/to/jar1,/path/to/jar2将加载jar1和jar2。
--files:用于指定需要在作业中使用的文件。例如,--files /path/to/file1,/path/to/file2将加载file1和file2。

更多参数和说明可以通过运行spark-submit --help来查看。
4简述Spark的作业提交流程
Spark的任务提交方式实际上有两种,分别是YarnClient模式和YarnCluster模式。大家在回答这个问题的时候,也需要分类去介绍。千万不要被冗长的步骤吓到,一定要学会总结差异,发现规律,通过图形去增强记忆。

YarnClient  运行模式介绍


YarnCluster 模式介绍


Spark作业的提交流程如下:

编写应用程序:首先需要编写Spark应用程序。这通常是一个基于Spark API的Scala、Java或Python脚本,它定义了数据处理的流程和逻辑。
打包应用程序:使用构建工具(如sbt、Maven或Gradle)将应用程序打包成JAR文件。
准备环境:确保集群中的所有节点都安装了相同版本的Spark,并且应用程序所需的所有依赖项都已安装。
启动Spark集群:在集群的一个节点上启动Spark Master,然后启动一些Spark Worker进程。
提交作业:使用spark-submit命令提交应用程序,命令包括应用程序的JAR文件和一些参数,例如作业名称、Master节点URL、作业配置等。
分配资源:Spark提交器将根据作业配置、集群资源可用性和其他因素,将作业分配给可用的Worker节点,并为每个Executor分配资源。
运行任务:一旦资源分配完成,Spark将启动Driver程序,并将作业的任务发送给Worker节点上的Executor进行执行。
监控进度:Spark Web UI提供了有关作业进度和性能的实时监控信息,可以用于诊断问题或调整配置。
收集结果:一旦作业完成,Spark会将结果收集并返回给Driver程序。在驱动程序中,可以将结果保存到外部存储系统或进行进一步处理。
总之,Spark作业提交流程是一个复杂的过程,需要综合考虑应用程序、集群配置和资源分配等多个因素。熟练掌握Spark作业提交流程是成为一名优秀Spark开发人员的关键。

5你是如何理解Spark中血统(RDD)的概念?它的作用是什么?
概念
RDD是弹性分布式数据集,是Spark中最基本的数据抽象,代表一个不可变、可分区、里面的元素可并行计算 的集合。

作用
提供了一个抽象的数据模型,将具体的应用逻辑表达为一系列转换操作(函数)。另外不同RDD之间的转换操作之间还可以形成依赖关系,进而实现管道化,从而避免了中间结果的存储,大大降低了数据复制、磁盘IO和序列化开销,并且还提供了更多的API(map/reduec/filter/groupBy...)

特点
容错性:RDD具有容错性,因为它会自动将数据划分成多个分区,并在集群中的多个节点上进行复制,从而实现数据的高可靠性和容错性。
数据共享:RDD允许多个并行操作共享相同的数据集合,以便在不同的计算步骤中复用数据,从而避免了重复的IO操作,提高了计算效率。
优化计算:RDD通过支持多个转换操作和行动操作,允许进行复杂的计算和数据分析,同时也支持对计算过程进行优化,以便最大限度地减少计算成本。
血统跟踪:RDD通过记录其前一个RDD的依赖关系,构建了一个有向无环图(DAG)来跟踪其数据处理流程,从而允许Spark在节点故障时重新计算丢失的分区,实现了弹性计算。
血统是指RDD之间的依赖关系,这种依赖关系可以通过DAG(有向无环图)来表示。每个RDD都会记录其父RDD的引用和产生该RDD的转换操作,这样,如果某个RDD的分区丢失或出现故障,Spark可以根据血统信息重新计算该RDD的丢失分区,实现了弹性计算。因此,RDD的血统跟踪是Spark实现容错性的重要机制。






6简述Spark的宽窄依赖,以及Spark如何划分stage,每个stage又根据什么决定task个数?
spark的宽窄依赖问题是SparkCore部分的重点考察内容,多数出现在笔试中,大家需要注意。

窄依赖:父RDD的一个分区只会被子RDD的一个分区依赖

宽依赖:父RDD的一个分区会被子RDD的多个分区依赖(涉及到shuffle)

那Stage是如何划分的呢?

根据RDD之间的依赖关系的不同将Job划分成不同的Stage,遇到一个宽依赖则划分一个Stage。
每个stage又根据什么决定task个数?
Stage是一个TaskSet,将Stage根据分区数划分成一个个的Task。
这里为了方便大家理解,贴上一张过程图

Spark中的宽依赖和窄依赖是指RDD之间的依赖关系类型。在Spark中,每个RDD都有一个或多个父RDD和一个或多个子RDD,RDD之间的依赖关系分为宽依赖和窄依赖两种类型:

窄依赖(Narrow Dependency):指一个RDD的每个分区只依赖于父RDD的一个或多个分区,父RDD的每个分区最多只被一个子RDD的分区使用。窄依赖的特点是数据局部性高,可以在同一个节点上完成计算,从而提高计算效率。
宽依赖(Wide Dependency):指一个RDD的一个或多个分区依赖于父RDD的多个分区,或者父RDD的同一个分区被多个子RDD的分区使用。宽依赖的特点是数据局部性较低,需要进行数据的洗牌操作(Shuffle),从而增加了计算成本和网络传输开销。
在Spark中,每个宽依赖和窄依赖之间的转换都会形成一个Stage,每个Stage包含一组具有相同依赖关系的Task。一个Stage中的Task个数由多个因素决定,包括可用的CPU核心数、可用内存大小、数据分区数等。具体来说,Spark会将RDD划分成多个分区,并在每个分区上执行一个Task,以便实现并行计算。Task的个数通常等于RDD的分区数,这样可以确保所有Task都具有相同的计算量,并且可以在不同的节点上并行执行。

在Spark中,Stage划分的基本原则是:如果两个RDD之间存在宽依赖,那么它们就属于不同的Stage。这是因为宽依赖需要进行Shuffle操作,需要将数据从多个节点收集到一个节点上进行计算,这会产生较大的网络开销和计算成本。因此,将宽依赖放在不同的Stage中可以提高计算效率。而对于窄依赖,Spark会尽量将它们放在同一个Stage中,以便在同一个节点上执行计算,从而提高计算效率。

7列举Spark常用的transformation和action算子,有哪些算子会导致Shuffle?
Spark中常用的transformation算子有:
map:对RDD中的每个元素应用一个函数,返回一个新的RDD。

filter:对RDD中的每个元素应用一个谓词函数,返回一个包含满足谓词的元素的新RDD。

flatMap:类似于map,但是每个输入元素可以映射到多个输出元素,返回一个新的RDD。

groupByKey:将具有相同key的元素进行分组,返回一个(key, values)的Tuple,其中values是一个迭代器。

reduceByKey:将具有相同key的元素进行分组,并将每个key对应的values应用一个reduce函数,返回一个(key, reduced value)的Tuple。

join:对两个具有相同key的RDD进行join操作,返回一个新的RDD。

常用的action算子有:
count:返回RDD中元素的个数。

collect:将RDD中的所有元素收集到Driver节点上,并返回一个数组。

first:返回RDD中的第一个元素。

take:返回RDD中前n个元素。

reduce:对RDD中的元素应用一个reduce函数,返回一个单个值。

上述算子中,groupByKey、reduceByKey、join等算子会导致Shuffle操作,因为它们需要将具有相同key的元素进行分组,而这些元素通常分布在不同的节点上。Shuffle操作会将数据从不同的节点收集到一个节点上进行计算,因此需要消耗大量的网络和计算资源。
join()和cogroup():这两个算子需要将具有相同键的元素进行连接操作,也需要进行Shuffle操作。

sortByKey():这个算子需要对RDD中的元素进行排序,因此需要进行Shuffle操作。

repartition()和coalesce():这两个算子都需要对RDD进行重新分区操作,需要进行Shuffle操作。

8reduceByKey与groupByKey的区别,哪一种更具优势?
reduceByKey:按照key进行聚合,在shuffle之前有combine(预聚合)操作,返回结果是RDD[k,v]。
groupByKey:按照key进行分组,直接进行shuffle
所以,在实际开发过程中,reduceByKey比groupByKey,更建议使用。但是需要注意是否会影响业务逻辑。

9Repartition和Coalesce 的关系与区别,能简单说说吗?
1)关系:
两者都是用来改变RDD的partition数量的,repartition底层调用的就是coalesce方法:coalesce(numPartitions, shuffle = true)

2)区别:
repartition一定会发生shuffle,coalesce 根据传入的参数来判断是否发生shuffle。

一般情况下增大rdd的partition数量使用repartition,减少partition数量时使用coalesce。

10简述下Spark中的缓存(cache和persist)与checkpoint机制,并指出两者的区别和联系
关于Spark缓存和检查点的区别,大致可以从这3个角度去回答:
位置

Persist 和 Cache将数据保存在内存,Checkpoint将数据保存在HDFS
生命周期

Persist 和 Cache  程序结束后会被清除或手动调用unpersist方法,Checkpoint永久存储不会被删除。
RDD依赖关系

Persist 和 Cache,不会丢掉RDD间的依赖链/依赖关系,CheckPoint会斩断依赖链。
11简述Spark中共享变量(广播变量和累加器)的基本原理与用途
关于Spark中的广播变量和累加器的基本原理和用途,

累加器(accumulator)是Spark中提供的一种分布式的变量机制,其原理类似于mapreduce,即分布式的改变,然后聚合这些改变。累加器的一个常见用途是在调试时对作业执行过程中的事件进行计数。
广播变量是在每个机器上缓存一份,不可变,只读的,相同的变量,该节点每个任务都能访问,起到节省资源和优化的作用。它通常用来高效分发较大的对象。
12当Spark涉及到数据库的操作时,如何减少Spark运行中的数据库连接数?
嗯,有点“调优”的味道,感觉真正的“风暴”即将到来,这道题还是很好回答的,我们只需要减少连接数据库的次数即可。

使用foreachPartition代替foreach,在foreachPartition内获取数据库的连接。
13能介绍下你所知道和使用过的Spark调优吗?
资源参数调优
num-executors:设置Spark作业总共要用多少个Executor进程来执行

executor-memory:设置每个Executor进程的内存

executor-cores:设置每个Executor进程的CPU core数量

driver-memory:设置Driver进程的内存

spark.default.parallelism:设置每个stage的默认task数量

开发调优
避免创建重复的RDD

尽可能复用同一个RDD

对多次使用的RDD进行持久化

尽量避免使用shuffle类算子

使用map-side预聚合的shuffle操作

使用高性能的算子

①使用reduceByKey/aggregateByKey替代groupByKey

②使用mapPartitions替代普通map

③使用foreachPartitions替代foreach

④使用filter之后进行coalesce操作

⑤使用repartitionAndSortWithinPartitions替代repartition与sort类操作
广播大变量
在算子函数中使用到外部变量时,默认情况下,Spark会将该变量复制多个副本,通过网络传输到task中,此时每个task都有一个变量副本。如果变量本身比较大的话(比如100M,甚至1G),那么大量的变量副本在网络中传输的性能开销,以及在各个节点的Executor中占用过多内存导致的频繁GC(垃圾回收),都会极大地影响性能。
使用Kryo优化序列化性能
优化数据结构
在可能以及合适的情况下,使用占用内存较少的数据结构,但是前提是要保证代码的可维护性。

14如何使用Spark实现TopN的获取(描述思路或使用伪代码)?
使用Spark实现TopN的一般思路是先使用MapReduce或者Spark计算出各个数据的得分(或者其他排序依据),然后再对这些得分进行排序,最后取出前N个得分最高的数据。其中,排序的过程是需要进行全局操作的,会产生Shuffle操作,因此在性能上需要考虑。

以下是一种使用Spark进行TopN操作的伪代码:

读取数据并将数据转换为RDD格式 rdd = sc.textFile("path/to/data").map(parse_data)
计算每个数据的得分 scores_rdd = rdd.map(lambda data: (data, compute_score(data)))
对得分进行排序 sorted_scores_rdd = scores_rdd.sortBy(lambda score: score[1], ascending=False)
取出前N个得分最高的数据 topN_rdd = sorted_scores_rdd.take(N)
其中,parse_data函数用于将原始数据解析成程序中需要的格式,compute_score函数用于计算数据的得分。在第二步和第三步中,需要根据实际情况选择合适的算子,如map()、reduceByKey()、sortBy()等。



作者: 薛秋艳


欢迎关注微信公众号 :大数据球球