大数据面试系列之——Spark

Apache Spark 是专为大规模数据处理而设计的快速通用的计算引擎。

1.Spark有几种部署模式,各个模式的特点

  • 1.本地模式
    • Spark不一定非要跑在hadoop集群,可以在本地,起多个线程的方式来指定。方便调试,本地模式分三类
      • local:只启动一个executor
      • local[k]: 启动k个executor
      • local:启动跟cpu数目相同的 executor
  • 2.standalone模式
    • 分布式部署集群,自带完整的服务,资源管理和任务监控是Spark自己监控,这个模式也是其他模式的基础
  • 3.Spark on yarn模式
    • 分布式部署集群,资源和任务监控交给yarn管理
    • 粗粒度资源分配方式,包含cluster和client运行模式
      • cluster 适合生产,driver运行在集群子节点,具有容错功能
      • client 适合调试,dirver运行在客户端
  • 4.Spark On Mesos模式
    • 粗粒度(coarse-grained),优点:启动task的时候开销比较小,但是该模式运行的时候每个application会一直占有一定的资源,直到整个application结束后才会释放资源
    • 细粒度(fine-grained),细粒度模式在spark2.0后开始弃用,优点:支持资源抢占,缺点:spark中运行的每个task的运行都需要去申请资源,也就是说启动每个task都增加了额外的开销

2.Spark技术栈有那些组件,每个组件有什么功能,分别适用与什么场景

  • 1.Spark core
    • 是其他组件的基础,spark的内核
    • 主要包括:有向循环图、RDD、Lingage、Cache、broadcast等
  • 2.SparkStreaming
    • 是一个对实时数据流进行高通量、容错处理的流式处理系统
    • 将流式计算分解成一系列短小的批处理作业
  • 3.Spark sql
    • 能够统一处理关系表和RDD,使得开发人员可以轻松地使用SQL命令进行外部查询
  • 4.MLBase
    • 是Spark生态圈的一部分专注于机器学习,让机器学习的门槛更低
    • MLBase分为四部分:MLlib、MLI、ML Optimizer和MLRuntime。
  • 5.GraphX
    • 是Spark中用于图和图并行计算

3.Spark有哪些组件

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

4.spark工作机制

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

5. Spark应用程序的执行过程

  • 1.构建Spark Application的运行环境(启动SparkContext)
  • 2.SparkContext向资源管理器(可以是Standalone、Mesos或YARN)注册并申请运行Executor资源;
  • 3.资源管理器分配Executor资源,Executor运行情况将随着心跳发送到资源管理器上;
  • 4.SparkContext构建成DAG图,将DAG图分解成Stage,并把Taskset发送给Task Scheduler;
  • 5.Executor向SparkContext申请Task,Task Scheduler将Task发放给Executor运行,SparkContext将应用程序代码发放给Executor;
  • 6.Task在Executor上运行,运行完毕释放所有资源。

6.driver的功能是什么

  • 一个spark作业运行时包括一个Driver进程,也是作业的主进程,具有main函数,并且有SparkContext实例,是程序的入口
  • 功能
    • 向集群申请资源
    • 负责作业的调度和解析
    • 生成Stage并调度Task到Executor上(包括DAGScheduler,TaskScheduler)

7.Spark中Work的主要工作是什么?

  • 1.管理当前节点内存,CPU的使用状况,接收master分配过来的资源指令,通过ExecutorRunner启动程序分配任务
  • 2.worker就类似于包工头,管理分配新进程,做计算的服务,相当于process服务
  • 3.worker不会运行代码,具体运行的是Executor是可以运行具体appliaction写的业务逻辑代码

8.task有几种类型?

  • resultTask类型,最后一个task
  • shuffleMapTask类型,除过最后一个task其他都是

9.什么是shuffle,以及为什么需要shuffle?

  • shuffle中文翻译为洗牌,需要shuffle的原因是:某种具有共同特征的数据汇聚到一个计算节点上进行计算

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

  • 因为程序在运行之前,已经申请过资源了,driver和Executors通讯,不需要和master进行通讯的。

11.Spark并行度怎么设置比较合适

  • spark并行度,每个core承载2~4个partition(并行度)
  • 并行度和数据规模无关,只和内存和cpu有关

12.Spark程序执行,有时候默认为什么会产生很多task,怎么修改默认task执行个数?

  • 有很多小文件的时候,有多少个输入block就会有多少个task启动
  • spark中有partition的概念,每个partition都会对应一个task,task越多,在处理大规模数据的时候,就会越有效率

13.Spark中数据的位置是被谁管理的?

  • 每个数据分片都对应具体物理位置,数据的位置是被blockManager管理

14.为什么要进行序列化

  • 减少存储空间,高效存储和传输数据
  • 缺点:使用时需要反序列化,非常消耗CPU

15.Spark如何处理不能被序列化的对象?

  • 封装成object

16.Spark提交你的jar包时所用的命令是什么?

  • spark-submit

17.Mapreduce和Spark的相同和区别?(重点面试题)

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

  • hadoop的一个作业:job

    • job分为map task和reduce task,每个task都是在自己的进程中运行的
    • 当task结束时,进程也会结束
  • spark用户提交的任务:application

    • 一个application对应一个sparkcontext,app中存在多个job
    • 每触发一次action操作就会产生一个job
    • 这些job可以并行或串行执行
    • 每个job中有多个stage,stage是shuffle过程中DAGSchaduler通过RDD之间的依赖关系划分job而来的
    • 每个stage里面有多个task,组成taskset有TaskSchaduler分发到各个executor中执行
    • executor的生命周期是和app一样的,即使没有job运行也是存在的,所以task可以快速启动读取内存进行计算。
  • hadoop的job只有map和reduce操作,表达能力比较欠缺

    • 在mr过程中会重复的读写hdfs,造成大量的io操作,多个job需要自己管理关系。
  • spark的迭代计算都是在内存中进行的

    • API中提供了大量的RDD操作如join,groupby等
    • 通过DAG图可以实现良好的容错

