大数据常见面试题之spark core

一.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管理,spark客户端直接连接yarn不需要格外构建spark集群.有yarn-client和yarn-cluster两种模式,主要区别在于:Driver程序的运行节点
  • 1)cluster适合生产,driver运行在集群子节点,具有容错功能
  • 2)client适合调试,driver运行在客户端

二.driver的功能

  • 一个spark作业运行时包括一个Driver进程,也是作业的主进程,具有main函数,并且具有SparkContext的实例,是程序的入口点
  • 功能:负责向集群申请资源,向master注册信息,负责了作业的调度,负责作业的解析,生成stage并调度Task到Executor上.包括DAGScheduler,TaskScheduler

三.hadoop和spark都是并行计算,他们有什么相同点和不同点

  • 两者都是用mr模型来进行并行计算,hadoop的一个作业称为job,job里面分为map task和reduce task,每个task都是在自己的进程中运行的,当task结束时,进程也会结束
  • 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可以快速启动读取内存进行计算,spark的迭代计算都是在内存中进行的,API提供了大量的RDD操作如joinmgroupby等,而且通过DAG图可以实现良好的容错
  • hadoop的job只有map和reduce操作,表达能力比较欠缺而且在mr过程中会重复的读写hdfs,造成大量的io操作,多个job需要自己管理关系

四.RDD

  • RDD(resilient distributed dataset)叫做弹性分布式数据集,是spark中最基本的数据抽象,它代表一个不可变,可分区,里面的元素可进行计算的集合
    RDD五大特性
  • 1)A list of paritions 一个分区列表,RDD中的数据都存在一个分区列表里面
  • 2)A function for computing each split 作用在每一个分区中的函数
  • 3)A list of dependencies on the RDDs 一个RDD依赖于其他多个RDD,这个点很重要,RDD的容错机制就是根据这个特性而来的
  • 4)Optionally ,a Partitioner for key-value RDDs(e.g. to say that the RDD is hash-partitioned) 可选的,针对于kv类型的RDD才具有这个特性,作用是决定了数据的来源以及数据处理后的去向
  • 5)Optionally,a list of preferred locations to compute each split on(e.g. block locations for an HDFS file) 可选项,数据本地性,数据位置最优

五.简述宽依赖和窄依赖概念,groupByKey,reduceByKey,map,filter,union都是什么依赖?

1.窄依赖

  • 指父RDD的每一个分区最多被一个子RDD的分区所用,表现为一个父RDD的分区对应于一个子RDD的分区,和两个父RDD的分区对应于一个RDD的分区.map/filter和union属于第一类,对输入进行协同划分(co-partitioned)的join属于第二类

2.宽依赖

  • 指子RDD的分区依赖于父RDD的所有分区,这是因为shuffle类操作

算子的宽窄依赖

  • 对RDD进行map,filter,union等Transformations一般是窄依赖
  • 宽依赖一般是对RDD进行groupByKey,reduceByKey等操作,就是对RDD中的partition中的数据进行重分区(shuffle)
  • join操作既可能是宽依赖也可能是窄依赖,当要对RDD进行join操作时,如果RDD进行过重分区则为窄依赖,否则为宽依赖

六.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都是HashPartitioner,默认值是父RDD中最大的分区数,这个参数通过spark.default.parallelism控制(在spark-sql中用saprk.sql.shuffle.partitions),该参数只对HashPartitioner有效,所以如果是别的Partitioner或者自己实现的Partitioner就不能用该参数来控制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资源分配均匀

6.使用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时间

七.stage task 和 job的区别和划分方式

  • job:一个由多个任务组成的并行计算,当你需要执行一个rdd的action时,会生成一个job
  • stage:每个job被拆分成更小的被称作stage(阶段)的task(任务)组,stage彼此之间是相互依赖的,各个stage会按照执行顺序依次执行
  • task:一个将要被发送到executor中的工作单元,是stage的一个任务执行单元,一般来说,一个rdd有多少个partition,就会有多少个task,因为每一个task只是处理一个partition上的数据

八.spark提交作业参数

  • executors-cores ------每个executor使用的内核数,默认为1,官方建议为2-5个
  • num-executors ------启动executors的数量,默认为2
  • executor-memory ------executor内存大小,默认1G
  • driver-cores ------driver使用的内核数,默认为1
  • driver-memory ------driver内存大小,默认512M