18.简单说一下hadoop和spark的shuffle相同和差异?

  • high-level角度:

    • 两者并没有大的差别 都是将 mapper(Spark: ShuffleMapTask)的输出进行 partition,不同的 partition 送到不同的 reducer(Spark 里 reducer 可能是下一个 stage 里的 ShuffleMapTask,也可能是 ResultTask)
      Reducer 以内存作缓冲区,边 shuffle 边 aggregate 数据,等到数据 aggregate 好以后进行 reduce()。
  • low-level 角度:

    • Hadoop MapReduce 是 sort-based,进入 combine() 和 reduce() 的 records 必须先 sort。
    • 好处:combine/reduce() 可以处理大规模的数据
      • 因为其输入数据可以通过外排得到
      • mapper 对每段数据先做排序
      • reducer 的 shuffle 对排好序的每段数据做归并
    • Spark 默认选择的是 hash-based,通常使用 HashMap 来对 shuffle 来的数据进行 aggregate,不提前排序
    • 如果用户需要经过排序的数据:sortByKey()
  • 实现角度:

    • Hadoop MapReduce 将处理流程划分出明显的几个阶段:map(), spilt, merge, shuffle, sort, reduce()
    • Spark 没有这样功能明确的阶段,只有不同的 stage 和一系列的 transformation(),spill, merge, aggregate 等操作需要蕴含在 transformation() 中

19. 简单说一下hadoop和spark的shuffle过程

  • hadoop:map端保存分片数据,通过网络收集到reduce端
  • spark:spark的shuffle是在DAGSchedular划分Stage的时候产生的,TaskSchedule要分发Stage到各个worker的executor,减少shuffle可以提高性能

20.partition和block的关联

  • hdfs中的block是分布式存储的最小单元,等分,可设置冗余,这样设计有一部分磁盘空间的浪费,但是整齐的block大小,便于快速找到、读取对应的内容
  • Spark中的partition是RDD的最小单元,RDD是由分布在各个节点上的partition组成的。
  • partition是指的spark在计算过程中,生成的数据在计算空间内最小单元
    同一份数据(RDD)的partion大小不一,数量不定,是根据application里的算子和最初读入的数据分块数量决定
  • block位于存储空间;partion位于计算空间,block的大小是固定的、partion大小是不固定的,是从2个不同的角度去看数据。

21.Spark为什么比mapreduce快?(重点面试题)

  • 基于内存计算,减少低效的磁盘交互
  • 高效的调度算法,基于DAG
  • 容错机制Linage

22.Mapreduce操作的mapper和reducer阶段相当于spark中的哪几个算子?

  • 相当于spark中的map算子和reduceByKey算子,区别:MR会自动进行排序的,spark要看具体partitioner

23.RDD机制

  • 分布式弹性数据集,简单的理解成一种数据结构,是spark框架上的通用货币
  • 所有算子都是基于rdd来执行的
  • rdd执行过程中会形成dag图,然后形成lineage保证容错性等
  • 从物理的角度来看rdd存储的是block和node之间的映射

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

  • 自动的进行内存和磁盘的存储切换;
  • 基于Lingage的高效容错;
  • task如果失败会自动进行特定次数的重试;
  • stage如果失败会自动进行特定次数的重试,而且只会计算失败的分片;
  • checkpoint和persist,数据计算之后持久化缓存
  • 数据调度弹性,DAG TASK调度和资源无关
  • 数据分片的高度弹性,a.分片很多碎片可以合并成大的,b.par

25.RDD有那些缺陷?

  • 不支持细粒度的写和更新操作(如网络爬虫)
    • spark写数据是粗粒度的,所谓粗粒度,就是批量写入数据 (批量写)
    • 但是读数据是细粒度的也就是说可以一条条的读 (一条条读)
  • 不支持增量迭代计算,Flink支持

26.什么是RDD宽依赖和窄依赖?

  • RDD和它依赖的parent RDD(s)的关系有两种不同的类型
    • 窄依赖:每一个parent RDD的Partition最多被子RDD的一个Partition使用 (一父一子)
    • 宽依赖:多个子RDD的Partition会依赖同一个parent RDD的Partition (一父多子)

27.cache和persist的区别

  • cache和persist都是用于缓存RDD,避免重复计算
  • .cache() == .persist(MEMORY_ONLY)

28. cache后面能不能接其他算子,它是不是action操作?

  • 可以接其他算子,但是接了算子之后,起不到缓存应有的效果,因为会重新触发cache
  • cache不是action操作

29.什么场景下要进行persist操作?

以下场景会使用persist

  • 某个步骤计算非常耗时或计算链条非常长,需要进行persist持久化
  • shuffle之后为什么要persist,shuffle要进性网络传输,风险很大,数据丢失重来,恢复代价很大
  • shuffle之前进行persist,框架默认将数据持久化到磁盘,这个是框架自动做的

30.rdd有几种操作类型?3种

  • transformation,rdd由一种转为另一种rdd
  • action
  • cronroller,控制算子(cache/persist) 对性能和效率的有很好的支持

31.reduceByKey是不是action?

  • 不是,很多人都会以为是action,reduce rdd是action

32.collect功能是什么,其底层是怎么实现的?

  • driver通过collect把集群中各个节点的内容收集过来汇总成结果
  • collect返回结果是Array类型的,合并后Array中只有一个元素,是tuple类型(KV类型的)的

33.map与flatMap的区别

  • map:对RDD每个元素转换,文件中的每一行数据返回一个数组对象
  • flatMap:对RDD每个元素转换,然后再扁平化,将所有的对象合并为一个对象,会抛弃值为null的值

34. 列举你常用的action?

  • collect,reduce,take,count,saveAsTextFile等

35.union操作是产生宽依赖还是窄依赖?

  • 窄依赖

36.Spark累加器有哪些特点?

  • 全局的,只增不减,记录全局集群的唯一状态
  • 在exe中修改它,在driver读取
  • executor级别共享的,广播变量是task级别的共享
  • 两个application不可以共享累加器,但是同一个app不同的job可以共享

37.spark hashParitioner的弊端

  • 分区原理:对于给定的key,计算其hashCode
  • 弊端是数据不均匀,容易导致数据倾斜

38.RangePartitioner分区的原理

  • 尽量保证每个分区中数据量的均匀,而且分区与分区之间是有序的,也就是说一个分区中的元素肯定都是比另一个分区内的元素小或者大
  • 分区内的元素是不能保证顺序的
  • 简单的说就是将一定范围内的数映射到某一个分区内