#如下是一个提交任务的样式:
spark-submit \
--master local[5]	\
--driver-cores	2	\
--driver-memory	8g	\
--executor-cores	4	\
--num-executors	10	\
--executor-memory	8g	\
--class PackageName.ClassName	XXXX.jar	\
--name "spark job name"	\
InputPath	\
OutputPath

九.reduceByKey和groupByKey的区别

  • reduceByKey用于对每个key对应的多个value进行merge操作,最重要的是它能够在本地先进行merge操作,并且merge操作可以通过函数自定义
  • groupByKey也是对每个key对应的多个value进行操作,但是只是汇总生成一个sequence,本身不能自定义函数,只能额外通过map(func)来实现
  • 在大的数据集上,reduceByKey的效果比groupByKey的效果更好些,因为reduceBykey会在shuffle之前对数据进行合并,传输速度优于groupByKey
  • combineByKey,是一个比较底层的算子,reduceByKey就调用了该算子

十.foreach和map的区别

  • 两个方法的共同点:都是用于遍历集合对象,并对每一项执行指定的方法
  • 两者的区别:
  • 1)foreach无返回值(准确来说返回Unit),map返回集合对象.foreach用于遍历集合,而map在于映射集合到另一个集合
  • 2)foreach中的处理逻辑是串行的,map中的处理.逻辑是并行的
  • 3)map是转换算子,foreach是行动算子

十一.map和mapPartitions的区别

相同点:map与mapPartitions都属于转换算子
区别:

  • 1.本质
  • 1)map是对rdd中每一个元素进行操作
  • 2)mapPartitions则是对rdd中的每个分区的迭代器进行操作
  • 2.RDD中每个分区数量不大的情形
  • 1)map操作性能地下,比如一个partition中有一万条数据,那么在分析每个分区时,function要执行和计算一万次
  • 2)mapPartitions性能较高,使用mapPartitions操作之后,一个task仅仅会执行一次function.function一次接收所有的partition数据,只要执行一次就可以了,性能比较高
  • 3.RDD中的每个分区数据量超大的情形:比如一个Partition有100万条数据
  • 1)map能正常执行完
  • 2)mapPartitions一次传入一个function后,可能一下子内存不够用,造成OOM(内存溢出)

十二.foreach和foreachPartition的区别

相同点:foreach和foreachPartition都属于行动算子
区别:

  • 1)foreach每次处理RDD中的一条数据
  • 2)foreachPartition每次处理RDD中的每个分区的迭代器中的数据

十三.sortByKey是全局排序吗?

sortByKey是全局排序

  • 1)在sortByKey之前将数据使用partitioner根据数据范围来分
  • 2)使得p1分区所有的数据小于p2,p2分区所有的数据小于p3,以此类推(p1-pn是分区标识)
  • 3)然后使用sortByKey算子针对每一个Partition进行排序,这样全局的数据就被排序了

十四.coalesce与repartition的区别

  • 我们常认为coalesce不产生shuffle回避repartition产生shuffle效率高,而实际情况往往要根据具体问题具体分析,coalesce效率不一定高,有时还有大坑,要慎用此算子
  • coalesce与repartition都是对RDD的分区进行重新划分,repartition只是coalesce接口中shuffle为true的实现

例子详解

  • 假设源RDD有N个分区,需要重新划分为M个分区
  • 如果N<M.一般情况下N个分区有数据分布不均匀的状况.利用HashPartitioner函数将数据重新分区为M个,这时需要将shuffle设置为true(repartition实现,coalesce也实现不了)
  • 如果N>M并且和M相差不多,(假如N为1000,M为100)那么就可以将N个分区中的若干个分区合并成一个新的分区,最终合并成为M个分区,这时可以将shuffle设置为false(coalesce实现),如果M>N时,coalesce是无效的,不进行shuffle过程,父RDD和子RDD之间是窄依赖关系,无法使文件数(partition)变多,总之如果shuffle为false时,如果传入的参数大于现有的分区数目,RDD的分区数不变,也就是说不经过shuffle,是无法将RDD的分区数变多的
  • 如果N>M并且两者相差悬殊,这时要看executor数与要生成的partitions关系,如果executor数<=要生成partition数,coalesce效率高,反之如果用coalesce会导致(executor数-要生成partition数)个executor空跑从而降低效率.如果在M为1的时候,为了使coalesce之前的操作有更好的并行度,可以将shuffle设置为true

十五.spark血统-即RDD之间依赖关系

  • 处理分布式运算环境下的数据容错性(节点失效/数据丢失)问题采用的方案.为了保证RDD中数据的鲁棒性(也叫健壮性),RDD数据集通过所谓的血统关系(lineage)记住了它是如何从其他RDD中演变过来的.相比其他系统的细颗粒度的内存数据更新级别的备份或者LOG机制,RDD的lineage记录的是粗颗粒度的特定数据转换(Transformation)操作行为.当这个RDD的部分分区数据丢失时,它可以通过lineage获取足够的信息来重新运算和恢复丢失的数据分区.这种粗颗粒的数据模型,限制了spark的运用场合,但同时相比细颗粒度的数据模型,也带来了性能的提升
  • RDD在lineage依赖方面分为两种:窄依赖与宽依赖,用来解决数据容错时的高效性
  • **窄依赖:**是指父RDD的每一个分区最多被一个子RDD的分区对应一个子RDD的分区,表现为一个父RDD的的分区对应于一个子RDD的分区,或多个父RDD的分区对应一个子RDD的分区,也就是说一个父RDD的一个分区不可能对应一个子RDD的多个分区
  • **宽依赖:**是指子RDD的分区依赖于父RDD的多个分区或所有分区,也就是说存在一个父RDD的一个分区对应一个子RDD的多个分区
  • 对于宽依赖,这种计算的输入和输出在不同的节点,lineage方法对于输入节点完好,而输出节点宕机时,通过重新计算这种情况下这种方法是有效的,否则无效,因为无需重试,需要向上其祖先追溯看是否可以重试(这就是lineage,血统的意思),窄依赖对于数据的重算开销要远小于宽依赖的数据重算开销
  • 在RDD计算,通过checkpoint进行容错,做checkpoint有两种方式,一个是checkpoint data,一个是logging the updates.用户可以控制采用哪种方式来实现容错,默认是logging the updates方式,通过记录跟踪所有生成RDD的转换,也就是记录每个RDD的血统来重新计算生成丢失的分区数据

十六.spark RDD 的持久化

1.cache() 和 persist()

  • 当对RDD执行持久化操作时,每个节点都会将自己操作的RDD的partition持久化到内存中,并且在之后对该RDD的反复使用中,直接使用内存缓存的partition,这样的话,对于针对一个RDD反复执行多个操作的场景,就只要对RDD计算一次即可,后面直接使用该RDD,而不需要计算多次该RDD
  • 巧妙使用RDD持久化,甚至在某些场景下,可以将spark应用程序的性能提升10倍,对于迭代式算法和快速交互式应用来说,RDD持久化是非常重要的
  • 要持久化一个RDD,只要调用其cache()或者persist()方法即可.在该RDD第一次被计算出来时,就会直接换存在每个节点中,而且spark的持久化机制还是自动容错的.如果持久化的RDD的任何partition丢失了,那么spark会自动通过其源RDD使用转换操作重新计算该partition
  • cache()和persist()的区别在于:cache是persist的一种简化方式,cache的底层就是调用的persist的无参版本,同时就是调用persist(MEMORY_ONLY),将数据持久化到内存中.如果需要从内存中去除缓存,那么就可以调用unpersist方法

2.checkPoint

场景:

  • 当业务场景非常复杂的时候,RDD的lineage依赖会非常的长,一旦血统较后的RDD数据丢失时,spark会根据血统依赖重新计算丢失的RDD,这样会造成计算的时间过长,spark提供了一个叫checkPoint的算子来解决这样的业务场景

使用:

  • 为当前RDD设置检查点,该函数会创建一个二进制文件,并存储到checkPoint目录中,该目录是使用SparkContext.setCheckpointDir()设置的.在checkpoint的过程中,该RDD的所有依赖于父RDD中的信息将全部被移除.对RDD进行checkpoint操作并不会马上被执行,必须执行Action操作才能触发

checkPoint的优点:

  • 持久化在hdfs上,hdfs默认的3副本备份使得持久化的备份数据更加安全
  • 切断RDD的依赖关系:当业务场景复杂的时候,RDD的依赖关系非常的长的时候,当靠后的RDD数据丢失的时候,会经历较长的重新计算的过程,采用checkPint会转为依赖checkPointRDD,可以避免长的lineage重新计算
  • 建议checkpoint之前进行cache操作,这样会直接将内存中的结果进行checkPoint,不用重新启动job重新计算