39.Spark中的HashShufle的有哪些不足?

  • shuffle产生海量的小文件在磁盘上,此时会产生大量耗时的、低效的IO操作
  • 容易导致内存不够用,由于内存需要保存海量的文件操作句柄和临时缓存信息
  • 容易出现数据倾斜,导致OOM

40.Spark的服务端口

  • 8080 spark集群web ui端口
  • 4040 sparkjob监控端口
  • 18080 jobhistory端口

41.一些零碎问题

  • park Job 默认的调度模式 - FIFO
  • RDD 特点 - 可分区/可序列化/可持久化
  • Broadcast - 任何函数调用/是只读的/存储在各个节点
  • Accumulator - 支持加法/支持数值类型/可并行
  • Task 数量由 Partition 决定
  • Task 运行在 Workder node 中 Executor 上的工作单元
  • master 和 worker 通过 Akka 方式进行通信的
  • 默认的存储级别 - MEMORY_ONLY
  • hive 的元数据存储在 derby 和 MySQL 中有什么区别 - 多会话
  • DataFrame 和 RDD 最大的区别 - 多了 schema

42.数据的本地性

在Spark中,数据本地性是指具体的task运行在哪台机器上,DAG划分stage的时候确定的。
Spark中的数据本地性有三种:

  • PROCESS_LOCAL是指读取缓存在本地节点的数据;
  • NODE_LOCAL是指读取本地节点的硬盘数据;
  • ANY是指读取非本地节点数据;

一般而言,读取数据PROCESS_LOCAL>NODE_LOCAL>ANY,尽量让数据以PROCESS_LOCAL或者NODE_LOCAL方式读取。其中PROCESS_LOCAL还和cache有关,如果RDD经常用的话将该RDD cache缓存到内存中。注意cache/persist是惰性的,所以必须经过action触发,才会真正执行缓存到内存中。

43.Spark master使用zookeeper进行HA的,有哪些元素保存在Zookeeper ?

Spark通过这个参数spark.deploy.zookeeper.dir指定master元数据在zookeeper中保存的位置,包括Worker,Driver和Application以及Executors。standby节点要从zk中,获得元数据信息,恢复集群运行状态,才能对外继续提供服务,作业提交资源申请等,在恢复前是不能接受请求的。另外,Master切换需要注意2点
1)在Master切换的过程中,所有的已经在运行的程序皆正常运行!因为Spark Application在运行前就已经通过Cluster Manager获得了计算资源,所以在运行时Job本身的调度和处理和Master是没有任何关系的!
2) 在Master的切换过程中唯一的影响是不能提交新的Job:一方面不能够提交新的应用程序给集群,因为只有Active Master才能接受新的程序的提交请求;另外一方面,已经运行的程序中也不能够因为Action操作触发新的Job的提交请求;

注意:Spark Master HA主从切换的过程不会影响集群中已有作业的运行,因为在程序运行之前,已经申请过资源了,driver和Executor通讯,不需要和master进行通讯的。

44.Spark on Yarn模式有几种类型以及Yarn集群的优点

Yarn集群有两种模式:

  • Yarn-Client模式:driver运行在本地客户端,负责调度Application,会与Yarn集群长生大量的网络通信,从而导致网卡流量激增。它的好处是执行时可在本地看到所有log,便于调试。因而它一般用于测试环境。
  • Yarn-Cluster模式:driver语行在NodeManager,每次运行都是随机分配到NodeManager机器上去,不会有网卡流量激增的问题。但他的缺点是:本地提交时看不到log,只能通过Yarn Application-log application id命令来查看。

关于这两者的区别,更多详情可参考:https://www.cnblogs.com/1130136248wlxk/articles/6289717.html

Yarn集群的优点:
1)与其他计算框架共享集群资源(eg.Spark框架与MapReduce框架同时运行,如果不用Yarn进行资源分配,MapReduce分到的内存资源会很少,效率低下);资源按需分配,进而提高集群资源利用等。
2)相较于Spark自带的Standalone模式,Yarn的资源分配更加细致
3)Application部署简化,例如Spark,Storm等多种框架的应用由客户端提交后,由Yarn负责资源的管理和调度,利用Container作为资源隔离的单位,以它为单位去使用内存,cpu等。
4)Yarn通过队列的方式,管理同时运行在Yarn集群中的多个服务,可根据不同类型的应用程序负载情况,调整对应的资源使用量,实现资源弹性管理。

45.Spark中的分区

Spark默认有两种分区:hashPartitioner和rangePartitioner
(1) hashPartitioner分区:
对于指定的key,计算其hashCode,并除以分区的个数取余,如果欲数小于0,则用余数+分区的个数,最后返回的值就是这个key所属的分区ID;
它的不足:原始数据不均匀时容易产生数据倾斜,极端情况下 某几个分区的会拥有rdd的所有数据。
(2) rangePartition分区:
范围分区的原理是尽量保证每个分区中数据量的均匀,而且分区与分区之间是有序的。但是分区内的元素是不能保证顺序的。简单而言,将一定范围内的数据映射到某一个分区内。
(3)除上述两个,在Spark中,我们还可以实现自定义分区。自定义分区器的时候继承org.apache.spark.Partitioner类,实现类中的三个方法:
def numPartitions: Int:这个方法需要返回你想要创建分区的个数;
def getPartition(key: Any): Int:这个函数需要对输入的key做计算,然后返回该key的分区ID,范围一定是0到numPartitions-1;
equals():这个是Java标准的判断相等的函数,之所以要求用户实现这个函数是因为Spark内部会比较两个RDD的分区是否一样。

46.谈谈对于Spark中的partition和HDFS中的block二者的理解

(1)hdfs中的block是分布式存储的最小单元,等分,可设置冗余,这样设计有一部分磁盘空间的浪费,但是整齐的block大小,便于快速找到、读取对应的内容;
(2)Spark中的partition是弹性分布式数据集RDD的最小单元,RDD是由分布在各个节点上的partition组成的。partition是指的spark在计算过程中,生成的数据在计算空间内最小单元,同一份数据(RDD)的partition大小不一,数量不定,是根据application里的算子和最初读入的数据分块数量决定;
(3)block位于存储空间,partition位于计算空间,block的大小是固定的、partion大小是不固定的,是从2个不同的角度去看数据。