checkPoint原理:

  • 当finalRDD执行Action类算子计算job任务的时候,spark会从finalRDD从后往前回溯查看哪些RDD使用了checkPoint算子
  • 将使用了checkPoint的算子标记
  • spark会自动地启动一个job来重新计算标记了的RDD,并将计算的结果存入hdfs,然后切断RDD的依赖关系

十七.spark提交任务流程

1.standalone-client方式提交任务

  • 1)client模式下提交任务,在客户端启动driver进程
  • 2)driver会向master申请启动application启动的资源
  • 3)资源申请成功,driver端将task发送到worker端执行
  • 4)worker将task执行结果返回到driver端

2.standalone-cluster方式提交任务

  • 1)standalone-cluster模式提交app后,会向master请求启动driver
  • 2)master接受请求后,随机在集群中一台节点启动driver进程
  • 3)driver启动后为当前的应用程序申请资源
  • 4)driver端发送task到worker节点上执行
  • 5)worker将执行情况和执行结果返回给driver端

3.yarn-client方式提交任务

  • 1)客户端提交一个application,在客户端启动一个driver进程
  • 2)应用程序启动后会向RS(ResourceManager)发送请求,启动AM(ApplicationMaster)的资源
  • 3)RS收到请求,随机选择一台NM启动AM,这里的NM相当于standalone中的worker节点
  • 4)AM启动后,会向RS请求一批container资源,用于启动executor
  • 5)RS会找到一批NM返回给AM,用于启动Executor

4.yarn-cluster方式提交任务

  • 1)客户机提交application应用程序,发送请求到RS,请求启动AM
  • 2)RS收到请求后随机在一台NM上启动AM(相当于driver端)
  • 3)AM启动,AM发送请求到RS,请求一批container用于启动executor
  • 4)RS返回一批NM节点给AM
  • 5)AM连接到NM,发送请求到NM启动executor
  • 6)executor反向注册到AM所在节点的driver.driver发送task到executor

十八.spark join的优化

  • spark作为分布式的计算框架,最为影响其执行效率的地方就是频繁的网络传输.所以一般的在不存在数据倾斜的情况下,想要提高spark job的执行效率,就尽量减少job的shuffle过程(减少 job 的stage)或者减小shuffle带来的影响
  • 1)尽量减少参与join的RDD的数据量
  • 2)尽量避免参与join的RDD都具有重复的key
  • 3)尽量避免或者减少shuffle过程
  • 4)条件允许的情况下,使用map-join 完成join

十九.spark 的shuffle方式

shuffle方式共分三种,分别是:HashShuffle,SortShuffle(默认),TungstenShuffle
在spark程序中设置方式:通过设置spark.shuffle.manager进行配置:

//  可设置为hash sort tungsten-sort
  private val session: SparkSession = SparkSession.builder()
    .appName("xxx").master("local[*]").
    config("spark.shuffle.manager","hash").getOrCreate()

HashShuffleManager特点:

  • 1)数据不进行排序,速度较快
  • 2)直接写入缓冲区,缓冲区写满后溢写为文件
  • 3)本ShuffleMapStage的每一个task会生成与下一个ShuffleMapStage并行度相同的文件数量
  • 4)海里文件操作句柄和临时缓存信息,占用内存容易内存溢出

SortShuffleManager的特点:

  • 1)会对数据进行排序
  • 2)在写入缓存之前,如果是reduceByKey之类的算子,则会先写入到一个Map内存数据结构中,而如果是join之类的算子,则先写入到Array内存数据结构中.在每条数据写入前先判断是否当到达一定阀值,到达则写入到缓冲区
  • 3)复用一个core的task会写到同一个文件里,并生成一个索引文件.其中记录了下一个ShuffleMapStage中每一个task所要拉取数据的start offset 和end offset

二十.广播变量作用

  • 使用广播变量,每个executor的内存中只驻留一份变量副本,而不是对每个task都传输一次大变量,省了很多的网络传输,对性能提升有很大的帮助,而且会通过高效的广播算法(比特洪流技术)来减少传输代价
  • 使用广播变量的场景很多,我们都知道spark一种常见的优化方式就是小表广播,使用map join来代替reduce join,我们通过把小数据集广播到各个节点上,节省了一次特别expensive的shuffle操作
  • 比如driver上有一张数据量很小的表,其他节点上的task都需要lookup这张表,那么driver可以先把这张表copy到这些节点,这样task就可以在本地查表了