47.简要描述Spark写数据的流程

(1)RDD调用compute方法,进行指定分区的写入
(2)CacheManager中调用BlockManager判断数据是否已经写入,如果未写,则写入
(3)BlockManager中数据与其他节点同步
(4)BlockManager根据存储级别写入指定的存储层
(5)BlockManager向主节点汇报存储状态中

48.RDD的数据结构

一个RDD对象,包含下面5个核心属性:
(1)一个分区列表,每个分区里是RDD的部分数据(或称数据块);
(2)一个依赖列表,存储依赖的其他RDD;
(3)一个名为compute的计算函数,用于计算RDD各分区的值;
(4)分区器(可选),用于键/值类型的RDD,比如某个RDD是按照散列来分区;
(5)计算各分区时优先的位置列表(可选),比如从HDFS上的文件生成RDD时,RDD分区的位置优先选择数据所在的节点,这样可以避免数据移动带来的开销。

49.Spark如何做性能优化(重要面试问题)

Spark的性能优化比较复杂,一般而言,它主要涉及到以下方面:开发调优,资源调优,数据倾斜调优,shuffle调优。

1.开发调优

避免创建重复的RDD
尽可能复用同一个RDD
对多次使用的RDD进行持久化
尽量避免使用shuffle类算子
使用map-side预聚合的shuffle操作
使用高性能的算子
广播大变量
使用Kryo优化序列化性能
优化数据结构

2.资源调优

所谓的Spark资源参数调优,其实主要就是对Spark运行过程中各个使用资源的地方,通过调节各种参数,来优化资源使用的效率,从而提升Spark作业的执行性能。以下参数就是Spark中主要的资源参数,每个参数都对应着作业运行原理中的某个部分,我们同时也给出了一个调优的参考值。

num-executors

参数说明:该参数用于设置Spark作业总共要用多少个Executor进程来执行。Driver在向YARN集群管理器申请资源时,YARN集群管理器会尽可能按照你的设置来在集群的各个工作节点上,启动相应数量的Executor进程。这个参数非常之重要,如果不设置的话,默认只会给你启动少量的Executor进程,此时你的Spark作业的运行速度是非常慢的。

参数调优建议:每个Spark作业的运行一般设置50~100个左右的Executor进程比较合适,设置太少或太多的Executor进程都不好。设置的太少,无法充分利用集群资源;设置的太多的话,大部分队列可能无法给予充分的资源。

executor-memory

参数说明:该参数用于设置每个Executor进程的内存。Executor内存的大小,很多时候直接决定了Spark作业的性能,而且跟常见的JVM OOM异常,也有直接的关联。

参数调优建议:每个Executor进程的内存设置4G8G较为合适。但是这只是一个参考值,具体的设置还是得根据不同部门的资源队列来定。可以看看自己团队的资源队列的最大内存限制是多少,num-executors乘以executor-memory,就代表了你的Spark作业申请到的总内存量(也就是所有Executor进程的内存总和),这个量是不能超过队列的最大内存量的。此外,如果你是跟团队里其他人共享这个资源队列,那么申请的总内存量最好不要超过资源队列最大总内存的1/31/2,避免你自己的Spark作业占用了队列所有的资源,导致别的同学的作业无法运行。

executor-cores

参数说明:该参数用于设置每个Executor进程的CPU core数量。这个参数决定了每个Executor进程并行执行task线程的能力。因为每个CPU core同一时间只能执行一个task线程,因此每个Executor进程的CPU core数量越多,越能够快速地执行完分配给自己的所有task线程。

参数调优建议:Executor的CPU core数量设置为2~4个较为合适。同样得根据不同部门的资源队列来定,可以看看自己的资源队列的最大CPU core限制是多少,再依据设置的Executor数量,来决定每个Executor进程可以分配到几个CPU core。同样建议,如果是跟他人共享这个队列,那么num-executors * executor-cores不要超过队列总CPU core的1/3~1/2左右比较合适,也是避免影响其他同学的作业运行。

driver-memory

参数说明:该参数用于设置Driver进程的内存。

参数调优建议:Driver的内存通常来说不设置,或者设置1G左右应该就够了。唯一需要注意的一点是,如果需要使用collect算子将RDD的数据全部拉取到Driver上进行处理,那么必须确保Driver的内存足够大,否则会出现OOM内存溢出的问题。

spark.default.parallelism

参数说明:该参数用于设置每个stage的默认task数量。这个参数极为重要,如果不设置可能会直接影响你的Spark作业性能。

参数调优建议:Spark作业的默认task数量为500~1000个较为合适。很多同学常犯的一个错误就是不去设置这个参数,那么此时就会导致Spark自己根据底层HDFS的block数量来设置task的数量,默认是一个HDFS block对应一个task。通常来说,Spark默认设置的数量是偏少的(比如就几十个task),如果task数量偏少的话,就会导致你前面设置好的Executor的参数都前功尽弃。试想一下,无论你的Executor进程有多少个,内存和CPU有多大,但是task只有1个或者10个,那么90%的Executor进程可能根本就没有task执行,也就是白白浪费了资源!因此Spark官网建议的设置原则是,设置该参数为num-executors * executor-cores的2~3倍较为合适,比如Executor的总CPU core数量为300个,那么设置1000个task是可以的,此时可以充分地利用Spark集群的资源。

spark.storage.memoryFraction

参数说明:该参数用于设置RDD持久化数据在Executor内存中能占的比例,默认是0.6。也就是说,默认Executor 60%的内存,可以用来保存持久化的RDD数据。根据你选择的不同的持久化策略,如果内存不够时,可能数据就不会持久化,或者数据会写入磁盘。

参数调优建议:如果Spark作业中,有较多的RDD持久化操作,该参数的值可以适当提高一些,保证持久化的数据能够容纳在内存中。避免内存不够缓存所有的数据,导致数据只能写入磁盘中,降低了性能。但是如果Spark作业中的shuffle类操作比较多,而持久化操作比较少,那么这个参数的值适当降低一些比较合适。此外,如果发现作业由于频繁的gc导致运行缓慢(通过spark web ui可以观察到作业的gc耗时),意味着task执行用户代码的内存不够用,那么同样建议调低这个参数的值。

spark.shuffle.memoryFraction

参数说明:该参数用于设置shuffle过程中一个task拉取到上个stage的task的输出后,进行聚合操作时能够使用的Executor内存的比例,默认是0.2。也就是说,Executor默认只有20%的内存用来进行该操作。shuffle操作在进行聚合时,如果发现使用的内存超出了这个20%的限制,那么多余的数据就会溢写到磁盘文件中去,此时就会极大地降低性能。

参数调优建议:如果Spark作业中的RDD持久化操作较少,shuffle操作较多时,建议降低持久化操作的内存占比,提高shuffle操作的内存占比比例,避免shuffle过程中数据过多时内存不够用,必须溢写到磁盘上,降低了性能。此外,如果发现作业由于频繁的gc导致运行缓慢,意味着task执行用户代码的内存不够用,那么同样建议调低这个参数的值。
资源参数的调优,没有一个固定的值,需要同学们根据自己的实际情况(包括Spark作业中的shuffle操作数量、RDD持久化操作数量以及spark web ui中显示的作业gc情况),同时参考本篇文章中给出的原理以及调优建议,合理地设置上述参数。

资源参数参考示例
以下是一份spark-submit命令的示例,大家可以参考一下,并根据自己的实际情况进行调节:

./bin/spark-submit \
  --master yarn-cluster \
  --num-executors 100 \
  --executor-memory 6G \
  --executor-cores 4 \
  --driver-memory 1G \
  --conf spark.default.parallelism=1000 \
  --conf spark.storage.memoryFraction=0.5 \
  --conf spark.shuffle.memoryFraction=0.3 \

3.数据倾斜调优

数据倾斜是大数据里面常见的问题,关于数据调优 网上有许多资料。南国在之前的博客中 也提到过一些。这里只做个解决方案的简单概括:

1.使用Hive ETL预处理数据
2.过滤少数导致倾斜的key
3.提高shuffle操作的并行度
4.两阶段聚合(局部聚合+全局聚合)
5.将reduce join转为map join
6.采样倾斜key并分拆join操作
7.使用随机前缀和扩容RDD进行join

这其中第3 4点在日常开发中应用的比较广泛。在实践中发现,很多情况下,如果只是处理较为简单的数据倾斜场景,那么使用上述方案中的某一种基本就可以解决。但是如果要处理一个较为复杂的数据倾斜场景,那么可能需要将多种方案组合起来使用。比如说,我们针对出现了多个数据倾斜环节的Spark作业,可以先运用解决方案一和二,预处理一部分数据,并过滤一部分数据来缓解;其次可以对某些shuffle操作提升并行度,优化其性能;最后还可以针对不同的聚合或join操作,选择一种方案来优化其性能。大家需要对这些方案的思路和原理都透彻理解之后,在实践中根据各种不同的情况,灵活运用多种方案,来解决自己的数据倾斜问题。

4.shuffle调优

大多数Spark作业的性能主要就是消耗在了shuffle环节,因为该环节包含了大量的磁盘IO、序列化、网络数据传输等操作。因此,如果要让作业的性能更上一层楼,就有必要对shuffle过程进行调优。但是也必须提醒大家的是,影响一个Spark作业性能的因素,主要还是代码开发、资源参数以及数据倾斜,shuffle调优只能在整个Spark的性能调优中占到一小部分而已。

在Spark的源码中,负责shuffle过程的执行、计算和处理的组件主要就是ShuffleManager,也即shuffle管理器。而随着Spark的版本的发展,ShuffleManager也在不断迭代,变得越来越先进。

在Spark 1.2以前,默认的shuffle计算引擎是HashShuffleManager。该ShuffleManager而HashShuffleManager有着一个非常严重的弊端,就是会产生大量的中间磁盘文件,进而由大量的磁盘IO操作影响了性能。【这里优化后的HashShuffleManager可开启consolidate机制。开启consolidate机制之后,在shuffle write过程中,task就不是为下游stage的每个task创建一个磁盘文件了。此时会出现shuffleFileGroup的概念】

因此在Spark 1.2以后的版本中,默认的ShuffleManager改成了SortShuffleManager。SortShuffleManager相较于HashShuffleManager来说,有了一定的改进。主要就在于,每个Task在进行shuffle操作时,虽然也会产生较多的临时磁盘文件,但是最后会将所有的临时文件合并(merge)成一个磁盘文件,因此每个Task就只有一个磁盘文件。在下一个stage的shuffle read task拉取自己的数据时,只要根据索引读取每个磁盘文件中的部分数据即可。

关于shuffle优化,主要是涉及到一些参数的设置:
spark.shuffle.file.buffer
  1、默认值:32k
  参数说明:该参数用于设置shuffle write task的BufferedOutputStream的buffer缓冲大小。将数据写到磁盘文件之前,会先写入buffer缓冲中,待缓冲写满之后,才会溢写到磁盘。
  调优建议:如果作业可用的内存资源较为充足的话,可以适当增加这个参数的大小(比如64k),从而减少shuffle write过程中溢写磁盘文件的次数,也就可以减少磁盘IO次数,进而提升性能。在实践中发现,合理调节该参数,性能会有1%~5%的提升。

spark.reducer.maxSizeInFlight
  默认值:48m
  参数说明:该参数用于设置shuffle read task的buffer缓冲大小,而这个buffer缓冲决定了每次能够拉取多少数据。
  调优建议:如果作业可用的内存资源较为充足的话,可以适当增加这个参数的大小(比如96m),从而减少拉取数据的次数,也就可以减少网络传输的次数,进而提升性能。在实践中发现,合理调节该参数,性能会有1%~5%的提升。

spark.shuffle.io.maxRetries
  默认值:3
  参数说明:shuffle read task从shuffle write task所在节点拉取属于自己的数据时,如果因为网络异常导致拉取失败,是会自动进行重试的。该参数就代表了可以重试的最大次数。如果在指定次数之内拉取还是没有成功,就可能会导致作业执行失败。
  调优建议:对于那些包含了特别耗时的shuffle操作的作业,建议增加重试最大次数(比如60次),以避免由于JVM的full gc或者网络不稳定等因素导致的数据拉取失败。在实践中发现,对于针对超大数据量(数十亿~上百亿)的shuffle过程,调节该参数可以大幅度提升稳定性。