廿一.数据倾斜解决方案

  • 数据倾斜的发生一般都是一个key对应的数据过大,而导致task执行过慢,或者内存溢出,OOM,一般发生在shuffle的时候,比如reduceByKey,countByKey,groupByKey容易产生数据倾斜
  • 如何解决数据倾斜,首先看log日志信息,因为log日志报错时会提示在哪些行,然后就是检查发生shuffle的地方,这些地方比较容易发生数据倾斜

方案一:聚合源数据

  • 我们的数据一般来源于hive表,那么在生成hive表的时候对数据进行聚合,按照key进行分组,将key对应的所有values以另一种格式存储,比如拼接一个字符串这样的话,可以省略groupByKey和reduceByKey的操作,那么没有这样操作的话,就不用shuffle了,没有shuffle的话不可能出现数据倾斜,如果不能完美拼接,但是能少量拼接也能减少key对应的数据量,也可提高性能

方案二:过滤导致倾斜的key

  • 这种方案就是说如果业务允许或者沟通后能理解的话,我们可以把大量的key进行过滤,这样可以轻松解决问题

方案三:提高shuffle操作reduce并行度

  • 通过提高reduce端的task执行数量,来分担数据压力,也就是说将task执行数量提高,性能也会相应提高,这样的方式如果在运行中确实解决了数据倾斜是最好的,但是如果出现之前运行时候九OOM了,加大了reduce端task的数量,可以运行了,但是执行时间相当的长,那么就放弃这个方案

方案四:利用双重聚合

  • 用于groupByKey和reduceByKey,比较适用于join,但是通常不用这样做,也就是说首先第一轮对key进行打散,将原来一样的key变成不一样的key(前面加前缀),相当于将一样的key分了多个组,然后进行局部聚合,接着除掉每个key的前缀,然后再进行全局聚合,进行两次聚合,避免数据倾斜问题

方案五:reduce join 转换成 map join

  • 如果两个rdd进行join,有一个表比较小的化,可以将小表广播出去,这样每个节点的blockmanager中都有一份,这样的话根本不会发生shuffle,那么也就确定不会存在数据倾斜问题.如果join中有数据倾斜的情况,第一时间考虑这样的方式,但是如果两个表都很大,那么就不用这种方案,这种方案是牺牲一点点内存换来性能提升

方案六:sample抽样分解聚合

  • 也就是说将倾斜的key单拉出来,然后用一个rdd进行打乱join

方案七:使用随机数和扩容进行join

  • 也就是说通过faltMap进行扩容,然后再将随机数打入进去,再进行join,这样的话不能根本的解决数据倾斜,但是可以有效的缓解数据倾斜问题,也会提高性能

廿二.spark通信机制

spark消息通信主要分成三个部分:整体框架,启动消息通信,运行时消息通信

1.概述

  • spark(旧版本)的远程进程通信(RPC)是通过Akka类库来实现的,Akka使用scala语言开发,基于Actor并发模型实现,Akka具有高可靠,高性能,可扩展等特点

2.具体通信流程

  • 1)首先启动Master进程,然后启动所有的worker进程
  • 2)worker启动后,在preStart方法中与Master建立连接,向Master发送注册信息,将worker的信息通过case class 封装起来发送给Master
  • 3)Master接收到Worker的注册消息后将其通过集合保存起来,然后向worker反馈注册成功的消息
  • 4)worker会定期向Master发送心跳包,领受新的计算任务
  • 5)Master会定期清理超时的Worker

3.通信框架

  • spark2.2使用Netty作为master与worker的通信框架,spark2.0之前使用的akka框架
  • 1)spark启动消息通信:
  • worker向master发送注册消息,master处理完毕后返回注册成功或者是失败的消息,如果成功,worker向master定时发送心跳
  • 2)spark运行时消息通信
  • 应用程序SparkContext向master发送注册消息,并由master为该应用分配Executor,executor启动之后会向SparkContext发送注册成功消息,然后SparkContext的rdd触发Action之后会形成一个DAG,通过DAGScheduler进行划分Stage并将其转化成TaskSet,然后TaskScheduler向Executor发送执行消息,Executor接收到信息之后启动并且运行,最后是由Driver处理结果并回收资源

猜你喜欢

转载自blog.csdn.net/sun_0128/article/details/107778530