spark.shuffle.io.retryWait
  默认值:5s
  参数说明:具体解释同上,该参数代表了每次重试拉取数据的等待间隔,默认是5s。
  调优建议:建议加大间隔时长(比如60s),以增加shuffle操作的稳定性。

spark.shuffle.memoryFraction
  默认值:0.2
  参数说明:该参数代表了Executor内存中,分配给shuffle read task进行聚合操作的内存比例,默认是20%。
  调优建议:在资源参数调优中讲解过这个参数。如果内存充足,而且很少使用持久化操作,建议调高这个比例,给shuffle read的聚合操作更多内存,以避免由于内存不足导致聚合过程中频繁读写磁盘。在实践中发现,合理调节该参数可以将性能提升10%左右。

spark.shuffle.manager
  默认值:sort
  参数说明:该参数用于设置ShuffleManager的类型。Spark 1.5以后,有三个可选项:hash、sort和tungsten-sort。HashShuffleManager是Spark 1.2以前的默认选项,但是Spark 1.2以及之后的版本默认都是SortShuffleManager了。tungsten-sort与sort类似,但是使用了tungsten计划中的堆外内存管理机制,内存使用效率更高。
  调优建议:由于SortShuffleManager默认会对数据进行排序,因此如果你的业务逻辑中需要该排序机制的话,则使用默认的SortShuffleManager就可以;而如果你的业务逻辑不需要对数据进行排序,那么建议参考后面的几个参数调优,通过bypass机制或优化的HashShuffleManager来避免排序操作,同时提供较好的磁盘读写性能。这里要注意的是,tungsten-sort要慎用,因为之前发现了一些相应的bug。

spark.shuffle.sort.bypassMergeThreshold
  默认值:200
  参数说明:当ShuffleManager为SortShuffleManager时,如果shuffle read task的数量小于这个阈值(默认是200),则shuffle write过程中不会进行排序操作,而是直接按照未经优化的HashShuffleManager的方式去写数据,但是最后会将每个task产生的所有临时磁盘文件都合并成一个文件,并会创建单独的索引文件。
  调优建议:当你使用SortShuffleManager时,如果的确不需要排序操作,那么建议将这个参数调大一些,大于shuffle read task的数量。那么此时就会自动启用bypass机制,map-side就不会进行排序了,减少了排序的性能开销。但是这种方式下,依然会产生大量的磁盘文件,因此shuffle write性能有待提高。

spark.shuffle.consolidateFiles
  默认值:false
  参数说明:如果使用HashShuffleManager,该参数有效。如果设置为true,那么就会开启consolidate机制,会大幅度合并shuffle write的输出文件,对于shuffle read task数量特别多的情况下,这种方法可以极大地减少磁盘IO开销,提升性能。
  调优建议:如果的确不需要SortShuffleManager的排序机制,那么除了使用bypass机制,还可以尝试将spark.shffle.manager参数手动指定为hash,使用HashShuffleManager,同时开启consolidate机制。在实践中尝试过,发现其性能比开启了bypass机制的SortShuffleManager要高出10%~30%。

50.Spark Streaming处理Kafka数据源

Kafka是一种高吞吐的分布式发布订阅消息系统,Spark Sreaming读取kafka数据支持两种方式:Receive方式和No Receive(Direct)方式。
(1)Receiver方式:Receiver适用kafka的高层次API来实现的。Receiver从kafka中获取的数据都是存储在Spark Excecutor的内存中的,然后Spark Streaming启动的job会去处理这些数据。
官网上提到说可靠的接收器方式,在接收数据并通过复制存储在Spark中时正确的向可靠的源发送确认。

注意:
1、Kafka中的topic的partition,与Spark中的RDD的partition是没有关系的。所以,在KafkaUtils.createStream()中,提高partition的数量,只会增加一个Receiver中,读取partition的线程的数量。不会增加Spark处理数据的并行度。
2、可以创建多个Kafka输入DStream,使用不同的consumer group和topic,来通过多个receiver并行接收数据。
3、如果基于容错的文件系统,比如HDFS,启用了预写日志机制,接收到的数据都会被复制一份到预写日志中。因此,在KafkaUtils.createStream()中,设置的持久化级别是StorageLevel.MEMORY_AND_DISK_SER。

(2)No Receivers方式:目前No Receivers方式在企业中使用的越来越多,No Receivers方式具有更强的自由度控制、语义一致性。No Receivers方式更符合数据读取和数据操作,在生产环境中建议采用NoReceivers direct的方式。
这种方式会周期性地查询Kafka,来获得每个topic+partition的最新的offset,从而定义每个batch的offset的范围。当处理数据的job启动时,就会使用Kafka的简单consumer api来获取Kafka指定offset范围的数据。
官网上说这是不可靠的接收器方式,不会像源发送确认。这可以用于不支持确认的源,甚至可以用于不需要或需要进入确认复杂性的可靠源。

这种方式有如下优点:
1、简化并行读取:如果要读取多个partition,不需要创建多个输入DStream然后对它们进行union操作。Spark会创建跟Kafka partition一样多的RDD partition,并且会并行从Kafka中读取数据。所以在Kafka partition和RDD partition之间,有一个一对一的映射关系。
2、高性能:如果要保证零数据丢失,在基于receiver的方式中,需要开启WAL机制。这种方式其实效率低下,因为数据实际上被复制了两份,Kafka自己本身就有高可靠的机制,会对数据复制一份,而这里又会复制一份到WAL中。而基于direct的方式,不依赖Receiver,不需要开启WAL机制,只要Kafka中作了数据的复制,那么就可以通过Kafka的副本进行恢复。
3、一次且仅一次的事务机制:
基于receiver的方式,是使用Kafka的高阶API来在ZooKeeper中保存消费过的offset的。这是消费Kafka数据的传统方式。这种方式配合着WAL机制可以保证数据零丢失的高可靠性,但是却无法保证数据被处理一次且仅一次,可能会处理两次。因为Spark和ZooKeeper之间可能是不同步的。
基于direct的方式,使用kafka的简单api,Spark Streaming自己就负责追踪消费的offset,并保存在checkpoint中。Spark自己一定是同步的,因此可以保证数据是消费一次且仅消费一次。

51.shuffle原理

概述:Shuffle描述着数据从map task输出到reduce task输入的这段过程。在分布式情况下,reduce task需要跨节点去拉取其它节点上的map task结果。这一过程将会产生网络资源消耗和内存,磁盘IO的消耗。

1.mapreduce的shuffle原理

1.1 map task端操作
每个map task都有一个内存缓冲区(默认是100MB),存储着map的输出结果,当缓冲区快满的时候需要将缓冲区的数据以一个临时文件的方式存放到磁盘,当整个map task结束后再对磁盘中这个map task产生的所有临时文件做合并,生成最终的正式输出文件,然后等待reduce task来拉数据。

Spill过程:这个从内存往磁盘写数据的过程被称为Spill,中文可译为溢写。整个缓冲区有个溢写的比例spill.percent(默认是0.8),当达到阀值时map task 可以继续往剩余的memory写,同时溢写线程锁定已用memory,先对key(序列化的字节)做排序,如果client程序设置了Combiner,那么在溢写的过程中就会进行局部聚合。

Merge过程:每次溢写都会生成一个临时文件,在map task真正完成时会将这些文件归并成一个文件,这个过程叫做Merge。

1.1.2 reduce task端操作
当某台TaskTracker上的所有map task执行完成,对应节点的reduce task开始启动,简单地说,此阶段就是不断地拉取(Fetcher)每个map task所在节点的最终结果,然后不断地做merge形成reduce task的输入文件。

Copy过程:Reduce进程启动一些数据copy线程(Fetcher)通过HTTP协议拉取TaskTracker的map阶段输出文件

Merge过程:Copy过来的数据会先放入内存缓冲区(基于JVM的heap size设置),如果内存缓冲区不足也会发生map task的spill(sort 默认,combine 可选),多个溢写文件时会发生map task的merge

下面总结下mapreduce的关键词:

存储相关的有:内存缓冲区,默认大小,溢写阀值

主要过程:溢写(spill),排序,合并(combine),归并(Merge),Copy或Fetch

相关参数:内存缓冲区默认大小,JVM heap size,spill.percent

关于排序方法:

在Map阶段,k-v溢写时,采用的正是快排;而溢出文件的合并使用的则是归并;
在Reduce阶段,通过shuffle从Map获取的文件进行合并的时候采用的也是归并;最后阶段则使用了堆排作最后的合并过程。

2 spark现在的SortShuffleManager

SortShuffleManager运行原理:
SortShuffleManager的运行机制主要分成两种,一种是普通运行机制,另一种是bypass运行机制。
当shuffle read task的数量小于等于spark.shuffle.sort.bypassMergeThreshold参数的值时(默认为200),就会启用bypass机制。

普通运行机制
在该模式下,数据会先写入一个内存数据结构中,此时根据不同的shuffle算子,可能选用不同的数据结构。如果是reduceByKey这种聚合类的shuffle算子,那么会选用Map数据结构,一边通过Map进行聚合,一边写入内存;如果是join这种普通的shuffle算子,那么会选用Array数据结构,直接写入内存。接着,每写一条数据进入内存数据结构之后,就会判断一下,是否达到了某个临界阈值。如果达到临界阈值的话,那么就会尝试将内存数据结构中的数据溢写到磁盘,然后清空内存数据结构。

在溢写到磁盘文件之前,会先根据key对内存数据结构中已有的数据进行排序。排序过后,会分批将数据写入磁盘文件。默认的batch数量是10000条,也就是说,排序好的数据,会以每批1万条数据的形式分批写入磁盘文件。写入磁盘文件是通过Java的BufferedOutputStream实现的。BufferedOutputStream是Java的缓冲输出流,首先会将数据缓冲在内存中,当内存缓冲满溢之后再一次写入磁盘文件中,这样可以减少磁盘IO次数,提升性能。

一个task将所有数据写入内存数据结构的过程中,会发生多次磁盘溢写操作,也就会产生多个临时文件。最后会将之前所有的临时磁盘文件都进行合并,这就是merge过程,此时会将之前所有临时磁盘文件中的数据读取出来,然后依次写入最终的磁盘文件之中。此外,由于一个task就只对应一个磁盘文件,也就意味着该task为下游stage的task准备的数据都在这一个文件中,因此还会单独写一份索引文件,其中标识了下游各个task的数据在文件中的start offset与end offset。

SortShuffleManager由于有一个磁盘文件merge的过程,因此大大减少了文件数量。比如第一个stage有50个task,总共有10个Executor,每个Executor执行5个task,而第二个stage有100个task。由于每个task最终只有一个磁盘文件,因此此时每个Executor上只有5个磁盘文件,所有Executor只有50个磁盘文件。

bypass运行机制
bypass运行机制的触发条件如下:
shuffle map task数量小于spark.shuffle.sort.bypassMergeThreshold参数的值(默认为200)。
不是排序类的shuffle算子(比如reduceByKey)。

此时task会为每个下游task都创建一个临时磁盘文件,并将数据按key进行hash然后根据key的hash值,将key写入对应的磁盘文件之中。当然,写入磁盘文件时也是先写入内存缓冲,缓冲写满之后再溢写到磁盘文件的。最后,同样会将所有临时磁盘文件都合并成一个磁盘文件,并创建一个单独的索引文件。

该过程的磁盘写机制其实跟未经优化的HashShuffleManager是一模一样的,因为都要创建数量惊人的磁盘文件,只是在最后会做一个磁盘文件的合并而已。因此少量的最终磁盘文件,也让该机制相对未经优化的HashShuffleManager来说,shuffle read的性能会更好。

bypass运行机制与普通SortShuffleManager运行机制的不同在于:第一,磁盘写机制不同;第二,不会进行排序。也就是说,启用该机制的最大好处在于,shuffle write过程中,不需要进行数据的排序操作,也就节省掉了这部分的性能开销。

3.Shuffle操作问题解决

1 数据倾斜原理

在进行shuffle的时候,必须将各个节点上相同的key拉取到某个节点上的一个task来进行处理,此时如果某个key对应的数据量特别大的话,就会发生数据倾斜

2 数据倾斜问题发现与定位

通过Spark Web UI来查看当前运行的stage各个task分配的数据量,从而进一步确定是不是task分配的数据不均匀导致了数据倾斜。

知道数据倾斜发生在哪一个stage之后,接着我们就需要根据stage划分原理,推算出来发生倾斜的那个stage对应代码中的哪一部分,这部分代码中肯定会有一个shuffle类算子。通过countByKey查看各个key的分布。

3 数据倾斜解决方案
前面有讲过,可以查看

52.SparkStreaming消费kafka的两种方式

1.基于接收者Receiver的方法

算子:KafkaUtils.createStream
方法:PUSH,从topic中去推送数据,将数据推送过来
API:调用的Kafka高级API
效果:SparkStreaming中的Receivers,恰好Kafka有发布/订阅 ,然而:此种方式企业不常用,说明有BUG,不符合企业需求。因为:接收到的数据存储在Executor的内存,会出现数据漏处理或者多处理状况
解释:这种方法使用Receiver来接收数据。Receiver是使用Kafka高级消费者API实现的。与所有的接收者一样,通过Receiver从Kafka接收的数据存储在Spark执行程序exector中,然后由Spark Streaming启动的作业处理数据。但是,在默认配置下,这种方法可能会在失败时丢失数据。为了确保零数据丢失,您必须在Spark Streaming(在Spark 1.2中引入)中额外启用写入日志,同时保存所有接收到的Kafka数据写入分布式文件系统(例如HDFS)的预先写入日志,以便所有数据都可以在失败时恢复。

缺点:
①、Kafka中的主题分区与Spark Streaming中生成的RDD的分区不相关。因此,增加主题特定分区KafkaUtils.createStream()的数量只会增加在单个接收器中使用哪些主题消耗的线程的数量。在处理数据时不会增加Spark的并行性
②、多个kafka输入到DStream会创建多个group和topic,用于使用多个接收器并行接收数据
③、如果已经使用HDFS等复制文件系统启用了写入日志,则接收到的数据已经在日志中复制。因此,输入流的存储级别为存储级别StorageLevel.MEMORY_AND_DISK_SER

2. 直接方法(无接收者)

算子:KafkaUtils.createDirectStream
方式:PULL,到topic中去拉取数据。
API:kafka低级API
效果:每次到Topic的每个分区依据偏移量进行获取数据,拉取数据以后进行处理,可以实现高可用
解释:在Spark 1.3中引入了这种新的无接收器“直接”方法,以确保更强大的端到端保证。这种方法不是使用接收器来接收数据,而是定期查询Kafka在每个topic+分partition中的最新偏移量,并相应地定义要在每个批次中处理的偏移量范围。当处理数据的作业启动时,Kafka简单的客户API用于读取Kafka中定义的偏移范围(类似于从文件系统读取文件)。请注意,此功能在Spark 1.3中为Scala和Java API引入,在Spark 1.4中针对Python API引入。

优势:
①、简化的并行性:不需要创建多个输入Kafka流并将其合并。与此同时directStream,Spark Streaming将创建与使用Kafka分区一样多的RDD分区,这些分区将全部从Kafka并行读取数据。所以在Kafka和RDD分区之间有一对一的映射关系,这更容易理解和调整。

②、效率:在第一种方法中实现零数据丢失需要将数据存储在预写日志中,这会进一步复制数据。这实际上是效率低下的,因为数据被有效地复制了两次,一次是由Kafka,另一次是由预先写入日志(Write Ahead Log)复制。此方法消除了这个问题,因为没有接收器,因此不需要预先写入日志。只要你有足够的kafka保留,消息可以从kafka恢复。

③、精确语义:第一种方法是使用Kafka的高级API在Zookeeper中存储消耗的偏移量。传统上这是从Kafka消费数据的方式。虽然这种方法(合并日志)可以确保零数据丢失,但在某些失败情况下,很小的几率两次数据都同时丢失,发生这种情况是因为Spark Streaming可靠接收到的数据与Zookeeper跟踪的偏移之间的不一致。因此,在第二种方法中,我们使用不使用Zookeeper的简单Kafka API。在其检查点内,Spark Streaming跟踪偏移量。这消除了Spark Streaming和Zookeeper / Kafka之间的不一致性,因此Spark Streaming每次记录都会在发生故障时有效地接收一次。

请注意,这种方法的一个缺点是它不会更新Zookeeper中的偏移量,因此基于Zookeeper的Kafka监控工具将不会显示进度。但是,您可以在每个批次中访问由此方法处理的偏移量,并自己更新Zookeeper

53.使用mr,spark,sparksql编写wordcount程序

  • Spark版本

val conf=newSparkConf().setAppName("wd").setMaster("local[1]")

  val sc=newSparkContext(conf,2)

  //加载

  val lines=sc.textFile("tructField("name",DataTypes.StringType,true)")

  val paris=lines.flatMap(line=>line.split("^A"))

  val words=paris.map((_,1))

  val result=words.reduceByKey(_+_).sortBy(x=>x._1,false)

  //打印

  result.foreach(

  wds=>{

  println("单词:"+wds._1+"个数:"+wds._2)

  }

  )

  sc.stop()
  • sparksql版本


val conf=newSparkConf().setAppName("sqlWd").setMaster("local[1]")

  val sc=newSparkContext(conf)

  val sqlContext=newSQLContext(sc)

  //加载

  val lines=sqlContext.textFile("E:idea15createRecommederdatawords.txt")

  val words=lines.flatMap(x=>x.split("")).map(y=>Row(y))

  val structType=StructType(Array(StructField("name",DataTypes.StringType,true)))

  val df=sqlContext.createDataFrame(rows,structType)

  df.registerTempTable("t_word_count")

  sqlContext.udf.register("num_word",(name:String)=>1)

  sqlContext.sql("selectname,num_word(name)fromt_word_count").groupBy(df.col("name")).count().show()

  sc.stop()

以上内容是总结网上各位大佬的,如有错误,麻烦指出,谢谢。

你可能感兴趣的:(Spark,大数据,面经)