Bootstrap

【大数据专题】Spark题库

1 . 简述什么是Spark ?

试题回答参考思路:

Spark是大数据的调度,监控和分配引擎。它是一个快速通用的集群计算平台.Spark扩展了流行的MapReduce模型.Spark提供的主要功能之一就是能够在内存中运行计算 ,但对于在磁盘上运行的复杂应用程序,系统也比MapReduce更有效

Spark 是一个通用的内存计算引擎。您可以将它与任何存储系统连接,如本地存储系统、HDFS、Amazon S3 等。它还让您可以自由使用您选择的资源管理器,无论是 Yarn、Mesos、Kubernetes 还是独立的。它旨在管理批处理应用程序工作负载、迭代算法、交互式查询和流式传输。Spark 支持高级 API,例如Java、Scala、Python和R。它是用 Scala 语言构建的。

2 . 简述Spark部署模式 ?

试题回答参考思路:

1、在独立模式下,Spark使用Master守护进程来协调运行执行程序的Worker的工作。独立模式是默认模式,Worker运行executor, 但不能在安全集群上使用。当提交应用程序时,可以选择其执行程序将使用多少内存,以及所有执行程序中的内核总数。

2、yarn模式
在YARN模式下,YARN ResourceManager执行Spark Master的功能。 Workers的功能由运行执行程序的YARN NodeManager守护程序执行。 YARN模式设置稍微复杂一些,但它支持安全性

3 . 简述Spark主要功能与特性 ?

试题回答参考思路:

多语言:Spark提供Java,Scala,Python和R中的高级API .Spark代码可以用这四种语言中的任何一种编写。 它为Scala和Python提供了shell。 可以通过./bin/spark-shell进入Scala Shell和通过./bin/pyspark 进入Python shell

速度:Spark的运行速度比Hadoop MapReduce快100倍,适用于大规模数据处理。 Spark能够通过控制分区实现此速度。 它使用分区管理数据,这些分区有助于以最小的网络流量并行化处理分布式数据处

多种格式:Spark支持多种数据源,如Parquet,JSON,Hive和Cassandra。 Data Sources API提供了一种可插入的机制,用于通过Spark SQL访问结构化数据。 数据源不仅仅是简单的管道,可以转换数据并将其拉入Spark。

延迟执行:Apache Spark延迟执行,直到绝对必要。 这是促进其速度的关键因素之一。 对于transformations,Spark将它们添加到计算的DAG中,并且仅当驱动程序请求某些数据时,才会实际执行此DAG。

实时计算:Spark的计算是实时的,并且由于其内存计算而具有较少的延迟。 Spark专为大规模可扩展性而设计,Spark团队已经记录了运行具有数千个节点的生产集群的系统用户,并支持多种计算模型。
Hadoop集成:Apache Spark提供与Hadoop的兼容性。 Spark是Hadoop的MapReduce的潜在替代品,而Spark能够使用YARN在现有的Hadoop集群上运行资源调度。

机器学习:Spark的MLlib是机器学习组件,在大数据处理方面很方便。 它消除了使用多种工具的需求,一种用于处理,一种用于机器学习。 Spark为数据工程师和数据科学家提供了一个功能强大,统一的引擎,既快速又易于使用。

4 . 简述Spark对MapReduce优势 ?

试题回答参考思路:

Spark与MapReduce相比具有以下优势:

(1)由于内存处理的可用性,Spark实现的处理速度比Hadoop MapReduce快10到100倍,而MapReduce则使用持久性存储来执行任何数据处理任务。
(2)与Hadoop不同,Spark提供了内置库,可以从同一个核心执行多个任务,如批处理,Steaming,机器学习,Interactive SQL查询。 但是,(3)Hadoop仅支持批处理。
(4)Hadoop与磁盘高度相关,而Spark则提升缓存和内存数据存储。
Spark能够在同一数据集上多次执行计算。 这称为迭代计算,而Hadoop没有直接实现迭代计算

5 . 简述Spark的任务执行流程 ?

试题回答参考思路:

1)构建Application的运行环境,Driver创建一个SparkContext
2) SparkContext向资源管理器(Standalone、Mesos、Yarn)申请Executor资源,资源管理器启动StandaloneExecutorbackend(Executor)
3) Executor向SparkContext申请Task
4) SparkContext将应用程序分发给Executor
5) SparkContext就建成DAG图,DAGScheduler将DAG图解析成Stage,每个Stage有多个task,形成taskset
发送给task Scheduler,由task Scheduler将Task发送给Executor运行
6) Task在Executor上运行,运行完释放所有资源

6 . 简述Spark的运行流程 ?

试题回答参考思路:

Spark流程是按照如下的核心步骤进行工作的:
1)任务提交后,都会先启动Driver程序;
2)随后Driver向集群管理器注册应用程序;
3)之后集群管理器根据此任务的配置文件分配Executor并启动;
4)Driver开始执行main函数,Spark查询为懒执行,当执行到Action算子时开始反向推算,根据宽依赖进 行Stage的划分,随后每一个Stage对应一个Taskset,Taskset中有多个Task,查找可用资源Executor进行调度;
5)根据本地化原则,Task会被分发到指定的Executor去执行,在任务执行的过程中,Executor也会不断 与Driver进行通信,报告任务运行情况。

7 . 简述Spark的作业运行流程是怎么样的 ?

试题回答参考思路:

目 前 用 得 比 较 多 的 是 Standalone 模 式 和 YARN 模 式:

1、Standalone
Standalone模式是Spark实现的资源调度框架,其主要的节点有Client节点、Master节点和Worker节点。Driver既可以运行在Master节点上,也可以运行在本地Client端。当用spark-shell交互式工具提交spark的
Job时,Driver在Master节点上运行。当使用spark-submit工具提交Job或者在Eclipse、IDEA等开发平台上使用“new SparkConf().setMaster(spark://master:7077)"方式运行Spark任务时,Driver是运行在本地Client 端上的。
当用spark-shell交互式工具提交Spark的Job时,使用spark-shell启动脚本。该脚本启动一个交互式的
Scala命令界面,可供用户来运行Spark相关命令。在Spark的安装目录下启动spark-shell,启动命令如下 所示:

在spark-shell的启动过程中可以看到如下的信息,从中可以看到Spark的版本为2.1.0,spark内嵌的Scala 版本为2.11.8,Java的版本为1.8.同时spark-shell在启动的过程中会初始化SparkContext为sc,以及初始化SQLContext为sqlContext。界面出现”scala>"这样的提示符,说明Spark交互式命令窗口启动成功,如图所 示。用户可在该窗口下编写Spark相关代码。

spark-shell启动的时候也可以手动指定每个节点的内存和Executor使用的cpu个数,启动命令如下所示: 当以Standalone模式向spark集群提交作业时

1)首先,SparkContext连接到Master,向Master注册并申请资源。
2) Worker定期发送心跳信息给Master并报告Executor状态。
3) Master根据SparkContext的资源申请要求和Worker心跳周期内报告的信息决定在哪个Worker上分配资 源,然后在该Worker上获取资源,启动StandaloneExecutorBackend。
4) StandaloneExecutorBackend向SparkContext注册。
5) SparkContext将Application代码发送给StandaloneExecutorBackend,并且SparkContext解析
Application代码,构建DAG图,并提交DAG Scheduler,分解成Stage(当碰到Action操作时),就会催生
Job,每个Job中含有一个或多个Stage),然后分配到相应的Worker,最后提交给
StandaloneExecutorBackend执行。
6) StandaloneExecutorBackend会建立Executor线程池,开始执行Task,并向SparkContext报告,直至
Task完成。
7)所有Task完成后,SparkContext向Master注销,释放资源。

2、YARN模式
YARN模式根据Driver在集群中的位置分为两种,一种是YARN-Client模式(客户端模式),另一种是
YARN-Cluster模式(集群模式)。
在YARN模式中,不需要启动Spark独立集群,所以这个时候去访问http://master:8080 是访问不了的。启动YARN客户端模式的Spark Shell命令如下所示:
在YARN集群模式下,Driver运行在Application Master上,Application Master进程同时负责驱动
Application和从YARN中申请资源。该进程运行在YARN Container内,所以启动Application Master的Client
可以立即关闭,而不必持续到Application的声明周期。

作业流程如下所示:
1)客户端生成作业信息提交给ResourceManager。
2) ResourceManager在某一个NodeManager(由YARN决定)启动Container,并将Application Master 分配给该NodeManager。
3) NodeManager接收到ResourceManager的分配,启动Application Master并初始化作业,此时
NodeManager就称为Driver。
4) Application向ResourceManager申请资源,ResourceManager分配资源的同时通知其他NodeManager启 动相应的Executor。
5) Executor向NodeManager上的Application Master注册汇报并完成相应的任务。
如下图是YARN客户端模式的作业运行流程。Application Master仅仅从YARN中申请资源给Executpr。之后
Client会与Container通信进行作业调度。

YARN模式的作业运行调度描述如下
1)客户端生成作业信息提交给ResourceManager
2) ResourceManager在本地NodeManager启动Container,并将Application Master分配给该
NodeManager。
3) NodeManager接收到ResourceManager的分配,启动Application Master并初始化作业,此时这个NodeManager就称为Driver。
4) Application向ResourceManager申请资源,ResouceManager分配资源同时通知其他NodeManager启动 相应的Executor。
5) Executor向本地启动的Application Master注册汇报并完成相应的任务。

YARN-Cluster和YARN-Client区别:
在YARN-Cluster模式下,SparkDriver运行在Application Master(AM)中,它负责向YARN申请资源,并监督作业的运行状况。当用户提交了作业之后,就可以关掉Client,作业会继续在YARN上运行,所以YARN-
Cluster模式不适合运行交互类型的作业。然而在YARN-Client模式下,AM仅仅向YARN请求Executor,
Client会与请求得到的Container通信来调度它们的工作,也就是是Client不能离开。
总结来说,集群模式的Spark Driver运行在AM中,而客户端模式的Spark Driver运行在客户端。所以, YARN-Cluster适用于生产,而YARN-Client适用于交互和调试,也就是希望快速地看到应用的输出信息。

8 . 简述Spark源码中的任务调度 ?

试题回答参考思路:

1.Spark Stage 级调度
Spark 的任务调度是从 DAG 切割开始,主要是由 DAGScheduler 来完成。当遇到一个 Action 操作后就会触发一个 Job 的计算,并交给 DAGScheduler 来提交。Job 由最终的 RDD 和 Action 方法封装而成。SparkContex t将 Job 交给 DAGScheduler 提交,它会根据 RDD 的血缘关系构成的 DAG 进行切分,将一个 Job 划分为若干 Stages,具体划分策略是,由最终的 RDD 不断通过依赖回溯判断父依赖是否是宽依赖,即以 Shuffle 为界,划分 Stage,窄依赖的 RDD 之间被划分到同一个 Stage 中,可以进行 pipeline 式的计算。划分的 Stages 分两类,一类叫做 ResultStage,为 DAG 最下游的 Stage,由 Action 方法决定,另一类叫做 ShuffleMapStage,为下游 Stage 准备数据,下面看一个简单的例子 WordCount。

步骤如下:

Job 由 saveAsTextFile 触发,该 Job 由 RDD-3 和 saveAsTextFile 方法组成;
从 RDD-3 开始回溯搜索,RDD-3 依赖 RDD-2,并且是宽依赖,所以在 RDD-2 和 RDD-3 之间划分 Stage;
RDD-2 依赖 RDD-1,RDD-1 依赖 RDD-0,这些依赖都是窄依赖,所以将 RDD-0、RDD-1 和 RDD-2 划分到同一个 Stage。
一个 Stage 是否被提交,需要判断它的父 Stage 是否执行,只有在父 Stage 执行完毕才能提交当前 Stage,如果一个 Stage 没有父 Stage,那么从该 Stage 开始提交。Stage 提交时会将 Task 信息(分区信息以及方法等)序列化并被打包成 TaskSet 交给 TaskScheduler,一个 Partition 对应一个 Task,另一方面 TaskScheduler 会监控 Stage 的运行状态,只有 Executor 丢失或者 Task 由于 Fetch 失败才需要重新提交失败的 Stage 以调度运行失败的任务,其他类型的 Task 失败会在 TaskScheduler 的调度过程中重试。

总结一下就是 DAGScheduler 仅仅是在 Stage 层面上划分 DAG,提交 Stage 并监控相关状态信息。

2.Spark Task 级调度
DAGScheduler 将 Stage 打包到交给 TaskScheduler,TaskScheduler 会将 TaskSet 封装为 TaskSetManager 加入到调度队列中。TaskSetManager 负责监控管理同一个 Stage 中的 Tasks,TaskScheduler 就是以 TaskSetManager 为单元来调度任务。

TaskScheduler 初始化后会启动 SchedulerBackend,它负责跟外界打交道,接收 Executor 的注册信息,并维护 Executor 的状态,它在启动后会定期地去“询问” TaskScheduler 有没有任务要运行,TaskScheduler 在 SchedulerBackend“询问”它的时候,会从调度队列中按照指定的调度策略选择 TaskSetManager 去调度运行。TaskScheduler 支持两种调度策略,一种是 FIFO,另一种是 FAIR。

FIFO调度策略直接简单地将 TaskSetManager 按照先来先到的方式入队,出队时直接拿出最先进队的 TaskSetManager;
FAIR 模式中有一个 rootPool 和多个子 Pool,各个子 Pool 中存储着所有待分配的 TaskSetMagager

9 . 简述Spark作业调度 ?

试题回答参考思路:

静态资源分配原理
spark提供了许多功能用来在集群中同时调度多个作业。首先,回想一下,每个spark作业都会运行自己独立的一批executor进程,此时集群管理器会为我们提供同时调度多个作业的功能。第二,在每个spark作业内部,多个job也可以并行执行,比如说spark-shell就是一个spark application,但是随着我们输入scala rdd action类代码,就会触发多个job,多个job是可以并行执行的。为这种情况,spark也提供了不同的调度器来在一个application内部调度多个job。

先来看一下多个作业的同时调度

静态资源分配

当一个spark application运行在集群中时,会获取一批独立的executor进程专门为自己服务,比如运行task和存储数据。如果多个用户同时在使用一个集群,并且同时提交多个作业,那么根据cluster manager的不同,有几种不同的方式来管理作业间的资源分配。
最简单的一种方式,是所有cluster manager都提供的,也就是静态资源分配。在这种方式下,每个作业都会被给予一个它能使用的最大资源量的限额,并且可以在运行期间持有这些资源。这是spark standalone集群和YARN集群使用的默认方式。
Standalone集群: 默认情况下,提交到standalone集群上的多个作业,会通过FIFO的方式来运行,每个作业都会尝试获取所有的资源。可以限制每个作业能够使用的cpu core的最大数量(spark.cores.max),或者设置每个作业的默认cpu core使用量(spark.deploy.defaultCores)。最后,除了控制cpu core之外,每个作业的spark.executor.memory也用来控制它的最大内存的使用。
YARN: --num-executors属性用来配置作业可以在集群中分配到多少个executor,–executor-memory和–executor-cores可以控制每个executor能够使用的资源。
要注意的是,没有一种cluster manager可以提供多个作业间的内存共享功能。如果你想要通过这种方式来在多个作业间共享数据,我们建议就运行一个spark作业,但是可以接收网络请求,并对相同RDD的进行计算操作。在未来的版本中,内存存储系统,比如Tachyon会提供其他的方式来共享RDD数据。
动态资源分配原理
动态资源分配原理

spark 1.2开始,引入了一种根据作业负载动态分配集群资源给你的多个作业的功能。这意味着你的作业在申请到了资源之后,可以在使用完之后将资源还给cluster manager,而且可以在之后有需要的时候再次申请这些资源。这个功能对于多个作业在集群中共享资源是非常有用的。如果部分资源被分配给了一个作业,然后出现了空闲,那么可以还给cluster manager的资源池中,并且被其他作业使用。在spark中,动态资源分配在executor粒度上被实现,可以通过spark.dynamicAllocation.enabled来启用。
资源分配策略

一个较高的角度来说,当executor不再被使用的时候,spark就应该释放这些executor,并且在需要的时候再次获取这些executor。因为没有一个绝对的方法去预测一个未来可能会运行一个task的executor应该被移除掉,或者一个新的executor应该别加入,我们需要一系列的探索式算法来决定什么应该移除和申请executor。
申请策略

一个启用了动态资源分配的spark作业会在它有pending住的task等待被调度时,申请额外的executor。这个条件必要地暗示了,已经存在的executor是不足以同时运行所有的task的,这些task已经提交了,但是没有完成。
driver会轮询式地申请executor。当在一定时间内(spark.dynamicAllocation.schedulerBacklogTimeout)有pending的task时,就会触发真正的executor申请,然后每隔一定时间后(spark.dynamicAllocation.sustainedSchedulerBacklogTimeout),如果又有pending的task了,则再次触发申请操作。此外,每一轮申请到的executor数量都会比上一轮要增加。举例来说,一个作业需要增加一个executor在第一轮申请时,那么在后续的一轮中会申请2个、4个、8个executor。
每轮增加executor数量的原因主要有两方面。第一,一个作业应该在开始谨慎地申请以防它只需要一点点executor就足够了。第二,作业应该会随着时间的推移逐渐增加它的资源使用量,以防突然大量executor被增加进来。
移除策略

移除一个executor的策略比较简单。一个spark作业会在它的executor出现了空闲超过一定时间后(spark.dynamicAllocation.executorIdleTimeout),被移除掉。要注意,在大多数环境下,这个条件都是跟申请条件互斥的,因为如果有task被pending住的话,executor是不该是空闲的。
executor如何优雅地被释放掉

在使用动态分配之前,executor无论是发生了故障失败,还是关联的application退出了,都还是存在的。在所有场景中,executor关联的所有状态都不再被需要,并且可以被安全地抛弃。使用动态分配之后,executor移除之后,作业还是存在的。如果作业尝试获取executor写的中间状态数据,就需要去重新计算哪些数据。因此,spark需要一种机制来优雅地卸载executor,在移除它之前要保护它的状态。
解决方案就是使用一个外部的shuffle服务来保存每个executor的中间写状态,这也是spark 1.2引入的特性。这个服务是一个长时间运行的进程,集群的每个节点上都会运行一个,位你的spark作业和executor服务。如果服务被启用了,那么spark executor会在shuffle write和read时,将数据写入该服务,并从该服务获取数据。这意味着所有executor写的shuffle数据都可以在executor声明周期之外继续使用。
除了写shuffle文件,executor也会在内存或磁盘中持久化数据。当一个executor被移除掉时,所有缓存的数据都会消失。目前还没有有效的方案。在未来的版本中,缓存的数据可能会通过堆外存储来进行保存,就像external shuffle service保存shuffle write文件一样。
standalone模式下使用动态资源分配
./sbin/start-shuffle-service.sh
spark-shell --master spark://192.168.75.101:7077
–jars /usr/local/hive/lib/mysql-connector-java-5.1.17.jar
–conf spark.shuffle.service.enabled=true
–conf spark.dynamicAllocation.enabled=true
–conf spark.shuffle.service.port=7337
启动external shuffle service
启动spark-shell,启用动态资源分配
过60s,发现打印日志,说executor被removed,executor进程也没了
然后动手写一个wordcount程序,最后提交job的时候,会动态申请一个新的executor,出来一个新的executor进程
然后整个作业执行完毕,证明external shuffle service+动态资源分配,流程可以走通
再等60s,executor又被释放掉
yarn模式下使用动态资源分配

先停止之前为standalone集群启动的shuffle service
./sbin/stop-shuffle-service.sh
配置

动态资源分配功能使用的所有配置,都是以spark.dynamicAllocation作为前缀的。要启用这个功能,你的作业必须将spark.dynamicAllocation.enabled设置为true。其他相关的配置之后会详细说明。

此外,你的作业必须有一个外部shuffle服务(external shuffle service)。这个服务的目的是去保存executor的shuffle write文件,从而让executor可以被安全地移除。要启用这个服务,可以将spark.shuffle.service.enabled设置为true。在YARN中,这个外部shuffle service是由org.apache.spark.yarn.network.YarnShuffleService实现的,在每个NodeManager中都会运行。要启用这个服务,需要使用以下步骤:

使用预编译好的spark版本。

定位到spark–yarn-shuffle.jar。这个应该在$SPARK_HOME/lib目录下。

将上面的jar加入到所有NodeManager的classpath中/usr/local/hadoop/share/hadoop/yarn/lib/。

在yarn-site.xml中,将yarn.nodemanager.aux-services设置为spark_shuffle,将yarn.nodemanager.aux-services.spark_shuffle.class设置为org.apache.spark.network.yarn.YarnShuffleService
yarn.nodemanager.aux-services
spark_shuffle
yarn.nodemanager.aux-services.spark_shuffle.class
org.apache.spark.network.yarn.YarnShuffleService
yarn.log-aggregation-enable
true

重启所有NodeManager

spark-shell --master yarn-client
–jars /usr/local/hive/lib/mysql-connector-java-5.1.17.jar
–conf spark.shuffle.service.enabled=true
–conf spark.dynamicAllocation.enabled=true
–conf spark.shuffle.service.port=7337

首先配置好yarn的shuffle service,然后重启集群
接着呢,启动spark shell,并启用动态资源分配,但是这里跟standalone不一样,上来不会立刻申请executor
接着执行wordcount,会尝试动态申请executor,并且申请到后,执行job,在spark web ui上,有两个executor
过了一会儿,60s过后,executor由于空闲,所以自动被释放掉了,在看spark web ui,没有executor了
多个job资源调度原理
在一个spark作业内部,多个并行的job是可以同时运行的。对于job,就是一个spark action操作触发的计算单元。spark的调度器是完全线程安全的,而且支持一个spark application来服务多个网络请求,以及并发执行多个job。
默认情况下,spark的调度会使用FIFO的方式来调度多个job。每个job都会被划分为多个stage,而且第一个job会对所有可用的资源获取优先使用权,并且让它的stage的task去运行,然后第二个job再获取资源的使用权,以此类推。如果队列头部的job不需要使用整个集群资源,之后的job可以立即运行,但是如果队列头部的job使用了集群几乎所有的资源,那么之后的job的运行会被推迟。
从spark 0.8开始,我们是可以在多个job之间配置公平的调度器的。在公平的资源共享策略下,spark会将多个job的task使用一种轮询的方式来分配资源和执行,所以所有的job都有一个基本公平的机会去使用集群的资源。这就意味着,即使运行时间很长的job先提交并在运行了,之后提交的运行时间较短的job,也同样可以立即获取到资源并且运行,而不会等待运行时间很长的job结束之后才能获取到资源。这种模式对于多个并发的job是最好的一种调度方式。
Fair Scheduler使用详解
要启用Fair Scheduler,只要简单地将spark.scheduler.mode属性设置为FAIR即可
val conf = new SparkConf().setMaster(…).setAppName(…)
conf.set(“spark.scheduler.mode”, “FAIR”)
val sc = new SparkContext(conf)

或者

–conf spark.scheduler.mode=FAIR
fair scheduler也支持将job分成多个组并放入多个池中,以及为每个池设置不同的调度优先级。这个feature对于将重要的和不重要的job隔离运行的情况非常有用,可以为重要的job分配一个池,并给予更高的优先级; 为不重要的job分配另一个池,并给予较低的优先级。
默认情况下,新提交的job会进入一个默认池,但是job的池是可以通过spark.scheduler.pool属性来设置的。
如果spark application是作为一个服务启动的,SparkContext 7*24小时长时间存在,然后服务每次接收到一个请求,就用一个子线程去服务它:
在子线程内部,去执行一系列的RDD算子以及代码来触发job的执行
在子线程内部,可以调用SparkContext.setLocalProperty(“spark.scheduler.pool”, “pool1”)
在设置这个属性之后,所有在这个线程中提交的job都会进入这个池中。同样也可以通过将该属性设置为null来清空池子。
池的默认行为

默认情况下,每个池子都会对集群资源有相同的优先使用权,但是在每个池内,job会使用FIFO的模式来执行。举例来说,如果要为每个用户创建一个池,这就意味着每个用户都会获得集群的公平使用权,但是每个用户自己的job会按照顺序来执行。
配置池的属性

可以通过配置文件来修改池的属性。每个池都支持以下三个属性:
schedulingMode: 可以是FIFO或FAIR,来控制池中的jobs是否要排队,或者是共享池中的资源
weight: 控制每个池子对集群资源使用的权重。默认情况下,所有池子的权重都是1.如果指定了一个池子的权重为2。举例来说,它就会获取其他池子两倍的资源使用权。设置一个很高的权重值,比如1000,也会很有影响,基本上该池子的task会在其他所有池子的task之前运行。
minShare: 除了权重之外,每个池子还能被给予一个最小的资源使用量。
池子的配置是通过xml文件来配置的,在spark/conf的fairscheduler.xml中配置
然后去设置这个文件的路径,conf.set(“spark.scheduler.allocation.file”, “/path/to/file”)
文件内容大致如下所示
FAIR
FIFO

10 . 简述spark部署模式(资源调度模式) ?

试题回答参考思路:

1、资源调度模式
1.1、local模式(本地)
运行该模式非常简单,只需要把Spark的安装包解压后,改一些常用的配置即可使用,而不用启动Spark的Master、Worker守护进程( 只有采用集群的Standalone方式时,才需要这两个角色),也不用启动Hadoop的各服务(除非要用到HDFS文件系统)。
Spark不一定非要跑在hadoop集群,可以在本地,起多个线程的方式来指定。将Spark应用以多线程的方式直接运行在本地,一般都是为了方便调试,本地单机模式分三类:
local: 只启动一个executor
local[k]: 启动k个executor
local[*]: 启动跟cpu数目相同的 executor

1.2、standalone模式
即独立模式, 自带完整的服务,可单独部署到一个集群中,资源管理和任务监控是Spark自己监控,无需依赖任何其他资源管理系统。这个模式也是其他模式的基础. 不使用其他调度工具时会存在单点故障,使用Zookeeper等可以解决

1.3、on-yarn模式
这是一种最有前景的部署模式。但限于YARN自身的发展,目前仅支持**粗粒度模式(**Coarse-grained Mode)。这是由于YARN上的Container资源是不可以动态伸缩的,一旦Container启动之后,可使用的资源不能再发生变化,不过这个已经在YARN计划中了。

应用场景:
考虑到尽量用一个统一的资源调度模式来运行多种任务,
这样可以减轻运维的工作压力,
同时也可以减少资源调度之间的配合(基于集群考虑)

spark on yarn 的支持两种模式:
1.、yarn-cluster:Driver运行在 YARN集群下的某台机器上的JVM进程中,适用于生产环境;
2.、yarn-client:Driver 运行在当前提交程序的机器上,适用于交互、调试
yarn-cluster和yarn-client的区别在于yarn appMaster,每个yarn app实例有一个appMaster进程,是为app启动的第一个container;负责从ResourceManager请求资源,获取到资源后,告诉NodeManager为其启动container。在实际生产环境中一般都采用yarn-cluster;而如果你仅仅是Debug程序,可以选择yarn-client。

1.4、messos模式
这是很多公司采用的模式,官方推荐这种模式(当然,原因之一是血缘关系)。正是由于Spark开发之初就考虑到支持Mesos,因此,目前而言,Spark运行在Mesos上会比运行在YARN上更加灵活,更加自然。目前在Spark On Mesos环境中,用户可选择两种调度模式之一运行自己的应用程序:

1)、 粗粒度模式(Coarse-grained Mode):每个应用程序的运行环境由一个Dirver和若干个Executor组成,其中,每个Executor占用若干资源,内部可运行多个Task(对应多少个“slot”)。应用程序的各个任务正式运行之前,需要将运行环境中的资源全部申请好,即使不用, 运行过程中也要一直占用这些资源,,最后程序运行结束后,回收这些资源。

2)、细粒度模式(Fine-grained Mode):鉴于粗粒度模式会造成大量资源浪费,Spark On Mesos还提供了另外一种调度模式:细粒度模式,这种模式类似于现在的云计算,思想是按需分配。
与粗粒度模式一样,应用程序启动时,先会启动executor,但每个executor占用资源仅仅是自己运行所需的资源,不需要考虑将来要运行的任务,之后,mesos会为每个executor动态分配资源,每分配一些,便可以运行一个新任务,单个Task运行完之后可以马上释放对应的资源。每个Task会汇报状态给Mesos slave和Mesos Master,便于更加细粒度管理和容错,这种调度模式类似于MapReduce调度模式,每个Task完全独立,优点是便于资源控制和隔离,但缺点也很明显,短作业运行延迟大。

1.5、docker
Docker是一种相比虚拟机更加轻量级的虚拟化解决方案,所以在Docker上搭建Spark集群具有可行性。

1.6、cloud
2、这么多资源调度模式,到底用哪种比较好?
需要通过公司需求和运行速度来综合衡量

3、哪种调度模式速度快呢?
standalone模式

4、为什么有很多企业在用spark-on-yarn模式?
考虑到尽量用一个统一的资源调度模式来运行多种任务,
这样可以减轻运维的工作压力,
同时也可以减少资源调度之间的配合(基于集群考虑)

5、Yarn的任务调度流程
1、client向ResourceManager注册并提交任务
2、ResourceManager向NodeManager进行通信,开始在某个NodeManager启动AppMaster
3、AppMaster启动后开始向ResourceManager申请资源
4、ApplicationManager开始资源调度,开始通知NodeManager启动YarnChild
5、YarnChild开始和AppMaster进行通信,AppMaster对所有YarnChild进行监控
6、MR执行完成以后,YarnChild被AppMaster回收,AppMaster把自己回收掉9

11 . 简述Spark的使用场景 ?

试题回答参考思路:

大数据场景主要有以下几种类型:
复杂的批处理(Batch Data Processing),偏重点在于处理。海量数据的能力,至于处理速度可忍受,通常的时间可能是在数十分钟到数小时;
基于历史数据的交互式查询(Interactive Query),通常的时间在数十秒到数十分钟之间 ; 基于实时数据流的大数据处理(Streaming Data Processing),通常在数百毫秒到数秒之间 ;
对于上述的情况,不使用Spark来处理的框架如下:
第一种情况可以用 Hadoop 的 MapReduce 来进行批量海量数据处理第二种情况可以 Impala、Kylin 进行交互式查询
第三中情况可以用 Storm 分布式处理框架处理实时流式数据
使用Spark来进行处理:
第一种情况使用 Spark Core 解决第二种情况使用 Spark SQL 解决
第三种情况使用 Spark Streaming 解决
综上所述,Spark使用场景如下:
Spark 是基于内存的迭代计算框架,适用于需要多次操作特定数据集的应用场合。需要反复操作的次数越多,所需读取的数据量越大,受益越大,数据量小但是计算密集度较大 的场合,受益就相对较小 ;
由于 RDD 的特性,Spark 不适用那种异步细粒度更新状态的应用,例如 web 服务的存储或者是增量的 web 爬虫和索引。就是对于那种增量修改的应用模型不适合 ;

数据量不是特别大,但是要求实时统计分析需求 ;
满足以上条件的均可采用Spark技术进行处理,在实际应用中,目前大数据在互联网公司主要应用在广 告、报表、推荐系统等业务上,在广告业务方面需要大数据做应用分析、效果分析、定向优化等,在推 荐系统方面则需要大数据优化相关排名、个性化推荐以及热点点击分析等。
这些应用场景的普遍特点是计算量大、效率要求高,Spark恰恰可以满足这些要求,该项目一经推出便 受到开源社区的广泛关注和好评,并在近两年内发展成为大数据处理领域炙手可热的开源项目。
Spark使用Scala语言进行实现,它是一种面向对象、函数式编程语言,能够像操作本地集合对象一样轻 松地操作分布式数据集,具有运行速度快、易用性好、通用性强以及随处运行等特点,适合大多数批处 理工作,并已成为大数据时代企业大数据处理优选技术,其中有代表性企业有腾讯、Yahoo、淘宝以及 优酷土豆等。

12 . 简述Spark on Standalone运行过程 ?

试题回答参考思路:

Standalone模式是Spark实现的资源调度框架,其主要的节点有Client节点、Master节点和Worker节点。 其中Driver既可以运行在Master节点上中,也可以运行在本地Client端。当用spark-shell交互式工具提交
Spark的Job时,Driver在Master节点上运行;当使用spark-submit工具提交Job或者在Eclipse、IDEA等开发
平台上使用 new SparkConf().setMaster(“spark://master:7077”) 方式运行Spark任务时,Driver 是运行在本地Client端上的
运行流程如下:
1)我们提交一个任务,任务就叫Application;
2)初始化程序的入口SparkContext;
初始化DAG Scheduler
初始化Task Scheduler
3) Task Scheduler向master去进行注册并申请资源(CPU Core和Memory);
4) Master根据SparkContext的资源申请要求和Worker心跳周期内报告的信息决定在哪个Worker上分配资 源,然后在该Worker上获取资源,然后启动StandaloneExecutorBackend;顺便初始化好了一个线程池;
5) StandaloneExecutorBackend向Driver(SparkContext)注册,这样Driver就知道哪些Executor为他进行服务了。到这个时候其实我们的初始化过程基本完成了,我们开始执行transformation的代码,但是代码并 不会真正的运行,直到我们遇到一个action操作。生产一个job任务,进行stage的划分;
6) SparkContext将Applicaiton代码发送给StandaloneExecutorBackend;并且SparkContext解析
Applicaiton代码,构建DAG图,并提交给DAG Scheduler分解成Stage(当碰到Action操作时,就会催生
Job;每个Job中含有1个或多个Stage,Stage一般在获取外部数据和shuule之前产生);
7)将Stage(或者称为TaskSet)提交给Task Scheduler。Task Scheduler负责将Task分配到相应的
Worker,最后提交给StandaloneExecutorBackend执行;
8)对task进行序列化,并根据task的分配算法,分配task;
9)executor对接收过来的task进行反序列化,把task封装成一个线程;
10)开始执行Task,并向SparkContext报告,直至Task完成; 11)资源注销

13 . 简述Spark on YARN运行过程 ?

试题回答参考思路:

Spark on YARN模式根据Driver在集群中的位置分为两种模式:一种是YARN-Client模式,另一种是YARN- Cluster(或称为YARN-Standalone模式)。
YARN-Client模式
Yarn-Client模式中,Driver在客户端本地运行,这种模式可以使得Spark Application和客户端进行交互, 因为Driver在客户端,所以可以通过webUI访问Driver的状态,默认是 http://hadoop1:4040 访问,而
YARN通过 http:// hadoop1:8088 访问
YARN-client的工作流程分为以下几个步骤:
1) Spark Yarn Client向YARN的ResourceManager申请启动Application Master。同时在SparkContent初始化中将创建DAGScheduler和TASKScheduler等,由于我们选择的是Yarn-Client模式,程序会选择
YarnClientClusterScheduler和YarnClientSchedulerBackend;
2) ResourceManager收到请求后,在集群中选择一个NodeManager,为该应用程序分配第一个
Container,要求它在这个Container中启动应用程序的ApplicationMaster,与YARN-Cluster区别的是在该
ApplicationMaster不运行SparkContext,只与SparkContext进行联系进行资源的分派;
3) Client中的SparkContext初始化完毕后,与ApplicationMaster建立通讯,向ResourceManager注册,根 据任务信息向ResourceManager申请资源(Container);
4)一旦ApplicationMaster申请到资源(也就是Container)后,便与对应的NodeManager通信,要求它在 获得的Container中启动启动CoarseGrainedExecutorBackend,CoarseGrainedExecutorBackend启动后会向
Client中的SparkContext注册并申请Task;
5)Client中的SparkContext分配Task给CoarseGrainedExecutorBackend执行,
CoarseGrainedExecutorBackend运行Task并向Driver汇报运行的状态和进度,以让Client随时掌握各个任务 的运行状态,从而可以在任务失败时重新启动任务;
6)应用程序运行完成后,Client的SparkContext向ResourceManager申请注销并关闭自己。
YARN-Cluster模式
在YARN-Cluster模式中,当用户向YARN中提交一个应用程序后,YARN将分两个阶段运行该应用程序:第 一个阶段是把Spark的Driver作为一个ApplicationMaster在YARN集群中先启动;第二个阶段是由
ApplicationMaster创建应用程序,然后为它向ResourceManager申请资源,并启动Executor来运行Task, 同时监控它的整个运行过程,直到运行完成
YARN-cluster的工作流程分为以下几个步骤:
1) Spark Yarn Client向YARN中提交应用程序,包括ApplicationMaster程序、启动ApplicationMaster的命令、需要在Executor中运行的程序等;
2) ResourceManager收到请求后,在集群中选择一个NodeManager,为该应用程序分配第一个
Container,要求它在这个Container中启动应用程序的ApplicationMaster,其中ApplicationMaster进行
SparkContext等的初始化;
3) ApplicationMaster向ResourceManager注册,这样用户可以直接通过ResourceManage查看应用程序的 运行状态,然后它将采用轮询的方式通过RPC协议为各个任务申请资源,并监控它们的运行状态直到运 行结束;
4)一旦ApplicationMaster申请到资源(也就是Container)后,便与对应的NodeManager通信,要求它在 获得的Container中启动启动CoarseGrainedExecutorBackend,CoarseGrainedExecutorBackend启动后会向
ApplicationMaster中的SparkContext注册并申请Task。这一点和Standalone模式一样,只不过SparkContext在Spark Application中初始化时,使用CoarseGrainedSchedulerBackend配合YarnClusterScheduler进行任务的调度,其中YarnClusterScheduler只是对TaskSchedulerImpl的一个简单包装,增加了对Executor的等待逻辑等;
5)ApplicationMaster中的SparkContext分配Task给CoarseGrainedExecutorBackend执行, CoarseGrainedExecutorBackend运行Task并向ApplicationMaster汇报运行的状态和进度,以让ApplicationMaster随时掌握各个任务的运行状态,从而可以在任务失败时重新启动任务;
6)应用程序运行完成后,ApplicationMaster向ResourceManager申请注销并关闭自己

14 . 简述YARN-Client 与 YARN-Cluster 区别 ?

试题回答参考思路:

理解YARN-Client和YARN-Cluster深层次的区别之前先清楚一个概念:Application Master。在YARN中,每个Application实例都有一个ApplicationMaster进程,它是Application启动的第一个容器。它负责和
ResourceManager打交道并请求资源,获取资源之后告诉NodeManager为其启动Container。从深层次的 含义讲YARN-Cluster和YARN-Client模式的区别其实就是ApplicationMaster进程的区别。
YARN-Cluster模式下,Driver运行在AM(Application Master)中,它负责向YARN申请资源,并监督作业的运行状况。当用户提交了作业之后,就可以关掉Client,作业会继续在YARN上运行,因而YARN-Cluster模式 不适合运行交互类型的作业;
YARN-Client模式下,Application Master仅仅向YARN请求Executor,Client会和请求的Container通信来调度他们工作,也就是说Client不能离开

15 . 简述Spark的yarn-cluster涉及的参数有哪些 ?

试题回答参考思路:

Spark On Yarn的Cluster模式指的是Driver程序运行在Yarn集群上
1 --class org.apache.spark.examples.SparkPi
2 --master yarn
3 --deploy-mode cluster
4 --driver-memory 1g
5 --executor-memory 1g
6 --executor-cores 2
7 --queue default

参数 解释
–class 程序的main方法所在的类
–master 指定 Master 的地址
–deploy-mode 指定运行模式(client/cluster)
–driver-memory Driver运行所需要的内存, 默认1g
–executor-memory 指定每个 executor 可用内存为 2g, 默认1g
–executor-cores 指定每一个 executor 可用的核数
–queue 指定任务的对列

16 . 简述Spark提交job的流程 ?

试题回答参考思路:

Job提交运行的总流程,大致分为两个阶段:
1、Stage划分与提交
Job按照RDD之间的依赖关系是否为宽依赖,由DAGScheduler划分为一个个Stage,并将每个Stage提 交给TaskScheduler;
Stage随后被提交,并由TaskScheduler将每个stage转化为一个TaskSet。
2、Task调度与执行:由TaskScheduler负责将TaskSet中的Task调度到Worker节点的Executor上执行
对于第一阶段Stage划分与提交,又主要分为三个阶段:
1、Job的调度模型与运行反馈
1)首先由DAGScheduler负责将Job提交到事件队列eventProcessLoop中,等待调度执行
该事件队列为DAGSchedulerEventProcessLoop类型,内部封装了一个BlockingQueue阻塞队列,并由一个 后台线程eventThread不断的调用onReceive()方法处理其中的事件
2)创建一个JobWaiter对象并返回给客户端
利用这个JobWaiter对象的awaitResult()方法对Job进行监控与运行反馈,并获得JobSucceeded和
JobFailed两种Job运行结果
3)DAGSchedulerEventProcessLoop的onReceive()方法处理事件

onReceive()方法继续调用doOnReceive(event)方法,然后根据传入的事件类型DAGSchedulerEvent决定调 用哪个方法处理事件,这里传入的是JobSubmitted事件,调用的是DAGScheduler的
handleJobSubmitted()方法,继而进入下一个阶段

2、Stage划分
在第一阶段将JobSubmitted事件提交到事件队列后,DAGScheduler的handleJobSubmitted()方法就开始了Stage的划分。
1)根据finalRDD获取其Parent Stages,即ShuuleMapStage列表
2)利用finalRDD生成最后一个Stage,即ResultStage
3)生成ActiveJob对象,并维护各种stage、job等数据结构

3、Stage提交:对应TaskSet的生成
1)提交finalStage
2)提交其parent Stage,如果对应parent Stage还存在尚未提交的parent Stage,提交之3)对于没有parent Stage的Stage,根据stage中rdd的分区,生成tasks,即TaskSet,创建
TaskSetManager,并由SchedulerBackend申请资源

17 . 简述Spark的阶段划分流程 ?

试题回答参考思路:

这里说下Driver的工作流程,Driver线程主要是初始化SparkContext对象,准备运行所需的上下文,然后 一方面保持与ApplicationMaster的RPC连接,通过ApplicationMaster申请资源,另一方面根据用户业务逻 辑开始调度任务,将任务下发到已有的空闲Executor上。
当ResourceManager向ApplicationMaster返回Container资源时,ApplicationMaster就尝试在对应的
Container上启动Executor进程,Executor进程起来后,会向Driver反向注册,注册成功后保持与Driver的 心跳,同时等待Driver分发任务,当分发的任务执行完毕后,将任务状态上报给Driver。
当Driver起来后,Driver则会根据用户程序逻辑准备任务,并根据Executor资源情况逐步分发任务。
一个RDD任务分为:Application、Job、Stage 和 Task。
Application:初始化一个 SparkContext 就生成一个 Application;
Job:以Action方法为界,遇到一个Action方法则触发一个Job;
Stage:Job的子集,以RDD宽依赖(即Shuule)为界,遇到Shuule做一次划分;
Task:Stage的子集,以并行度(分区数)来衡量,分区数是多少,则有多少个task

Spark RDD通过其Transactions操作,形成了RDD血缘(依赖)关系图,即DAG,最后通过Action的调用, 触发Job并调度执行,执行过程中会创建两个调度器:DAGScheduler和TaskScheduler。
DAGScheduler负责Stage级的调度,主要是将job切分成若干Stages,并将每个Stage打包成TaskSet交 给TaskScheduler调度。
TaskScheduler负责Task级的调度,将DAGScheduler给过来的TaskSet按照指定的调度策略分发到
Executor上执行,调度过程中SchedulerBackend负责提供可用资源,其中SchedulerBackend有多种实现,分别对接不同的资源管理系统。
Driver初始化SparkContext过程中,会分别初始化DAGScheduler、TaskScheduler、SchedulerBackend以及
HeartbeatReceiver,并启动SchedulerBackend以及HeartbeatReceiver。SchedulerBackend通过
ApplicationMaster申请资源,并不断从TaskScheduler中拿到合适的Task分发到Executor执行。
HeartbeatReceiver负责接收Executor的心跳信息,监控Executor的存活状况,并通知到TaskScheduler

18 . 简述Spark处理数据的具体流程 ?

试题回答参考思路:

Spark Streaming是Spark中处理流式数据的模块

Spark Streaming启动之后,启动一个job一直接收数据,将接收来的数据每隔一段时间封装到一个batch 中,这里的一段时间就是batchInterval。batch又被封装到RDD中,RDD封装到DStream中,DStream有自 己的算子对数据进行处理,Transformation类算子可以对DStream进行转换(懒执行),outputOperator 类对DStream进行触发执行。
假设batchInterval=5s,集群处理一批次数据时间是3s:
0-5s:集群接收数据,5-8s:一边接收数据一边处理数据,8-10s:只是接收数据,10-13s…集群处理一批 次有休息,不能充分利用集群的资源。
假设batchInterval=5s,集群处理一批次数据时间是8s:
0-5s:集群接收数据,5-10s:一边接收数据一边处理数据,10-13s:一边接收数据一边处理数据,13-
15s:一边接收数据一边处理数据…集群处理数据,有任务的堆积,如果接收来的数据存放内存不够,要 溢写磁盘,加大数据处理的延迟。
batchInterval生成一批次数据,集群处理一批次数据时间也是batchInterval,集群既不会资源不能充分利 用,也不会有任务堆积。

19 . 简述Spark join的分类 ?

试题回答参考思路:

hash join是传统数据库中的单机join算法,在分布式环境下需要经过一定的分布式改造,说到底就是尽可能利用分布式计算资源进行并行化计算,提高总体效率。hash join分布式改造一般有两种经典方案:
broadcast hash join:将其中一张小表广播分发到另一张大表所在的分区节点上,分别并发地与其上的分区记录进行hash join。broadcast适用于小表很小,可以直接广播的场景。
shuuler hash join:一旦小表数据量较大,此时就不再适合进行广播分发。这种情况下,可以根据join
key相同必然分区相同的原理,将两张表分别按照join key进行重新组织分区,这样就可以将join分而治之,划分为很多小join,充分利用集群资源并行化。
Spark join方式主要有以下三种:
1、Broadcast Hash Join
broadcast hash join可以分为两步:
1) broadcast阶段:将小表广播分发到大表所在的所有主机。广播算法可以有很多,最简单的是先发给
driver,driver再统一分发给所有executor;要不就是基于bittorrete的p2p思路;
2) hash join阶段:在每个executor上执行单机版hash join,小表映射,大表试探
适用于小表与大表的join,SparkSQL规定broadcast hash join执行的基本条件为被广播小表必须小于参数spark.sql.autoBroadcastJoinThreshold,默认为10M,本质上是去用空间换时间,
2、ShuGle Hash Join
在大数据条件下如果一张表很小,执行join操作最优的选择无疑是broadcast hash join,效率最高。但是一旦小表数据量增大,广播所需内存、带宽等资源必然就会太大,broadcast hash join就不再是最优方案。此时可以按照join key进行分区,根据key相同必然分区相同的原理,就可以将大表join分而治之,划分为很多小表的join,充分利用集群资源并行化。如下图所示,shuule hash join也可以分为两步:
1) shuule阶段:分别将两个表按照join key进行分区,将相同join key的记录重分布到同一节点,两张表的数据会被重分布到集群中所有节点。这个过程称为shuule
2) hash join阶段:每个分区节点上的数据单独执行单机hash join算法
适合于没有特别小的两个表进行关联的时候,默认设置的shuule partition的个数是200,也就是分了200 个区,然后两张表的key值分别去基于200做hash取余然后散步在每个区域中了,这样的思想先把相近的 合并在一个区内,再在每个分区内去做比较key值的等值比较,就避免了大范围的遍历比较,节省了时 间和内存。
3、Sort-Merge Join
SparkSQL对两张大表join采用了全新的算法:sort-merge join,如下图所示,整个过程分为三个步骤
1) shuule阶段:将两张大表根据join key进行重新分区,两张表数据会分布到整个集群,以便分布式并行处理;
2) sort阶段:对单个分区节点的两表数据,分别进行排序;
3) merge阶段:对排好序的两张分区表数据执行join操作。join操作很简单,分别遍历两个有序序列,碰 到相同join key就merge输出,否则取更小一边
这种适用于关联的两张表都特别大时,使用上述的两种方法加载到内存的时候对于内存的压力都非常大 时,因此在2方法的基础上,hash取余之后还要分别对两张表的key值进行排序,这样去做等值比较的时 候就不需要将某一方的全部数据都加载到内存进行计算了,只需要取一部分就能知道是否有相等的(比 如按升序排列,某个值明显比它大了,后面肯定就不会有相等的,就不用继续比较了,节省了时间和内 存),也就是在进行等值比较的时候即用即丢的。这个方法在前面进行排序的时候可能会消耗点时间, 但相对于后面的时间来说,总体是大大节省了时间

20 . 简述Spark map join的实现原理 ?

试题回答参考思路:

Map-Join适用于有一份数据较小的连接情况。做法是把该小份数据直接全部加载到内存当中,按Join关 键字建立索引。然后大份数据就作为MapTask的输入,对map()方法的每次输入都去内存当中直接进行匹 配连接。然后把连接结果按key输出。这种方法要使用Spark中的广播(Broadcast)功能把小份数据分放到 各个计算节点,每个maptask执行任务的节点都需要加载该数据到内存。由于Join是在Map阶段进行的,

故称为Map-Join 。缺点如下:
需要将小表建立索引,常用方式是建立Map表,在 Spark 中,可以通过 rdd.collectAsMap() 算子实现。但是 collectAsMap() 在 key 重复时,后面的 value 会覆盖前面的,所以对于存在重复 key 的表,需做其他处理。另外,在 collectAsMap() 过程中,由于需要在 Driver 节点进行 collect ,所以需要保证 Driver 节点内存充足,可在 spark-commit 提交执行任务时,通过设置 driver-memory 调节 Driver 节点大小。
Map-Join 同时需要将经过 Map 广播到不同的 executor ,供 Task 获取数据进行连接并输出结果。所以
executor 也需要保证内存充足,可在 spark-commit 提交执行任务时,通过设置 --executor-cores 调节
Driver 节点大小。

21 . 简述Spark ShuGle及其优缺点 ?

试题回答参考思路:

Spark Shuule 分为两种:一种是基于 Hash 的 Shuule;另一种是基于 Sort 的 Shuule。
对于下面的内容,先明确一个假设前提:每个Executor只有1个CPU core,也就是说,无论这个Executor
上分配多少个task线程,同一时间都只能执行一个task线程。
1、Hash ShuGle
1)未优化的HashShuule
如下图中有3个 Reducer,从Task 开始那边各自把自己进行 Hash 计算(分区器:hash/numreduce取模), 分类出3个不同的类别,每个 Task 都分成3种类别的数据,想把不同的数据汇聚然后计算出最终的结
果,所以Reducer 会在每个 Task 中把属于自己类别的数据收集过来,汇聚成一个同类别的大集合,每1 个 Task 输出3份本地文件,这里有4个 Mapper Tasks,所以总共输出了4个 Tasks x 3个分类文件 = 12个本地小文件。
2)优化后的HashShuule
优化的HashShuule过程就是启用合并机制,合并机制就是复用buuer,开启合并机制的配置是
spark.shuffle.consolidateFiles 。该参数默认值为false,将其设置为true即可开启优化机制。通常来说,如果我们使用HashShuuleManager,那么都建议开启这个选项。
这里还是有4个Tasks,数据类别还是分成3种类型,因为Hash算法会根据你的 Key 进行分类,在同一个进程中,无论是有多少过Task,都会把同样的Key放在同一个Buuer里,然后把Buuer中的数据写入以
Core数量为单位的本地文件中,(一个Core只有一种类型的Key的数据),每1个Task所在的进程中,分别 写入共同进程中的3份本地文件,这里有4个Mapper Tasks,所以总共输出是 2个Cores x 3个分类文件 = 6 个本地小文件。

基于 Hash 的 Shuule 机制的优缺点
优点:
可以省略不必要的排序开销。避免了排序所需的内存开销。
缺点:
生产的文件过多,会对文件系统造成压力。大量小文件的随机读写带来一定的磁盘开销。
数据块写入时所需的缓存空间也会随之增加,对内存造成压力。

2、Sort ShuGle
1)普通SortShuule
在该模式下,数据会先写入一个数据结构,reduceByKey写入Map,一边通过Map局部聚合,一遍写入内 存。Join算子写入ArrayList直接写入内存中。然后需要判断是否达到阈值,如果达到就会将内存数据结 构的数据写入到磁盘,清空内存数据结构。
在溢写磁盘前,先根据key进行排序,排序过后的数据,会分批写入到磁盘文件中。默认批次为10000 条,数据会以每批一万条写入到磁盘文件。写入磁盘文件通过缓冲区溢写的方式,每次溢写都会产生一 个磁盘文件,也就是说一个Task过程会产生多个临时文件。
最后在每个Task中,将所有的临时文件合并,这就是merge过程,此过程将所有临时文件读取出来,一 次写入到最终文件。意味着一个Task的所有数据都在这一个文件中。同时单独写一份索引文件,标识下 游各个Task的数据在文件中的索引,start ouset和end ouset。

2) bypass SortShuule
bypass运行机制的触发条件如下:
shuule reduce task数量小于spark.shuule.sort.bypassMergeThreshold参数的值,默认为200
不是聚合类的shuule算子(比如reduceByKey)
此时task会为每个reduce端的task都创建一个临时磁盘文件,并将数据按key进行hash然后根据key的hash 值,将key写入对应的磁盘文件之中。当然,写入磁盘文件时也是先写入内存缓冲,缓冲写满之后再溢 写到磁盘文件的。最后,同样会将所有临时磁盘文件都合并成一个磁盘文件,并创建一个单独的索引文 件。
该过程的磁盘写机制其实跟未经优化的HashShuuleManager是一模一样的,因为都要创建数量惊人的磁 盘文件,只是在最后会做一个磁盘文件的合并而已。因此少量的最终磁盘文件,也让该机制相对未经优 化的HashShuuleManager来说,shuule read的性能会更好。
而该机制与普通SortShuuleManager运行机制的不同在于:不会进行排序。也就是说,启用该机制的最 大好处在于,shuule write过程中,不需要进行数据的排序操作,也就节省掉了这部分的性能开销。

3) Tungsten Sort Shuule 运行机制
基于 Tungsten Sort 的 Shuule 实现机制主要是借助 Tungsten 项目所做的优化来高效处理 Shuule。
Spark 提供了配置属性,用于选择具体的 Shuule 实现机制,但需要说明的是,虽然默认情况下 Spark 默认开启的是基于 SortShuule 实现机制,但实际上,参考 Shuule 的框架内核部分可知基于 SortShuule 的实现机制与基于 Tungsten Sort Shuule 实现机制都是使用 SortShuuleManager,而内部使用的具体的实现机制,是通过提供的两个方法进行判断的:
对应非基于 Tungsten Sort 时,通过 SortShuuleWriter.shouldBypassMergeSort方法判断是否需要回退到
Hash 风格的 Shuule 实现机制,当该方法返回的条件不满足时,则通过
SortShuuleManager.canUseSerializedShuule方法判断是否需要采用基于 Tungsten Sort Shuule 实现机制, 而当这两个方法返回都为 false,即都不满足对应的条件时,会自动采用普通运行机制。
因此,当设置了spark.shuule.manager=tungsten-sort时,也不能保证就一定采用基于 Tungsten Sort 的
Shuule 实现机制。
要实现 Tungsten Sort Shuule 机制需要满足以下条件:
Shuule 依赖中不带聚合操作或没有对输出进行排序的要求。
Shuule 的序列化器支持序列化值的重定位(当前仅支持 KryoSerializer Spark SQL 框架自定义的序列化器)。
Shuule 过程中的输出分区个数少于 16777216 个。
实际上,使用过程中还有其他一些限制,如引入 Page 形式的内存管理模型后,内部单条记录的长度不能超过 128 MB (具体内存模型可以参考 PackedRecordPointer 类)。另外,分区个数的限制也是该内存模型导致的。
所以,目前使用基于 Tungsten Sort Shuule 实现机制条件还是比较苛刻的。
基于 Sort 的 ShuGle 机制的优缺点优点:
小文件的数量大量减少,Mapper 端的内存占用变少;
Spark 不仅可以处理小规模的数据,即使处理大规模的数据,也不会很容易达到性能瓶颈。
缺点:
如果 Mapper 中 Task 的数量过大,依旧会产生很多小文件,此时在 Shuule 传数据的过程中到
Reducer 端, Reducer 会需要同时大量地记录进行反序列化,导致大量内存消耗和 GC 负担巨大, 造成系统缓慢,甚至崩溃;
强制了在 Mapper 端必须要排序,即使数据本身并不需要排序;
它要基于记录本身进行排序,这就是 Sort-Based Shuule 最致命的性能消耗

22 . 简述Apache Spark 中的 RDD 是什么 ?

试题回答参考思路:
RDD 代表弹性分布式数据集。它是任何 Spark 应用程序最重要的构建块 。它是不可变的。RDD 属性是:-
弹性:- 它具有容错特性,可以快速恢复丢失的数据。
分布式:- 数据分布在多个节点上以加快处理速度。
数据集:- 我们执行操作的数据点的集合。RDD 通过沿袭图提供容错能力。沿袭图跟踪调用动作后要执行的转换。沿袭图有助于重新计算由于节点故障而丢失或损坏的任何 RDD。RDD 用于低级转换和操作

23 . 简述SparkContext 与 SparkContext 之间的区别是什么 ?

试题回答参考思路:
在 Spark 1.x 版本中,我们必须为每个 API 创建不同的上下文。例如:-
Spark上下文
SQL上下文
Hive上下文 而在 spark 2.x 版本中,引入了一个名为 SparkSession 的新入口点,单独覆盖了所有功能。无需为入口点创建不同的上下文。
SparkContext是访问 spark 功能的主要入口点。它表示 spark 集群的连接,这对于在集群上构建 RDD、累加器和广播变量很有用。我们可以在 spark-shell 中访问 SparkContext 的默认对象,它存在于变量名“sc”中。

SparkSession:-在 spark 2.0 版本之前,我们需要不同的上下文来访问 spark 中的不同功能。而在 spark 2.0 中,我们有一个名为 SparkSession 的统一入口点。它包含 SQLContext、HiveContext 和 StreamingContext。无需创建单独的。在这些上下文中可访问的 API 同样在SparkSession中可用,并且 SparkSession 包含用于实际计算的SparkContext。

24 . 简述什么情况下会产生Spark ShuGle ?

试题回答参考思路:
Spark中会导致shuule操作的有以下几种算子:
repartition类的操作:比如repartition、repartitionAndSortWithinPartitions、coalesce等
byKey类的操作:比如reduceByKey、groupByKey、sortByKey等
join类的操作:比如join、cogroup等
重分区: 一般会shuule,因为需要在整个集群中,对之前所有的分区的数据进行随机,均匀的打乱,然后把数据放入下游新的指定数量的分区内。
byKey类的操作:因为你要对一个key,进行聚合操作,那么肯定要保证集群中,所有节点上的,相同的key,一定是到同一个节点上进行处理。
join类的操作:两个rdd进行join,就必须将相同join key的数据,shuule到同一个节点上,然后进行相同
key的两个rdd数据的笛卡尔乘积

25 . 简述为什么要Spark ShuGle ?

试题回答参考思路:

Shuule就是对数据进行重组,由于分布式计算的特性和要求,在实现细节上更加繁琐和复杂。
在DAG调度的过程中,Stage阶段的划分是根据是否有shuGle过程,也就是存在ShuuleDependency宽依 赖的时候,需要进行shuule,这时候会将作业job划分成多个Stage;并且在划分Stage的时候,构建
ShuuleDependency的时候进行shuule注册,获取后续数据读取所需要的ShuuleHandle,最终每一个job 提交后都会生成一个ResultStage和若干个ShuuleMapStage,其中ResultStage表示生成作业的最终结果所 在的Stage。ResultStage与ShuuleMapStage中的task分别对应着ResultTask与ShuuleMapTask。
RDD 的 Transformation 函数中,又分为窄依赖(narrow dependency)和宽依赖(wide dependency)的操作。窄依赖跟宽依赖的区别是是否发生shuule(洗牌) 操作。宽依赖会发生shuule操作。窄依赖是子RDD的各个分片(partition)不依赖于其他分片,能够独立计算得到结果,宽依赖指子RDD的各个分片会依赖于父 RDD的多个分片,所以会造成父 RDD 的各个分片在集群中重新分片

26 . 简述Spark为什么适合迭代处理 ?

试题回答参考思路:

迭代是重复反馈过程的活动,其目的通常是为了逼近所需目标或结果。每一次对过程的重复称为一次“迭代”,而每一次迭代得到的结果会作为下一次迭代的初始值。
重复执行一系列运算步骤,从前面的量依次求出后面的量的过程。此过程的每一次结果,都是由对前一 次所得结果施行相同的运算步骤得到的。例如利用迭代法求某一数学问题的解。
对计算机特定程序中需要反复执行的子程序(一组指令),进行一次重复,即重复执行程序中的循环,直 到满足某条件为止,亦称为迭代。
Spark为什么适合迭代处理:
Spark迭代运算,采用内存存储中间计算结果,减少了迭代运算的磁盘IO,并通过并行计算DAG图的优 化,减少不同任务之间的依赖,降低延迟等待时间。
相比于MapReduce,Spark在处理数据方面,可以一步接一步直接处理,而MapReduce每一个阶段都要将 数据存入磁盘中,相比而言,Spark会更有优势,也更适合迭代处理。

27 . 简述Spark数据倾斜问题,如何定位,解决方案 ?

试题回答参考思路:

新版数据倾斜及其解决方案
1、数据倾斜
Spark中的数据倾斜问题主要指shuule过程中出现的数据倾斜问题,是由于不同的key对应的数据量不同 导致的不同task所处理的数据量不同的问题
例如,reduce点一共要处理100万条数据,第一个和第二个task分别被分配到了1万条数据,计算5分钟内 完成,第三个task分配到了98万数据,此时第三个task可能需要10个小时完成,这使得整个Spark作业需 要10个小时才能运行完成,这就是数据倾斜所带来的后果
数据倾斜俩大直接致命后果
1)数据倾斜直接会导致一种情况:Out Of Memory
2)运行速度慢
注意,要区分开数据倾斜与数据量过量这两种情况,数据倾斜是指少数task被分配了绝大多数的数据, 因此少数task运行缓慢;数据过量是指所有task被分配的数据量都很大,相差不多,所有task都运行缓慢
数据倾斜的表现:
1) Spark作业的大部分task都执行迅速,只有有限的几个task执行的非常慢,此时可能出现了数据倾 斜,作业可以运行,但是运行得非常慢
2) Spark作业的大部分task都执行迅速,但是有的task在运行过程中会突然报出OOM,反复执行几次都 在某一个task报出OOM错误,此时可能出现了数据倾斜,作业无法正常运行

定位数据倾斜问题:
1)查阅代码中的shuule算子,例如reduceByKey、countByKey、groupByKey、join等算子,根据代码逻 辑判断此处是否会出现数据倾斜
2)查看Spark作业的log文件,log文件对于错误的记录会精确到代码的某一行,可以根据异常定位到的 代码位置来明确错误发生在第几个stage,对应的shuule算子是哪一个
2、解决方案一:聚合元数据
1)避免shuGle过程
绝大多数情况下,Spark作业的数据来源都是Hive表,这些Hive表基本都是经过ETL之后的昨天的数据
为了避免数据倾斜,我们可以考虑避免shuule过程,如果避免了shuule过程,那么从根本上就消除了发 生数据倾斜问题的可能
如果Spark作业的数据来源于Hive表,那么可以先在Hive表中对数据进行聚合,例如按照key进行分组, 将同一key对应的所有value用一种特殊的格式拼接到一个字符串里去,这样,一个key就只有一条数据 了;之后,对一个key的所有value进行处理时,只需要进行map操作即可,无需再进行任何的shuule操 作。通过上述方式就避免了执行shuule操作,也就不可能会发生任何的数据倾斜问题。
对于Hive表中数据的操作,不一定是拼接成一个字符串,也可以是直接对key的每一条数据进行累计计算
要区分开,处理的数据量大和数据倾斜的区别
2)缩小key粒度(增大数据倾斜可能性,降低每个task的数据量)
key的数量增加,可能使数据倾斜更严重
3)增大key粒度(减小数据倾斜可能性,增大每个task的数据量)
如果没有办法对每个key聚合出来一条数据,在特定场景下,可以考虑扩大key的聚合粒度
例如,目前有10万条用户数据,当前key的粒度是(省,城市,区,日期),现在我们考虑扩大粒度, 将key的粒度扩大为(省,城市,日期),这样的话,key的数量会减少,key之间的数据量差异也有可 能会减少,由此可以减轻数据倾斜的现象和问题。(此方法只针对特定类型的数据有效,当应用场景不 适宜时,会加重数据倾斜)
3、解决方案二:过滤导致倾斜的key
如果在Spark作业中允许丢弃某些数据,那么可以考虑将可能导致数据倾斜的key进行过滤,滤除可能导 致数据倾斜的key对应的数据,这样,在Spark作业中就不会发生数据倾斜了
4、解决方案三:提高shuGle操作中的reduce并行度
当方案一和方案二对于数据倾斜的处理没有很好的效果时,可以考虑提高shuule过程中的reduce端并行 度,reduce端并行度的提高就增加了reduce端task的数量,那么每个task分配到的数据量就会相应减少, 由此缓解数据倾斜问题
1) reduce端并行度的设置
在大部分的shuule算子中,都可以传入一个并行度的设置参数,比如reduceByKey(500),这个参数会决 定shuule过程中reduce端的并行度,在进行shuule操作的时候,就会对应着创建指定数量的reduce
task。对于Spark SQL中的shuule类语句,比如group by、join等,需要设置一个参数,即spark.sql.shuule.partitions,该参数代表了shuule read task的并行度,该值默认是200,对于很多场景来说都有点过小
增加shuule read task的数量,可以让原本分配给一个task的多个key分配给多个task,从而让每个task处理比原来更少的数据。举例来说,如果原本有5个key,每个key对应10条数据,这5个key都是分配给一 个task的,那么这个task就要处理50条数据。而增加了shuule read task以后,每个task就分配到一个
key,即每个task就处理10条数据,那么自然每个task的执行时间都会变短了

2) reduce端并行度设置存在的缺陷
提高reduce端并行度并没有从根本上改变数据倾斜的本质和问题(方案一和方案二从根本上避免了数据 倾斜的发生),只是尽可能地去缓解和减轻shuule reduce task的数据压力,以及数据倾斜的问题,适用于有较多key对应的数据量都比较大的情况
该方案通常无法彻底解决数据倾斜,因为如果出现一些极端情况,比如某个key对应的数据量有100万, 那么无论你的task数量增加到多少,这个对应着100万数据的key肯定还是会分配到一个task中去处理, 因此注定还是会发生数据倾斜的。所以这种方案只能说是在发现数据倾斜时尝试使用的第一种手段,尝 试去用嘴简单的方法缓解数据倾斜而已,或者是和其他方案结合起来使用
在理想情况下,reduce端并行度提升后,会在一定程度上减轻数据倾斜的问题,甚至基本消除数据倾 斜;但是,在一些情况下,只会让原来由于数据倾斜而运行缓慢的task运行速度稍有提升,或者避免了 某些task的OOM问题,但是,仍然运行缓慢,此时,要及时放弃方案三,开始尝试后面的方案
5、解决方案四:使用随机key实现双重聚合
当使用了类似于groupByKey、reduceByKey这样的算子时,可以考虑使用随机key实现双重聚合

首先,通过map算子给每个数据的key添加随机数前缀,对key进行打散,将原先一样的key变成不一样的
key,然后进行第一次聚合,这样就可以让原本被一个task处理的数据分散到多个task上去做局部聚合; 随后,去除掉每个key的前缀,再次进行聚合
此方法对于由groupByKey、reduceByKey这类算子造成的数据倾斜由比较好的效果,仅仅适用于聚合类 的shuule操作,适用范围相对较窄。如果是join类的shuule操作,还得用其他的解决方案
此方法也是前几种方案没有比较好的效果时要尝试的解决方案

6、解决方案五:将reduce join转换为map join
正常情况下,join操作都会执行shuule过程,并且执行的是reduce join,也就是先将所有相同的key和对应的value汇聚到一个reduce task中,然后再进行join
普通的join是会走shuule过程的,而一旦shuule,就相当于会将相同key的数据拉取到一个shuule read
task中再进行join,此时就是reduce join。但是如果一个RDD是比较小的,则可以采用广播小RDD全量数据+map算子来实现与join同样的效果,也就是map join,此时就不会发生shuule操作,也就不会发生数据倾斜
注意,RDD是并不能进行广播的,只能将RDD内部的数据通过collect拉取到Driver内存然后再进行广播
1)核心思路
不使用join算子进行连接操作,而使用Broadcast变量与map类算子实现join操作,进而完全规避掉shuule 类的操作,彻底避免数据倾斜的发生和出现。将较小RDD中的数据直接通过collect算子拉取到Driver端的 内存中来,然后对其创建一个Broadcast变量;接着对另外一个RDD执行map类算子,在算子函数内,从Broadcast变量中获取较小RDD的全量数据,与当前RDD的每一条数据按照连接key进行比对,如果连接
key相同的话,那么就将两个RDD的数据用你需要的方式连接起来
根据上述思路,根本不会发生shuule操作,从根本上杜绝了join操作可能导致的数据倾斜问题
当join操作有数据倾斜问题并且其中一个RDD的数据量较小时,可以优先考虑这种方式,效果非常好
2)不使用场景分析
由于Spark的广播变量是在每个Executor中保存一个副本,如果两个RDD数据量都比较大,那么如果将一 个数据量比较大的RDD做成广播变量,那么很有可能会造成内存溢出
7、解决方案六:sample采样对倾斜key单独进行join
在Spark中,如果某个RDD只有一个key,那么在shuule过程中会默认将此key对应的数据打散,由不同的
reduce端task进行处理
当由单个key导致数据倾斜时,可有将发生数据倾斜的key单独提取出来,组成一个RDD,然后用这个原 本会导致倾斜的key组成的RDD根其他RDD单独join,此时,根据Spark的运行机制,此RDD中的数据会在shuule阶段被分散到多个task中去进行join操作。
1)适用场景分析
对于RDD中的数据,可以将其转换为一个中间表,或者是直接使用countByKey()的方式,看一个这个RDD 中各个key对应的数据量,此时如果你发现整个RDD就一个key的数据量特别多,那么就可以考虑使用这 种方法
当数据量非常大时,可以考虑使用sample采样获取10%的数据,然后分析这10%的数据中哪个key可能会 导致数据倾斜,然后将这个key对应的数据单独提取出来
2)不适用场景分析
如果一个RDD中导致数据倾斜的key很多,那么此方案不适用
8、解决方案七:使用随机数以及扩容进行join

如果在进行join操作时,RDD中有大量的key导致数据倾斜,那么进行分拆key也没什么意义,此时就只能 使用最后一种方案来解决问题了,对于join操作,我们可以考虑对其中一个RDD数据进行扩容,另一个
RDD进行稀释后再join
我们会将原先一样的key通过附加随机前缀变成不一样的key,然后就可以将这些处理后的“不同key”分散 到多个task中去处理,而不是让一个task处理大量的相同key。这一种方案是针对有大量倾斜key的情
况,没法将部分key拆分出来进行单独处理,需要对整个RDD进行数据扩容,对内存资源要求很高
1)核心思想
选择一个RDD,使用flatMap进行扩容,对每条数据的key添加数值前缀(1~N的数值),将一条数据映射 为多条数据;(扩容)
选择另外一个RDD,进行map映射操作,每条数据的key都打上一个随机数作为前缀(1~N的随机数);
(稀释)
将两个处理后的RDD,进行join操作
2)局限性
如果两个RDD都很大,那么将RDD进行N倍的扩容显然行不通
使用扩容的方式只能缓解数据倾斜,不能彻底解决数据倾斜问题
3)使用方案七对方案六进一步优化分析
当RDD中有几个key导致数据倾斜时,方案六不再适用,而方案七又非常消耗资源,此时可以引入方案七 的思想完善方案六
(1)对包含少数几个数据量过大的key的那个RDD,通过sample算子采样出一份样本来,然后统计一下 每个key的数量,计算出来数据量最大的是哪几个key
(2)然后将这几个key对应的数据从原来的RDD中拆分出来,形成一个单独的RDD,并给每个key都打上
n以内的随机数作为前缀,而不会导致倾斜的大部分key形成另外一个RDD
(3)接着将需要join的另一个RDD,也过滤出来那几个倾斜key对应的数据并形成一个单独的RDD,将每 条数据膨胀成n条数据,这n条数据都按顺序附加一个0~n的前缀,不会导致倾斜的大部分key也形成另外 一个RDD
(4)再将附加了随机前缀的独立RDD与另一个膨胀n倍的独立RDD进行join,此时就可以将原先相同的
key打散成n份,分散到多个task中去进行join了
(5)而另外两个普通的RDD就照常join即可
(6)最后将两次join的结果使用union算子合并起来即可,就是最终的join结果

28 . Spark的stage如何划分?在源码中是怎么判断属于ShuGle Map Stage或Result Stage的 ?

试题回答参考思路:

Job按照RDD之间的依赖关系是否为宽依赖,由DAGScheduler划分为一个Stage

(1)Result Stage
ResultStage在RDD的某些分区上应用函数来计算action操作的结果。 对于诸如first()和lookup()之类的操作,某些stage可能无法在RDD的所有分区上运行。
简言之,ResultStage是应用action操作在action上进而得出计算结果

(2)ShuGle Map Stage
ShuuleMapStage 是中间的stage,为shuule生产数据。它们在shuule之前出现。当执行完毕之后,结果数据被保存,以便reduce 任务可以获取到

29 . 简述Spark join在什么情况下会变成窄依赖 ?

试题回答参考思路:

如果需要join的两个表,本身已经有分区器,且分区的数目相同,此时,相同的key在同一个分区内,就 是窄依赖。反之,如果两个需要join的表中没有分区器或者分区数量不同,在join的时候需要shuule,那 么就是宽依赖

30 . 简述Spark的内存模型( 重要详细 ) ?

试题回答参考思路:

1、堆内和堆外内存规划
作为一个JVM 进程,Executor 的内存管理建立在JVM的内存管理之上,Spark对 JVM的堆内(On-heap) 空间进行了更为详细的分配,以充分利用内存。同时,Spark引入了堆外(Ou-heap)内存,使之可以直 接在工作节点的系统内存中开辟空间,进一步优化了内存的使用。堆内内存受到JVM统一管理,堆外内 存是直接向操作系统进行内存的申请和释放。

1)堆内内存
默认情况下,Spark 仅仅使用了堆内内存。Executor 端的堆内内存区域大致可以分为以下四大块:
Execution 内存:主要用于存放 Shuule、Join、Sort、Aggregation 等计算过程中的临时数据
Storage 内存:主要用于存储 spark 的 cache 数据,例如RDD的缓存、unroll数据;
用户内存(User Memory):主要用于存储 RDD 转换操作所需要的数据,例如 RDD 依赖等信息。
预留内存(Reserved Memory):系统预留内存,会用来存储Spark内部对象。
堆内内存的大小,由Spark应用程序启动时的 –executor-memory 或 spark.executor.memory 参数配置。
Executor 内运行的并发任务共享 JVM 堆内内存,这些任务在缓存 RDD 数据和广播(Broadcast)数据时占用的内存被规划为存储(Storage)内存,而这些任务在执行 Shuule 时占用的内存被规划为执行
(Execution)内存,剩余的部分不做特殊规划,那些Spark内部的对象实例,或者用户定义的 Spark 应用程序中的对象实例,均占用剩余的空间。不同的管理模式下,这三部分占用的空间大小各不相同。
Spark对堆内内存的管理是一种逻辑上的”规划式”的管理,因为对象实例占用内存的申请和释放都由JVM
完成,Spark只能在申请后和释放前记录这些内存,我们来看其具体流程:
申请内存流程如下:
Spark 在代码中 new 一个对象实例;
JVM 从堆内内存分配空间,创建对象并返回对象引用;
Spark 保存该对象的引用,记录该对象占用的内存。

释放内存流程如下:
Spark记录该对象释放的内存,删除该对象的引用;
等待JVM的垃圾回收机制释放该对象占用的堆内内存。
2)堆外内存
为了进一步优化内存的使用以及提高Shuule时排序的效率,Spark引入了堆外(Ou-heap)内存,使之可 以直接在工作节点的系统内存中开辟空间,存储经过序列化的二进制数据。
堆外内存意味着把内存对象分配在Java虚拟机的堆以外的内存,这些内存直接受操作系统管理(而不是 虚拟机)。这样做的结果就是能保持一个较小的堆,以减少垃圾收集对应用的影响。
利用JDK Unsafe API(从Spark 2.0开始,在管理堆外的存储内存时不再基于Tachyon,而是与堆外的执行内存一样,基于 JDK Unsafe API 实现),Spark 可以直接操作系统堆外内存,减少了不必要的内存开销,以及频繁的 GC 扫描和回收,提升了处理性能。堆外内存可以被精确地申请和释放(堆外内存之所以能够被精确的申请和释放,是由于内存的申请和释放不再通过JVM机制,而是直接向操作系统申请, JVM对于内存的清理是无法准确指定时间点的,因此无法实现精确的释放),而且序列化的数据占用的 空间可以被精确计算,所以相比堆内内存来说降低了管理的难度,也降低了误差。
在默认情况下堆外内存并不启用,可通过配置 spark.memory.ouHeap.enabled 参数启用,并由
spark.memory.ouHeap.size 参数设定堆外空间的大小。除了没有 other 空间,堆外内存与堆内内存的划分方式相同,所有运行中的并发任务共享存储内存和执行内存。

2、内存空间分配
1)静态内存管理
在Spark最初采用的静态内存管理机制下,存储内存、执行内存和其他内存的大小在Spark应用程序运行 期间均为固定的,但用户可以应用程序启动前进行配置
可以看到,可用的堆内内存的大小需要按照下列方式计算:
1 可用的存储内存 = systemMaxMemory * spark.storage.memoryFraction * spark.storage.safety Fraction
2 可用的执行内存 = systemMaxMemory * spark.shuffle.memoryFraction * spark.shuffle.safety Fraction
其中systemMaxMemory取决于当前JVM堆内内存的大小,最后可用的执行内存或者存储内存要在此基础 上与各自的memoryFraction 参数和safetyFraction 参数相乘得出。上述计算公式中的两个 safetyFraction 参数,其意义在于在逻辑上预留出 1-safetyFraction 这么一块保险区域,降低因实际内存超出当前预设范围而导致 OOM 的风险(上文提到,对于非序列化对象的内存采样估算会产生误差)。值得注意的是, 这个预留的保险区域仅仅是一种逻辑上的规划,在具体使用时 Spark 并没有区别对待,和”其它内存”一样交给了 JVM 去管理。
Storage内存和Execution内存都有预留空间,目的是防止OOM,因为Spark堆内内存大小的记录是不准确 的,需要留出保险区域。
堆外的空间分配较为简单,只有存储内存和执行内存,如下图所示。可用的执行内存和存储内存占用的 空间大小直接由参数spark.memory.storageFraction 决定,由于堆外内存占用的空间可以被精确计算,所以无需再设定保险区域

2)统一内存管理
Spark1.6 之后引入的统一内存管理机制,与静态内存管理的区别在于存储内存和执行内存共享同一块空间,可以动态占用对方的空闲区域,
其中最重要的优化在于动态占用机制,其规则如下:
设定基本的存储内存和执行内存区域(spark.storage.storageFraction参数),该设定确定了双方各 自拥有的空间的范围;
双方的空间都不足时,则存储到硬盘;若己方空间不足而对方空余时,可借用对方的空间;(存储 空间不足是指不足以放下一个完整的Block)
执行内存的空间被对方占用后,可让对方将占用的部分转存到硬盘,然后”归还”借用的空间;
存储内存的空间被对方占用后,无法让对方”归还”,因为需要考虑 Shuule过程中的很多因素,实现起来较为复杂。
凭借统一内存管理机制,Spark 在一定程度上提高了堆内和堆外内存资源的利用率,降低了开发者维护
Spark 内存的难度,但并不意味着开发者可以高枕无忧。譬如,所以如果存储内存的空间太大或者说缓存的数据过多,反而会导致频繁的全量垃圾回收,降低任务执行时的性能,因为缓存的 RDD 数据通常都是长期驻留内存的。所以要想充分发挥 Spark 的性能,需要开发者进一步了解存储内存和执行内存各自的管理方式和实现原理。

3、存储内存管理
1) RDD的持久化机制
弹性分布式数据集(RDD)作为 Spark 最根本的数据抽象,是只读的分区记录(Partition)的集合,只能基于在稳定物理存储中的数据集上创建,或者在其他已有的RDD上执行转换(Transformation)操作产 生一个新的RDD。转换后的RDD与原始的RDD之间产生的依赖关系,构成了血统(Lineage)。凭借血
统,Spark 保证了每一个RDD都可以被重新恢复。但RDD的所有转换都是惰性的,即只有当一个返回结果给Driver的行动(Action)发生时,Spark才会创建任务读取RDD,然后真正触发转换的执行。
Task在启动之初读取一个分区时,会先判断这个分区是否已经被持久化,如果没有则需要检查
Checkpoint 或按照血统重新计算。所以如果一个 RDD 上要执行多次行动,可以在第一次行动中使用
persist或cache 方法,在内存或磁盘中持久化或缓存这个RDD,从而在后面的行动时提升计算速度
事实上,cache 方法是使用默认的 MEMORY_ONLY 的存储级别将 RDD 持久化到内存,故缓存是一种特殊的持久化。 堆内和堆外存储内存的设计,便可以对缓存RDD时使用的内存做统一的规划和管理。
RDD的持久化由 Spark的Storage模块负责,实现了RDD与物理存储的解耦合。Storage模块负责管理Spark 在计算过程中产生的数据,将那些在内存或磁盘、在本地或远程存取数据的功能封装了起来。在具体实 现时Driver端和 Executor 端的Storage模块构成了主从式的架构,即Driver端的BlockManager为Master,
Executor端的BlockManager 为 Slave。Storage模块在逻辑上以Block为基本存储单位,RDD的每个Partition 经过处理后唯一对应一个 Block(BlockId 的格式为rdd_RDD-ID_PARTITION-ID )。Driver端的Master负责整个Spark应用程序的Block的元数据信息的管理和维护,而Executor端的Slave需要将Block的更新等状态 上报到Master,同时接收Master 的命令,例如新增或删除一个RDD在对RDD持久化时,Spark规定了MEMORY_ONLY、MEMORY_AND_DISK 等7种不同的存储级别,而存储级别是以下5个变量的组合
1 class StorageLevel private(
2 private var _useDisk: Boolean, //磁盘
3 private var _useMemory: Boolean, //这里其实是指堆内内存
4 private var _useOffHeap: Boolean, //堆外内存
5 private var _deserialized: Boolean, //是否为非序列化
6 private var _replication: Int = 1 //副本个数7
)

2) RDD的缓存过程
RDD 在缓存到存储内存之前,Partition中的数据一般以迭代器( Iterator )的数据结构来访问,这是Scala语言中一种遍历数据集合的方法。通过Iterator可以获取分区中每一条序列化或者非序列化的数据 项(Record),这些Record的对象实例在逻辑上占用了JVM堆内内存的other部分的空间,同一Partition的不 同 Record 的存储空间并不连续。
RDD 在缓存到存储内存之后,Partition 被转换成Block,Record在堆内或堆外存储内存中占用一块连续的空间。将Partition由不连续的存储空间转换为连续存储空间的过程,Spark称之为"展开"(Unroll)。
Block 有序列化和非序列化两种存储格式,具体以哪种方式取决于该 RDD 的存储级别。非序列化的Block 以一种 DeserializedMemoryEntry 的数据结构定义,用一个数组存储所有的对象实例,序列化的Block则以SerializedMemoryEntry的数据结构定义,用字节缓冲区(ByteBuuer)来存储二进制数据。每个Executor 的 Storage模块用一个链式Map结构(LinkedHashMap)来管理堆内和堆外存储内存中所有的Block对象的实例,对这个LinkedHashMap新增和删除间接记录了内存的申请和释放。
因为不能保证存储空间可以一次容纳 Iterator 中的所有数据,当前的计算任务在 Unroll 时要向MemoryManager 申请足够的Unroll空间来临时占位,空间不足则Unroll失败,空间足够时可以继续进行。
对于序列化的Partition,其所需的Unroll空间可以直接累加计算,一次申请。
对于非序列化的 Partition 则要在遍历 Record 的过程中依次申请,即每读取一条 Record,采样估算其所需的Unroll空间并进行申请,空间不足时可以中断,释放已占用的Unroll空间。
如果最终Unroll成功,当前Partition所占用的Unroll空间被转换为正常的缓存 RDD的存储空间

在静态内存管理时,Spark 在存储内存中专门划分了一块 Unroll 空间,其大小是固定的,统一内存管理时则没有对 Unroll 空间进行特别区分,当存储空间不足时会根据动态占用机制进行处理。
3)淘汰与落盘
由于同一个Executor的所有的计算任务共享有限的存储内存空间,当有新的 Block 需要缓存但是剩余空间不足且无法动态占用时,就要对LinkedHashMap中的旧Block进行淘汰(Eviction),而被淘汰的Block 如果其存储级别中同时包含存储到磁盘的要求,则要对其进行落盘(Drop),否则直接删除该Block。
存储内存的淘汰规则为:
被淘汰的旧Block要与新Block的MemoryMode相同,即同属于堆外或堆内内存; 新旧Block不能属于同一个RDD,避免循环淘汰;
旧Block所属RDD不能处于被读状态,避免引发一致性问题;
遍历LinkedHashMap中Block,按照最近最少使用(LRU)的顺序淘汰,直到满足新Block所需的空 间。其中LRU是LinkedHashMap的特性。
落盘的流程则比较简单,如果其存储级别符合useDisk为true的条件,再根据其 deserialized判断是否是非序列化的形式,若是则对其进行序列化,最后将数据存储到磁盘,在Storage模块中更新其信息。
4、执行内存管理
执行内存主要用来存储任务在执行Shuule时占用的内存,Shuule是按照一定规则对RDD数据重新分区的 过程,我们来看Shuule的Write和Read两阶段对执行内存的使用:
1) Shuule Write
若在map端选择普通的排序方式,会采用ExternalSorter进行外排,在内存中存储数据时主要占用堆内执 行空间。
若在map端选择 Tungsten 的排序方式,则采用ShuuleExternalSorter直接对以序列化形式存储的数据排序,在内存中存储数据时可以占用堆外或堆内执行空间,取决于用户是否开启了堆外内存以及堆外执行 内存是否足够。
2) Shuule Read
在对reduce端的数据进行聚合时,要将数据交给Aggregator处理,在内存中存储数据时占用堆内执行空 间。

如果需要进行最终结果排序,则要将再次将数据交给ExternalSorter 处理,占用堆内执行空间。
在ExternalSorter和Aggregator中,Spark会使用一种叫AppendOnlyMap的哈希表在堆内执行内存中存储数 据,但在 Shuule 过程中所有数据并不能都保存到该哈希表中,当这个哈希表占用的内存会进行周期性地采样估算,当其大到一定程度,无法再从MemoryManager 申请到新的执行内存时,Spark就会将其全部内容存储到磁盘文件中,这个过程被称为溢存(Spill),溢存到磁盘的文件最后会被归并(Merge)。
Shuule Write 阶段中用到的Tungsten是Databricks公司提出的对Spark优化内存和CPU使用的计划(钨丝计划),解决了一些JVM在性能上的限制和弊端。Spark会根据Shuule的情况来自动选择是否采用Tungsten 排序。
Tungsten 采用的页式内存管理机制建立在MemoryManager之上,即 Tungsten 对执行内存的使用进行了一步的抽象,这样在 Shuule 过程中无需关心数据具体存储在堆内还是堆外。
每个内存页用一个MemoryBlock来定义,并用 Object obj 和 long ouset 这两个变量统一标识一个内存页在系统内存中的地址。
堆内的MemoryBlock是以long型数组的形式分配的内存,其obj的值为是这个数组的对象引用,ouset是
long型数组的在JVM中的初始偏移地址,两者配合使用可以定位这个数组在堆内的绝对地址;堆外的
MemoryBlock是直接申请到的内存块,其obj为null,ouset是这个内存块在系统内存中的64位绝对地址。Spark用MemoryBlock巧妙地将堆内和堆外内存页统一抽象封装,并用页表(pageTable)管理每个Task申请 到的内存页。
Tungsten 页式管理下的所有内存用64位的逻辑地址表示,由页号和页内偏移量组成:
页号:占13位,唯一标识一个内存页,Spark在申请内存页之前要先申请空闲页号。 页内偏移量:占51位,是在使用内存页存储数据时,数据在页内的偏移地址。
有了统一的寻址方式,Spark 可以用64位逻辑地址的指针定位到堆内或堆外的内存,整个Shuule Write排序的过程只需要对指针进行排序,并且无需反序列化,整个过程非常高效,对于内存访问效率和CPU使 用效率带来了明显的提升。
Spark的存储内存和执行内存有着截然不同的管理方式:对于存储内存来说,Spark用一个LinkedHashMap来集中管理所有的Block,Block由需要缓存的 RDD的Partition转化而成;而对于执行内存,Spark用AppendOnlyMap来存储 Shuule过程中的数据,在Tungsten排序中甚至抽象成为页式内存管理,开辟了全新的JVM内存管理机制。

31 . 简述Spark中7种存储级别 ?

试题回答参考思路:

MEMORY_ONLY 以非序列化的Java对象的方式持久化在JVM内存中。如果内存无法完 全存储RDD所有的partition,那么那些没有持久化的partition就会在下 一次需要使用它们的时候,重新被计算

MEMORY_AND_DISK 同上,但是当某些partition无法存储在内存中时,会持久化到磁盘中。下次需要使用这些partition时,需要从磁盘上读取

MEMORY_ONLY_SER 同MEMORY_ONLY,但是会使用Java序列化方式,将Java对象序列化后 进行持久化。可以减少内存开销,但是需要进行反序列化,因此会加 大CPU开销
MEMORY_AND_DISK_SER 同MEMORY_AND_DISK,但是使用序列化方式持久化Java对象
DISK_ONLY 使用非序列化Java对象的方式持久化,完全存储到磁盘上
MEMORY_ONLY_2 MEMORY_AND_DISK_2等
等 如果是尾部加了2的持久化级别,表示将持久化数据复用一份,保存 到其他节点,从而在数据丢失时,不需要再次计算,只需要使用备份数据即可

32 . 简述Spark分哪几个部分(模块)?分别有什么作用(做什么,自己用过哪些,做过什么) ?

试题回答参考思路:

Spark Core
Spark Core 中提供了 Spark 最基础与最核心的功能,Spark 其他的功能如:Spark SQL, Spark Streaming,GraphX, MLlib 都是在 Spark Core 的基础上进行扩展的

Spark SQL
Spark SQL 是Spark 用来操作结构化数据的组件。通过 Spark SQL,用户可以使用 SQL或者Apache Hive 版本的 SQL 方言(HQL)来查询数据。

Spark Streaming
Spark Streaming 是 Spark 平台上针对实时数据进行流式计算的组件,提供了丰富的处理数据流的API。

Spark MLlib
MLlib 是 Spark 提供的一个机器学习算法库。MLlib 不仅提供了模型评估、数据导入等额外的功能,还提供了一些更底层的机器学习原语。

Spark GraphX
GraphX 是 Spark 面向图计算提供的框架与算法库

33 . RDD的宽依赖和窄依赖,举例一些算子 ?

试题回答参考思路:

RDD和它依赖的parent RDD(s)的关系有两种不同的类型,即窄依赖(narrow dependency)和宽依赖(wide dependency)
1、窄依赖
窄依赖指的是每一个parent RDD的Partition最多被子RDD的一个Partition使用
比如map,filter,union属于窄依赖

2、宽依赖
宽依赖指的是多个子RDD的Partition会依赖同一个parent RDD的Partition
具有宽依赖的 transformations 包括:sort,reduceByKey,groupByKey,join,和调用rePartition函数的任何操作

34 . Spark SQL的GroupBy会造成窄依赖吗 ?

试题回答参考思路:

不会,它属于宽依赖

35 . 简述GroupBy是行动算子吗 ?

试题回答参考思路:

GroupBy在Spark中是Transformation,产生shuule

36 . 简述Spark的宽依赖和窄依赖,为什么要这么划分 ?

试题回答参考思路:

Spark中RDD的高效与DAG图有着莫大的关系,在DAG调度中需要对计算过程划分stage,而划分依据就是
RDD之间的依赖关系。针对不同的转换函数,RDD之间的依赖关系分类窄依赖(narrow dependency)和宽依赖(wide dependency, 也称 shuule dependency)。
1、宽依赖与窄依赖
注意:也可参考前面的题目
窄依赖是指父RDD的每个分区只被子RDD的一个分区所使用,子RDD分区通常对应常数个父RDD分 区(O(1),与数据规模无关)
相应的,宽依赖是指父RDD的每个分区都可能被多个子RDD分区所使用,子RDD分区通常对应所有 的父RDD分区(O(n),与数据规模有关)
2、划分宽窄依赖原由
需要从宽窄依赖和容错性方面考虑。

Spark基于lineage的容错性是指,如果一个RDD出错,那么可以从它的所有父RDD重新计算所得,如果一 个RDD仅有一个父RDD(即窄依赖),那么这种重新计算的代价会非常小
对于Spark基于Checkpoint(物化)的容错机制,在上图中,宽依赖得到的结果(经历过Shuule过程)是 很昂贵的,因此,Spark将此结果物化到磁盘上了,以备后面使用。
对于join操作有两种情况,如果join操作的每个partition仅仅和已知的Partition进行join,此时的join操作 就是窄依赖;其他情况的join操作就是宽依赖;因为是确定的Partition数量的依赖关系,所以就是窄依 赖,得出一个推论,窄依赖不仅包含一对一的窄依赖,还包含一对固定个数的窄依赖(也就是说对父RDD的依赖的Partition的数量不会随着RDD数据规模的改变而改变)。
如果不进行宽窄依赖的划分,对于一些算子随便使用,可以使用窄依赖算子时也使用宽依赖算子,就会 造成资源浪费,导致效率低下;同理,当需要使用宽依赖算子时,却使用窄依赖算子,则会导致我们得 不到需要的结果。
相比于宽依赖,窄依赖对优化还有以下几点优势:
宽依赖往往对应着shuule操作,需要在运行过程中将同一个父RDD的分区传入到不同的子RDD分区 中,中间可能涉及多个节点之间的数据传输;而窄依赖的每个父RDD的分区只会传入到一个子RDD 分区中,通常可以在一个节点内完成转换。
当RDD分区丢失时(某个节点故障),spark会对数据进行重算。
对于窄依赖,由于父RDD的一个分区只对应一个子RDD分区,这样只需要重算和子RDD分区对 应的父RDD分区即可,所以这个重算对数据的利用率是100%的;
对于宽依赖,重算的父RDD分区对应多个子RDD分区,这样实际上父RDD 中只有一部分的数据是被用于恢复这个丢失的子RDD分区的,另一部分对应子RDD的其它未丢失分区,这就造成了 多余的计算;更一般的,宽依赖中子RDD分区通常来自多个父RDD分区,极端情况下,所有的 父RDD分区都要进行重新计算。
如下图所示,b1分区丢失,则需要重新计算a1,a2和a3,这就产生了冗余计算(a1,a2,a3中对应
b2的数据)。

37 . Spark中的Transform和Action,为什么Spark要把操作分为Transform 和Action?常用的列举一些,说下算子原理 ?

试题回答参考思路:

Spark在运行转换中通过算子对RDD进行转换。 算子是RDD中定义的函数,可以对RDD中的数据进行转换和操作

输入:在Spark程序运行中,数据从外部数据空间(如分布式存储:textFile读取HDFS等,parallelize方法 输入Scala集合或数据)输入Spark,数据进入Spark运行时数据空间,转化为Spark中的数据块,通过BlockManager进行管理。
运行:在Spark数据输入形成RDD后便可以通过变换算子,如filter等,对数据进行操作并将RDD转化为新 的RDD,通过Action算子,触发Spark提交作业。 如果数据需要复用,可以通过Cache算子,将数据缓存到内存。
输出:程序运行结束数据会输出Spark运行时空间,存储到分布式存储中(如saveAsTextFile输出到
HDFS),或Scala数据或集合中(collect输出到Scala集合,count返回Scala int型数据)。
1、Transform和Action
Transformation是得到一个新的RDD,方式很多,比如从数据源生成一个新的RDD,从RDD生成一个新的RDD
Action是得到一个值,或者一个结果(直接将RDD cache到内存中)
因为所有的Transformation都是采用的懒策略,就是如果只是将Transformation提交是不会执行计算的, 计算只有在Action被提交的时候才被触发。这样有利于减少内存消耗,提高了执行效率。
2、算子原理1)Transformation
map(func):返回一个新的分布式数据集,由每个原元素经过func函数转换后组成。
filter(func):返回一个新的数据集,由经过func函数后返回值为true的原元素组成。
flatMap(func):类似于map,但是每一个输入元素,会被映射为0到多个输出元素(因此,func函数的返 回值是一个Seq,而不是单一元素)。
union(otherDataset):返回一个新的数据集,由原数据集和参数联合而成。
groupByKey([numTasks]):在一个由(K,V)对组成的数据集上调用,返回一个(K,Seq[V])对的数据
集。注意:默认情况下,使用8个并行任务进行分组,你可以传入numTask可选参数,根据数据量设置不 同数目的Task。
reduceByKey(func, [numTasks]):在一个(K,V)对的数据集上使用,返回一个(K,V)对的数据集,
key相同的值,都被使用指定的reduce函数聚合到一起。和groupbykey类似,任务的个数是可以通过第二 个可选参数来配置的。
join(otherDataset, [numTasks]):在类型为(K,V)和(K,W)类型的数据集上调用,返回一个(K,(V,W))对, 每个key中的所有元素都在一起的数据集。
2) Action
reduce(func):通过函数func聚集数据集中的所有元素。Func函数接受2个参数,返回一个值。这个函数 必须是关联性的,确保可以被正确的并发执行。
collect():在Driver的程序中,以数组的形式,返回数据集的所有元素。这通常会在使用filter或者其它操 作后,返回一个足够小的数据子集再使用,直接将整个RDD集Collect返回,很可能会让Driver程序OOM。
count():返回数据集的元素个数。
foreach(func): 在数据集的每一个元素上,运行函数func。这通常用于更新一个累加器变量,或者和外部存储系统做交互。

38 . 简述Spark的哪些算子会有shuGle过程 ?

试题回答参考思路:

spark中会导致shuGle操作的有以下几种算子:
1、重分区类操作
比如repartition、repartitionAndSortWithinPartitions、coalesce(shuule=true)等。重分区一般会shuule,因为需要在整个集群中,对之前所有的分区的数据进行随机,均匀的打乱,然后把数据放入下游新的指定 数量的分区内。
2、聚合、bykey类操作

比如reduceByKey、groupByKey、sortByKey等。byKey类的操作要对一个key,进行聚合操作,那么肯定 要保证集群中,所有节点上的相同的key,移动到同一个节点上进行处理。
3、集合/表间交互操作
比如join、cogroup等。两个rdd进行join,就必须将相同join key的数据,shuule到同一个节点上,然后进行相同key的两个rdd数据的笛卡尔乘积。
4、去重类操作
如distinct。
5、排序类操作
如sortByKey。
无shuGle操作的一些算子:
如map,filter,union等

39 . 简述Spark有了RDD,为什么还要有Dataform和DataSet ?

试题回答参考思路:

RDD叫做弹性分布式数据集,与RDD类似,DataFrame是一个分布式数据容器,但是DataFrame不是类型 安 全 的 。 DataSet是DataFrame API的一个扩展,是Spark最新的数据抽象,结合了RDD和DataFrame的优点

40 . 简述Spark的RDD、DataFrame、DataSet、DataStream区别 ?

试题回答参考思路:

RDD
RDD(Resilient Distributed Dataset)叫做分布式数据集,是Spark中最基本的数据抽象。代码中是一个抽象类,它代表一个不可变、可分区、里面的元素可并行计算的集合。
DataFrame
DataFrame是一种以RDD为基础的分布式数据集,类似于传统数据库中的二维表格。DataFrame与RDD的 主要区别在于,前者带有schema元信息,即DataFrame所表示的二维表数据集的每一列都带有名称和类 型。
DataSet
1)是Dataframe API的一个扩展,是Spark最新的数据抽象。它提供了RDD的优势(强类型,使用强大的
lambda函数的能力)以及Spark SQL优化执行引擎的优点。
2)用户友好的API风格,既具有类型安全检查也具有Dataframe的查询优化特性。
3) Dataset支持编解码器,当需要访问非堆上的数据时可以避免反序列化整个对象,提高了效率。
4)样例类被用来在Dataset中定义数据的结构信息,样例类中每个属性的名称直接映射到DataSet中的字 段名称。
5) Dataframe是Dataset的特列,DataFrame=Dataset[Row] ,所以可以通过as方法将Dataframe转换为
Dataset。Row是一个类型,跟Car、Person这些的类型一样,所有的表结构信息我都用Row来表示。
6) DataSet是强类型的。比如可以有Dataset[Car],Dataset[Person]。
7) DataFrame只是知道字段,但是不知道字段的类型,所以在执行这些操作的时候是没办法在编译的时 候检查是否类型失败的,比如你可以对一个String进行减法操作,在执行的时候才报错,而DataSet不仅 仅知道字段,而且知道字段类型,所以有更严格的错误检查。就跟JSON对象和类对象之间的类比。
DataStream
DStream是随时间推移而收到的数据的序列。在内部,每个时间区间收到的数据都作为RDD存在,而
DStream是由这些RDD所组成的序列(因此得名“离散化”)。所以简单来将,DStream就是对RDD在实时数据 处理场景的一种封装。

41 . 简述Spark的Job、Stage、Task分别介绍下,如何划分 ?

试题回答参考思路:

一个job含有多个stage,一个stage含有多个task。action 的触发会生成一个job,Job会提交给DAGScheduler,分解成Stage。
Job:
包含很多task的并行计算,可以认为是Spark RDD 里面的action,每个action的计算会生成一个job。用户提交的Job会提交给DAGScheduler,Job会被分解成Stage和Task。
Stage:
DAGScheduler根据shuule将job划分为不同的stage,同一个stage中包含多个task,这些tasks有相同 的shuule dependencies。
一个Job会被拆分为多组Task,每组任务被称为一个Stage,就像Map Stage,Reduce Stage。
Task:
即stage下的一个任务执行单元,一般来说,一个rdd有多少个partition,就会有多少个task,因为 每一个task只是处理一个partition上的数据。
每个executor执行的task的数目,可以由submit时,–num-executors(on yarn)来指定。

Job->Stage->Task每一层都是1对n的关系

42 . 简述Application 、job、Stage、task之间的关系 ?

试题回答参考思路:

Application是spark的一个应用程序,它包含了客户端写好的代码以及任务运行的时候需要的资源信息。 后期一个application中有很多个action操作,一个action操作就是一个job,一个job会存在大量的宽依赖,后期会按照宽依赖进行stage的划分,一个job又产生很多个stage,每一个stage内部有很多可以并行的task

43 . 简述Stage内部逻辑 ?

试题回答参考思路:

1)每一个stage中按照RDD的分区划分了很多个可以并行运行的task
2)把每一个stage中这些可以并行运行的task都封装到一个taskSet集合中
3)前面stage中task的输出结果数据 ,是后面stage中task输入数据

44 . 简述为什么要根据宽依赖划分Stage ?

试题回答参考思路:

DAG(Directed Acyclic Graph)有向无环图是由点和线组成的拓扑图形,该图形具有方向,不会闭环。原始的RDD通过一系列的转换就形成了DAG,根据RDD之间的依赖关系的不同将DAG划分成不同的Stage, 对于窄依赖,partition的转换处理在Stage中完成计算。对于宽依赖,由于有Shuule的存在,只能在parent RDD处理完成后,才能开始接下来的计算,因此宽依赖是划分Stage的依据

45 . 简述为什么要划分Stage ?

试题回答参考思路:

由于一个job任务中可能有大量的宽窄依赖,窄依赖不会产生shuule,宽依赖会产生shuule。后期划分完stage之后,在同一个stage中只有窄依赖,并没有宽依赖,这些窄依赖对应的task就可以相互独立的取运 行。划分完stage之后,它内部是有很多可以并行运行task

46 . 简述Stage的数量等于什么 ?

试题回答参考思路:

等于宽依赖数量+1

47 . 简述对RDD、DAG 和Task的理解 ?

试题回答参考思路:

1、RDD
RDD(Resilient Distributed Dataset)叫做分布式数据集,是Spark中最基本的数据抽象。代码中是一个抽象类,它代表一个弹性的、不可变、可分区、里面的元素可并行计算的集合。
RDD特点:
1)弹性
存储的弹性:内存和磁盘的自动切换;
容错的弹性:数据丢失可以自动回复; 计算的弹性:计算出错重试机制;
分片的弹性:可根据需要重新分片。
2)分布式
数据存储在大数据集群的不同节点上
3)数据集
RDD封装了计算逻辑,并不保存数据4)数据抽象
RDD是一个抽象类,需要子类具体实现
5)不可变
RDD封装了计算逻辑,是不可以改变的,想要改变,只能产生新的RDD,在新的RDD里面封装计算逻辑
6)可分区、并行计算
RDD属性:
1)一组分区(Partition),即是数据集的基本组成单位;
2)一个计算每个分区的函数;
3)RDD之间的依赖关系;
4)一个Partitioner,即RDD的分片函数;控制分区的数据流向(键值对);
5)一个列表,存储存取每个Partition的优先位置(preferredlocation)。移动数据不如移动资源,除非 资源不够。
2、DAG
DAG(Directed Acyclic Graph)有向无环图是由点和线组成的拓扑图形,该图形具有方向,不会闭环。原始的RDD通过一系列的转换就形成了DAG,根据RDD之间的依赖关系的不同将DAG划分成不同的Stage, 对于窄依赖,partition的转换处理在Stage中完成计算。对于宽依赖,由于有Shuule的存在,只能在parent RDD处理完成后,才能开始接下来的计算,宽依赖是划分Stage的依据。
3、Task
在RDD中,RDD任务切分中间分为:Application、Job、Stage和Task。Task是stage下的一个任务执行单 元,一般来说,一个rdd有多少个partition,就会有多少个task,因为每一个task只是处理一个partition上 的数据。

一个Stage阶段中,最后一个RDD的分区个数就是Task的个数。
每个executor执行的task的数目, 可以由submit时,–num-executors(on yarn) 来指定

48 . 简述DAG为什么适合Spark ?

试题回答参考思路:

DAG,全称 Directed Acyclic Graph, 中文为:有向无环图。在 Spark 中, 使用 DAG 来描述我们的计算逻辑。
1、什么是DAG
DAG 是一组顶点和边的组合。顶点代表了 RDD, 边代表了对 RDD 的一系列操作。
DAG Scheduler 会根据 RDD 的 transformation 动作,将 DAG 分为不同的 stage,每个 stage 中分为多个
task,这些 task 可以并行运行。
2、DAG解决了什么问题
DAG 的出现主要是为了解决 Hadoop MapReduce 框架的局限性。
MapReduce的局限性主要有两个:
每个MapReduce操作都是相互独立的,Hadoop不知道接下来会有哪些Map Reduce。每一步的输出结果,都会持久化到硬盘或者HDFS上。
当以上两个特点结合之后,我们就可以想象,如果在某些迭代的场景下,MapReduce 框架会对硬盘和
HDFS 的读写造成大量浪费。
而且每一步都是堵塞在上一步中,所以当我们处理复杂计算时,会需要很长时间,但是数据量却不大。 所以Spark 中引入了 DAG,它可以优化计算计划,比如减少 shuGle 数据

49 . 简述Spark的DAG以及它的生成过程 ?

试题回答参考思路:

DAG(Directed Acyclic Graph)叫做有向无环图,原始的RDD通过一系列的转换就形成了DAG,根据RDD之间依赖关系的不同将DAG划分成不同的Stage(调度阶段)。
对于窄依赖,partition的转换处理在一个Stage中完成计算。
对于宽依赖,由于有Shuule的存在,只能在parent RDD处理完成后,才能开始接下来的计算,因此宽依赖是划分Stage的依据。

DAG的边界:
开始:通过SparkContext创建的RDD
触发Action,一旦触发Action就形成了一个完整的DAG
一个Spark的Application应用中一个或者多个DAG(也就是一个Job),取决于触发了多少次Action。
在Spark中,DAG生成的流程关键在于回溯,在程序提交后,高层调度器将所有的RDD看成是一个Stage, 然后对此Stage进行从后往前的回溯,遇到Shuule就断开,遇到窄依赖,则归并到同一个Stage。等到所 有的步骤回溯完成,便生成一个DAG图

50 . 简述DAGScheduler如何划分?

试题回答参考思路:

DAGScheduler是面向Stage的高层级的调度器,DAGScheduler把DAG拆分成很多的Tasks,每组的Tasks都 是一个Stage,解析时是以Shuule为边界反向解析构建Stage,每当遇到Shuule,就会产生新的Stage,然后以一个个TaskSet(每个Stage封装一个TaskSet)的形式提交给底层调度器TaskScheduler。
DAGScheduler需要记录哪些RDD被存入磁盘等物化动作,同时要寻求Task的最优化调度,如在Stage内部 数据的本地性等。DAGScheduler还需要监视因为Shuule跨节点输出可能导致的失败,如果发现这个Stage 失败,可能就要重新提交该Stage。
在Spark源码中,DAGScheduler是整个Spark Application的入口,即在SparkContext中声明并实例化。在实例化DAGScheduler之前,已经实例化了SchedulerBackend和底层调度器TaskScheduler,而
SchedulerBackend和TaskScheduler是通过SparkContext的方法createTaskScheduler实例化的。
DAGScheduler在提交TaskSet给底层调度器的时候是面向TaskScheduler接口的,这符合面向对象中依赖 抽象,而不依赖具体实现的原则,带来底层资源调度器的可插拔性,以至于Spark可以运行在众多的部 署模式上,如Standalone、Yarn、Mesos、Local及其他自定义的部署模式。
DAGScheduler划分Stage的原理
Spark将数据在分布式环境下分区,然后将作业转化为DAG,并分阶段进行DAG的调度和任务的分布式并 行处理。DAG将调度提交给DAGScheduler,DAGScheduler调度时会根据是否需要经过Shuule过程将Job划 分为多个Stage。

51 . 简述Spark容错机制( 重点 ) ?

试题回答参考思路:

1、容错方式
容错指的是一个系统在部分模块出现故障时还能否持续的对外提供服务,一个高可用的系统应该具有很 高的容错性;对于一个大的集群系统来说,机器故障、网络异常等都是很常见的,Spark这样的大型分 布式计算集群提供了很多的容错机制来提高整个系统的可用性。
一般来说,分布式数据集的容错性有两种方式:数据检查点和记录数据的更新。
面向大规模数据分析,数据检查点操作成本很高,需要通过数据中心的网络连接在机器之间复制庞大的 数据集,而网络带宽往往比内存带宽低得多,同时还需要消耗更多的存储资源。
因此,Spark选择记录更新的方式。但是,如果更新粒度太细太多,那么记录更新成本也不低。因此,
RDD只支持粗粒度转换,即只记录单个块上执行的单个操作,然后将创建RDD的一系列变换序列(每个
RDD都包含了他是如何由其他RDD变换过来的以及如何重建某一块数据的信息。因此RDD的容错机制又 称“血统(Lineage)”容错)记录下来,以便恢复丢失的分区。
Lineage本质上很类似于数据库中的重做日志(Redo Log),只不过这个重做日志粒度很大,是对全局数据做同样的重做进而恢复数据。

2、Lineage机制Lineage简介
相比其他系统的细颗粒度的内存数据更新级别的备份或者LOG机制,RDD的Lineage记录的是粗颗粒度的 特定数据Transformation操作(如filter、map、join等)行为。当这个RDD的部分分区数据丢失时,它可 以通过Lineage获取足够的信息来重新运算和恢复丢失的数据分区。因为这种粗颗粒的数据模型,限制了Spark的运用场合,所以Spark并不适用于所有高性能要求的场景,但同时相比细颗粒度的数据模型,也 带来了性能的提升。
两种依赖关系
RDD在Lineage依赖方面分为两种:窄依赖(Narrow Dependencies)与宽依赖(Wide Dependencies,源码中称为Shuule
Dependencies),用来解决数据容错的高效性。
窄依赖是指父RDD的每一个分区最多被一个子RDD的分区所用,表现为一个父RDD的分区对应于一个子
RDD的分区
或多个父RDD的分区对应于一个子RDD的分区,也就是说一个父RDD的一个分区不可能对应一个子RDD的 多个分区。 1个父RDD分区对应1个子RDD分区,这其中又分两种情况:1个子RDD分区对应1个父RDD分区(如map、filter等算子),1个子RDD分区对应N个父RDD分区(如co-paritioned(协同划分)过的Join)。
宽依赖是指子RDD的分区依赖于父RDD的多个分区或所有分区,即存在一个父RDD的一个分区对应一个 子RDD的多个分区。 1个父RDD分区对应多个子RDD分区,这其中又分两种情况:1个父RDD对应所有子
RDD分区(未经协同划分的Join)或者1个父RDD对应非全部的多个RDD分区(如groupByKey)

Spark依赖的实现:
1 abstract class NarrowDependency[T](_rdd: RDD[T]) extends Dependency[T] {
2 //返回子RDD的partitionId依赖的所有的parent RDD的Partition(s)
3 def getParents(partitionId: Int): Seq[Int]
4 override def rdd: RDD[T] = _rdd
}
1)窄依赖是有两种具体实现,分别如下:
一种是一对一的依赖,即OneToOneDependency
1 class OneToOneDependency[T](rdd: RDD[T]) extends NarrowDependencyT {
2 override def getParents(partitionId: Int) = List(partitionId)
}
通过getParents的实现不难看出,RDD仅仅依赖于parent RDD相同ID的Partition。
还有一个是范围的依赖,即RangeDependency,它仅仅被org.apache.spark.rdd.UnionRDD使用。
UnionRDD是把多个RDD合成一个RDD,这些RDD是被拼接而成,即每个parent RDD的Partition的相对顺序不会变,只不过每个parent RDD在UnionRDD中的Partition的起始位置不同。因此它的getPartents如下
getParents(partitionId: Int) = {
2 if(partitionId >= outStart && partitionId < outStart + length) {
3 List(partitionId - outStart + inStart)
4 } else {
5 Nil
6 }
7 }

其中,inStart是parent RDD中Partition的起始位置,outStart是在UnionRDD中的起始位置,length就是parent RDD中Partition的数量。
2)宽依赖的实现
宽依赖的实现只有一种:ShuuleDependency。子RDD依赖于parent RDD的所有Partition,因此需要
Shuule过程
class ShuffleDependency[K, V, C](
@transient rdd: RDD[ <: Product2[K, V]], val partitioner: Partitioner,
val serializer: Option[Serializer] = None, val keyOrdering: Option[Ordering[K]] = None,
val aggregator: Option[Aggregator[K, V, C]] = None, val mapSideCombine: Boolean = false)
extends Dependency[Product2[K, V]] {
override def rdd = _rdd.asInstanceOf[RDD[Product2[K, V]]]
//获取新的shuffleId
val shuffleId: Int = _rdd.context.newShuffleId()
//向ShuffleManager注册Shuffle的信息val shuffleHandle: ShuffleHandle =
_rdd.context.env.shuffleManager.registerShuffle(
shuffleId, _rdd.partitions.size, this)
}

注意:宽依赖支持两种Shuule Manager。 即org.apache.spark.shuule.hash.HashShuuleManager(基于
Hash的Shuule机制)和org.apache.spark.shuule.sort.SortShuuleManager(基于排序的Shuule机制)。
本质理解:根据父RDD分区是对应1个还是多个子RDD分区来区分窄依赖(父分区对应一个子分区)和宽 依赖(父分区对应多个子分区)。如果对应多个,则当容错重算分区时,因为父分区数据只有一部分是 需要重算子分区的,其余数据重算就造成了冗余计算。
对于宽依赖,Stage计算的输入和输出在不同的节点上,对于输入节点完好,而输出节点死机的情况, 通过重新计算恢复数据这种情况下,这种方法容错是有效的,否则无效,因为无法重试,需要向上追溯 其祖先看是否可以重试(这就是lineage,血统的意思),窄依赖对于数据的重算开销要远小于宽依赖的 数据重算开销。
窄依赖和宽依赖的概念主要用在两个地方:一个是容错中相当于Redo日志的功能;另一个是在调度中构 建DAG作为不同Stage的划分点。
依赖关系的特性
第一,窄依赖可以在某个计算节点上直接通过计算父RDD的某块数据计算得到子RDD对应的某块数据; 宽依赖则要等到父RDD所有数据都计算完成之后,并且父RDD的计算结果进行hash并传到对应节点上之 后才能计算子RDD。
第二,数据丢失时,对于窄依赖只需要重新计算丢失的那一块数据来恢复;对于宽依赖则要将祖先RDD 中的所有数据块全部重新计算来恢复。所以在长“血统”链特别是有宽依赖的时候,需要在适当的时机设 置数据检查点。也是这两个特性要求对于不同依赖关系要采取不同的任务调度机制和容错恢复机制。
容错原理
在容错机制中,如果一个节点死机了,而且运算窄依赖,则只要把丢失的父RDD分区重算即可,不依赖 于其他节点。而宽依赖需要父RDD的所有分区都存在,重算就很昂贵了。可以这样理解开销的经济与
否:在窄依赖中,在子RDD的分区丢失、重算父RDD分区时,父RDD相应分区的所有数据都是子RDD分区 的数据,并不存在冗余计算。在宽依赖情况下,丢失一个子RDD分区重算的每个父RDD的每个分区的所

有数据并不是都给丢失的子RDD分区用的,会有一部分数据相当于对应的是未丢失的子RDD分区中需要 的数据,这样就会产生冗余计算开销,这也是宽依赖开销更大的原因。
3、Checkpoint机制
checkpoint就是把内存中的变化刷新到持久存储,斩断依赖链在存储中 checkpoint 是一个很常见的概念, 举几个例子:
数据库 checkpoint 过程中一般把内存中的变化进行持久化到物理页, 这时候就可以斩断依赖链, 就可以把 redo 日志删掉了, 然后更新下检查点。
hdfs namenode 的元数据 editlog, Secondary namenode 会把 edit log 应用到 fsimage, 然后刷到磁盘上, 也相当于做了一次 checkpoint, 就可以把老的 edit log 删除了。
spark streaming 中对于一些 有状态的操作, 这在某些 stateful 转换中是需要的,在这种转换中,生成 RDD 需要依赖前面的 batches,会导致依赖链随着时间而变长。为了避免这种没有尽头的变长, 要定期将中间生成的 RDDs 保存到可靠存储来切断依赖链, 必须隔一段时间进行一次进行一次
checkpoint。
cache 和 checkpoint 是有显著区别的, 缓存把 RDD 计算出来然后放在内存中, 但是RDD 的依赖链(相当于数据库中的redo 日志), 也不能丢掉, 当某个点某个 executor 宕了, 上面cache 的RDD就会丢
掉, 需要通过 依赖链重放计算出来, 不同的是, checkpoint 是把 RDD 保存在 HDFS中, 是多副本可靠存储,所以依赖链就可以丢掉了,就斩断了依赖链, 是通过复制实现的高容错。但是有一点要注意, 因为checkpoint是需要把 job 重新从头算一遍, 最好先cache一下, checkpoint就可以直接保存缓存中的
RDD 了, 就不需要重头计算一遍了, 对性能有极大的提升。
checkpoint 的正确使用姿势
1 val data = sc.textFile(“/tmp/spark/1.data”).cache() // 注意要cache
2 sc.setCheckpointDir(“/tmp/spark/checkpoint”)
3 data.checkpoint
4 data.count

使用很简单,就是设置一下 checkpoint 目录,然后再rdd上调用 checkpoint 方法,action 的时候就对数据进行了 checkpoint。
checkpoint写流程
RDD checkpoint 过程中会经过以下几个状态
Initialized –> marked for checkpointing –> checkpointing in progress –> checkpointed
我们看下状态转换流程
首先 driver program 需要使用 rdd.checkpoint() 去设定哪些 rdd 需要 checkpoint,设定后,该 rdd 就接受 RDDCheckpointData 管理。用户还要设定 checkpoint 的存储路径,一般在 HDFS 上。
marked for checkpointing:初始化后,RDDCheckpointData 会将 rdd 标记为 MarkedForCheckpoint。checkpointing in progress:每个 job 运行结束后会调用 finalRdd.doCheckpoint(),finalRdd 会顺着computing chain 回溯扫描,碰到要 checkpoint 的 RDD 就将其标记为 CheckpointingInProgress,然后将写磁盘(比如写 HDFS)需要的配置文件(如 core-site.xml 等)broadcast 到其他 worker 节点上的 blockManager。完成以后,启动一个 job 来完成 checkpoint(使用 rdd.context.runJob(rdd,
CheckpointRDD.writeToFile(path.toString, broadcastedConf)))。
checkpointed:job 完成 checkpoint 后,将该 rdd 的 dependency 全部清掉,并设定该 rdd 状态为
checkpointed。然后,为该 rdd 强加一个依赖,设置该 rdd 的 parent rdd 为 CheckpointRDD,该
CheckpointRDD 负责以后读取在文件系统上的 checkpoint 文件,生成该 rdd 的 partition。
checkpoint读流程

如果一个RDD 我们已经 checkpoint了那么是什么时候用呢,checkpoint 将 RDD 持久化到 HDFS 或本地文件夹,如果不被手动 remove 掉,是一直存在的,也就是说可以被下一个 driver program 使用。 比如spark streaming 挂掉了,重启后就可以使用之前 checkpoint 的数据进行 recover (这个流程我们在下面一篇文章会讲到) , 当然在同一个 driver program 也可以使用。 我们讲下在同一个 driver program 中是怎么使用 checkpoint 数据的。
如果 一个 RDD 被checkpoint了,如果这个 RDD 上有 action 操作时候,或者回溯的这个 RDD 的时候,这个
RDD 进行计算的时候,里面判断如果已经 checkpoint 过, 对分区和依赖的处理都是使用的 RDD 内部的
checkpointRDD 变量。
具体细节如下:
如果一个RDD被checkpoint了,那么这个 RDD 中对分区和依赖的处理都是使用的RDD内部的
checkpointRDD变量,具体实现是 ReliableCheckpointRDD 类型。这个是在 checkpoint 写流程中创建的。依赖和获取分区方法中先判断是否已经checkpoint,如果已经checkpoint了,就斩断依赖,使用ReliableCheckpointRDD,来处理依赖和获取分区。
如果没有,才往前回溯依赖。依赖就是没有依赖,因为已经斩断了依赖,获取分区数据就是读取
checkpoint 到 hdfs目录中不同分区保存下来的文件。
整个 checkpoint 读流程就完了。
在以下两种情况下,RDD需要加检查点。
DAG中的Lineage过长,如果重算,则开销太大(如在PageRank中)。 在宽依赖上做Checkpoint获得的收益更大。
由于RDD是只读的,所以Spark的RDD计算中一致性不是主要关心的内容,内存相对容易管理,这也是设 计者很有远见的地方,这样减少了框架的复杂性,提升了性能和可扩展性,为以后上层框架的丰富奠定 了强有力的基础。
在RDD计算中,通过检查点机制进行容错,传统做检查点有两种方式:通过冗余数据和日志记录更新操 作。在RDD中的doCheckPoint方法相当于通过冗余数据来缓存数据,而之前介绍的血统就是通过相当粗 粒度的记录更新操作来实现容错的。
检查点(本质是通过将RDD写入Disk做检查点)是为了通过lineage做容错的辅助,lineage过长会造成容 错成本过高,这样就不如在中间阶段做检查点容错,如果之后有节点出现问题而丢失分区,从做检查点 的RDD开始重做Lineage,就会减少开销。

52 . 简述RDD的容错机制 ?

试题回答参考思路:

1、Lineage机制(简称血统机制)
RDD的Lineage机制是将粗颗粒度相同的操作(map flatmap filter之类的)记录下来,万一RDD信息丢失可通过Lineage获取信息从新获得原来丢失的RDD
缺点:lineage如果过长 影响性能,开销太大
2、checkpoint机制
将RDD的结果存入HDFS,以文件的形式存在
3、cache机制
将结果存入内存
4、persist机制
将 结 果 存 入 磁 盘 checkpoint 和 cache 和 persist的异同相同点:都要在action算子之后触发不同点:
checkpoint:
是存在HDFS上的,更加安全可靠,并且checkpoint会改变依赖关系,并且之后会触发一个job操作
persist和cache
不改变依赖关系,运行完成后缓存数据自动消失

53 . 简述Executor如何内存分配 ?

试题回答参考思路:

在执行Spark 的应用程序时,Spark 集群会启动 Driver 和 Executor 两种 JVM 进程,前者为主控进程,负责创建 Spark 上下文,提交 Spark 作业(Job),并将作业转化为计算任务(Task),在各个 Executor 进程间协调任务的调度,后者负责在工作节点上执行具体的计算任务,并将结果返回给 Driver,同时为需要持久化的 RDD 提供存储功能。下方内容中的 Spark 内存均特指 Executor 的内存。
1、堆内和堆外内存规划
作为一个 JVM 进程,Executor 的内存管理建立在 JVM 的内存管理之上,Spark 对 JVM 的堆内(On-
heap)空间进行了更为详细的分配,以充分利用内存。同时,Spark 引入了堆外(Ou-heap)内存,使之可以直接在工作节点的系统内存中开辟空间,进一步优化了内存的使用。
堆内内存受到JVM统一管理,堆外内存是直接向操作系统进行内存的申请和释放

1)堆内内存
堆内内存的大小,由 Spark 应用程序启动时的 –executor-memory 或 spark.executor.memory 参数配置。
Executor 内运行的并发任务共享 JVM 堆内内存,这些任务在缓存 RDD 数据和广播(Broadcast)数据时占用的内存被规划为存储(Storage)内存,而这些任务在执行 Shuule 时占用的内存被规划为执行
(Execution)内存,剩余的部分不做特殊规划,那些 Spark 内部的对象实例,或者用户定义的 Spark 应用程序中的对象实例,均占用剩余的空间。不同的管理模式下,这三部分占用的空间大小各不相同。
Spark 对堆内内存的管理是一种逻辑上的”规划式”的管理,因为对象实例占用内存的申请和释放都由
JVM 完成,Spark 只能在申请后和释放前记录这些内存,我们来看其具体流程:
申请内存流程如下:
Spark 在代码中 new 一个对象实例;
JVM 从堆内内存分配空间,创建对象并返回对象引用;
Spark 保存该对象的引用,记录该对象占用的内存。
释放内存流程如下:
Spark记录该对象释放的内存,删除该对象的引用;
等待JVM的垃圾回收机制释放该对象占用的堆内内存。
我们知道,JVM 的对象可以以序列化的方式存储,序列化的过程是将对象转换为二进制字节流,本质上可以理解为将非连续空间的链式存储转化为连续空间或块存储,在访问时则需要进行序列化的逆过程
——反序列化,将字节流转化为对象,序列化的方式可以节省存储空间,但增加了存储和读取时候的计 算开销。
对于 Spark 中序列化的对象,由于是字节流的形式,其占用的内存大小可直接计算,而对于非序列化的对象,其占用的内存是通过周期性地采样近似估算而得,即并不是每次新增的数据项都会计算一次占用 的内存大小,这种方法降低了时间开销但是有可能误差较大,导致某一时刻的实际内存有可能远远超出 预期。此外,在被 Spark 标记为释放的对象实例,很有可能在实际上并没有被 JVM 回收,导致实际可用的内存小于 Spark 记录的可用内存。所以 Spark 并不能准确记录实际可用的堆内内存,从而也就无法完全避免内存溢出(OOM, Out of Memory)的异常。
虽然不能精准控制堆内内存的申请和释放,但 Spark 通过对存储内存和执行内存各自独立的规划管理, 可以决定是否要在存储内存里缓存新的 RDD,以及是否为新的任务分配执行内存,在一定程度上可以提升内存的利用率,减少异常的出现。

2)堆外内存
为了进一步优化内存的使用以及提高 Shuule 时排序的效率,Spark 引入了堆外(Ou-heap)内存,使之可以直接在工作节点的系统内存中开辟空间,存储经过序列化的二进制数据。
堆外内存意味着把内存对象分配在Java虚拟机的堆以外的内存,这些内存直接受操作系统管理(而不是 虚拟机)。这样做的结果就是能保持一个较小的堆,以减少垃圾收集对应用的影响。
利用 JDK Unsafe API(从 Spark 2.0 开始,在管理堆外的存储内存时不再基于 Tachyon,而是与堆外的执行内存一样,基于 JDK Unsafe API 实现),Spark 可以直接操作系统堆外内存,减少了不必要的内存开销,以及频繁的 GC 扫描和回收,提升了处理性能。堆外内存可以被精确地申请和释放(堆外内存之所以能够被精确的申请和释放,是由于内存的申请和释放不再通过JVM机制,而是直接向操作系统申请,
JVM对于内存的清理是无法准确指定时间点的,因此无法实现精确的释放),而且序列化的数据占用的 空间可以被精确计算,所以相比堆内内存来说降低了管理的难度,也降低了误差。
在默认情况下堆外内存并不启用,可通过配置 spark.memory.ouHeap.enabled 参数启用,并由
spark.memory.ouHeap.size 参数设定堆外空间的大小。除了没有 other 空间,堆外内存与堆内内存的划分方式相同,所有运行中的并发任务共享存储内存和执行内存。

2、内存空间分配
1)静态内存管理

在 Spark 最初采用的静态内存管理机制下,存储内存、执行内存和其他内存的大小在 Spark 应用程序运行期间均为固定的,但用户可以应用程序启动前进行配置
systemMaxMemory 取决于当前 JVM 堆内内存的大小,最后可用的执行内存或者存储内存要在此基础上与各自的 memoryFraction 参数和 safetyFraction 参数相乘得出。上述计算公式中的两个
safetyFraction 参数,其意义在于在逻辑上预留出 1-safetyFraction 这么一块保险区域,降低因实际内存超出当前预设范围而导致 OOM 的风险(上文提到,对于非序列化对象的内存采样估算会产生误差)。值得注意的是,这个预留的保险区域仅仅是一种逻辑上的规划,在具体使用时 Spark 并没有区别对待, 和”其它内存”一样交给了 JVM 去管理。
Storage内存和Execution内存都有预留空间,目的是防止OOM,因为Spark堆内内存大小的记录是不准确 的,需要留出保险区域。
堆外的空间分配较为简单,只有存储内存和执行内存,如下入所示。可用的执行内存和存储内存占用的 空间大小直接由参数spark.memory.storageFraction 决定,由于堆外内存占用的空间可以被精确计算,所以无需再设定保险区域。

静态内存管理机制实现起来较为简单,但如果用户不熟悉 Spark 的存储机制,或没有根据具体的数据规模和计算任务或做相应的配置,很容易造成”一半海水,一半火焰”的局面,即存储内存和执行内存中的 一方剩余大量的空间,而另一方却早早被占满,不得不淘汰或移出旧的内容以存储新的内容。由于新的 内存管理机制的出现,这种方式目前已经很少有开发者使用,出于兼容旧版本的应用程序的目的,
Spark 仍然保留了它的实现。

2)统一内存管理
Spark 1.6 之后引入的统一内存管理机制,与静态内存管理的区别在于存储内存和执行内存共享同一块空间,可以动态占用对方的空闲区域
其中最重要的优化在于动态占用机制,其规则如下:
设定基本的存储内存和执行内存区域(spark.storage.storageFraction 参数),该设定确定了双方各自拥有的空间的范围;
双方的空间都不足时,则存储到硬盘;若己方空间不足而对方空余时,可借用对方的空间;(存储 空间不足是指不足以放下一个完整的 Block)
执行内存的空间被对方占用后,可让对方将占用的部分转存到硬盘,然后”归还”借用的空间;
存储内存的空间被对方占用后,无法让对方”归还”,因为需要考虑 Shuule 过程中的很多因素,实现起来较为复杂。

凭借统一内存管理机制,Spark 在一定程度上提高了堆内和堆外内存资源的利用率,降低了开发者维护
Spark 内存的难度,但并不意味着开发者可以高枕无忧。如果存储内存的空间太大或者说缓存的数据过多,反而会导致频繁的全量垃圾回收,降低任务执行时的性能,因为缓存的 RDD 数据通常都是长期驻留内存的。所以要想充分发挥 Spark 的性能,需要开发者进一步了解存储内存和执行内存各自的管理方式和实现原理。

54 . 简述Spark的batchsize,怎么解决小文件合并问题 ?

试题回答参考思路:

1、相关问题描述
当我们使用spark sql执行etl时候出现了,可能最终结果大小只有几百k,但是小文件一个分区有上千的情况。
这样就会导致以下的一些危害:
hdfs有最大文件数限制;
浪费磁盘资源(可能存在空文件);
hive中进行统计,计算的时候,会产生很多个map,影响计算的速度。

2、解决方案
1) 方法一:通过spark的coalesce()方法和repartition()方法
1 val rdd2 = rdd1.coalesce(8, true) // true表示是否shuffle
2 val rdd3 = rdd1.repartition(8)
coalesce:coalesce()方法的作用是返回指定一个新的指定分区的Rdd,如果是生成一个窄依赖的结果, 那么可以不发生shuule,分区的数量发生激烈的变化,计算节点不足,不设置true可能会出错。
repartition:coalesce()方法shuule为true的情况。
2)方法二:降低spark并行度,即调节spark.sql.shuGle.partitions
比如之前设置的为100,按理说应该生成的文件数为100;但是由于业务比较特殊,采用的大量的union all,且union all在spark中属于窄依赖,不会进行shuule,所以导致最终会生成(union all数量+1)*100 的文件数。如有10个union all,会生成1100个小文件。这样导致降低并行度为10之后,执行时长大大增加,且文件数依旧有110个,效果有,但是不理想。

3)方法三:新增一个并行度=1任务,专门合并小文件。
先将原来的任务数据写到一个临时分区(如tmp);再起一个并行度为1的任务,类似:
insert overwrite 目标表 select * from 临时分区
但是结果小文件数还是没有减少,原因:‘select * from 临时分区’ 这个任务在spark中属于窄依赖;并且
spark DAG中分为宽依赖和窄依赖,只有宽依赖会进行shuule;故并行度shuule,
spark.sql.shuule.partitions=1也就没有起到作用;由于数据量本身不是特别大,所以可以直接采用group
by(在spark中属于宽依赖)的方式,类似:
insert overwrite 目标表 select * from 临时分区 group by *
先运行原任务,写到tmp分区,‘dfs -count’查看文件数,1100个,运行加上group by的临时任务
(spark.sql.shuule.partitions=1),查看结果目录,文件数=1,成功。
最后又加了个删除tmp分区的任务。
3、总结
1)方便的话,可以采用coalesce()方法和repartition()方法。2)如果任务逻辑简单,数据量少,可以直接降低并行度。
3)任务逻辑复杂,数据量很大,原任务大并行度计算写到临时分区,再加两个任务:一个用来将临时 分区的文件用小并行度(加宽依赖)合并成少量文件到实际分区;另一个删除临时分区。
4)hive任务减少小文件相对比较简单,可以直接设置参数,如: Map-only的任务结束时合并小文件:
sethive.merge.mapfiles = true
在Map-Reduce的任务结束时合并小文件:
sethive.merge.mapredfiles= true
当输出文件的平均大小小于1GB时,启动一个独立的map-reduce任务进行文件merge:
sethive.merge.smallfiles.avgsize=1024000000

55 . 简述Spark参数(性能)调优 ?

试题回答参考思路:

1、常规性能调优

1)常规性能调优一:最优资源配置
Spark性能调优的第一步,就是为任务分配更多的资源,在一定范围内,增加资源的分配与性能的提升 是成正比的,实现了最优的资源配置后,在此基础上再考虑进行后面论述的性能调优策略。
资源的分配在使用脚本提交Spark任务时进行指定,标准的Spark任务提交脚本如以下代码所示
1 /usr/opt/modules/spark/bin/spark-submit
2 --class com.atguigu.spark.Analysis
3 --num-executors 80
4 --driver-memory 6g
5 --executor-memory 6g
6 --executor-cores 3
7 /usr/opt/modules/spark/jar/spark.jar \

可以进行分配的资源如下表所示:

名称 说明
–num-executors 配置Executor的数量
–driver-memory 配置Driver内存(影响不大)
–executor-memory 配置每个Executor的内存大小
–executor-cores 配置每个Executor的CPU core数量
调节原则:尽量将任务分配的资源调节到可以使用的资源的最大限度。对于具体资源的分配,我们分别讨论Spark的两种Cluster运行模式:
第一种是Spark Standalone模式,你在提交任务前,一定知道或者可以从运维部门获取到你可以使用的资源情况,在编写submit脚本的时候,就根据可用的资源情况进行资源的分配,比如说集群有
15台机器,每台机器为8G内存,2个CPU core,那么就指定15个Executor,每个Executor分配8G内存,2个CPU core。
第二种是Spark Yarn模式,由于Yarn使用资源队列进行资源的分配和调度,在表写submit脚本的时候,就根据Spark作业要提交到的资源队列,进行资源的分配,比如资源队列有400G内存,100个CPU core,那么指定50个Executor,每个Executor分配8G内存,2个CPU core。

2)常规性能调优二:RDD优化
(1) RDD复用
在对RDD进行算子时,要避免相同的算子和计算逻辑之下对RDD进行重复的计算

(2) RDD持久化
在Spark中,当多次对同一个RDD执行算子操作时,每一次都会对这个RDD以之前的父RDD重新计算一次 ,这种情况是必须要避免的,对同一个RDD的重复计算是对资源的极大浪费,因此,必须对多次使用的RDD进行持久化,通过持久化将公共RDD的数据缓存到内存/磁盘中,之后对于公共RDD的计算都会从内 存/磁盘中直接获取RDD数据。
对于RDD的持久化,有两点需要说明:
RDD的持久化是可以进行序列化的,当内存无法将RDD的数据完整的进行存放的时候,可以考虑使 用序列化的方式减小数据体积,将数据完整存储在内存中。
如果对于数据的可靠性要求很高,并且内存充足,可以使用副本机制,对RDD数据进行持久化。当 持久化启用了复本机制时,对于持久化的每个数据单元都存储一个副本,放在其他节点上面,由此 实现数据的容错,一旦一个副本数据丢失,不需要重新计算,还可以使用另外一个副本。
(3) RDD尽可能早的filter操作
获取到初始RDD后,应该考虑尽早地过滤掉不需要的数据,进而减少对内存的占用,从而提升Spark作业 的运行效率。

3)常规性能调优三:并行度调节
Spark作业中的并行度指各个stage的task的数量。
如果并行度设置不合理而导致并行度过低,会导致资源的极大浪费,例如,20个Executor,每个Executor 分配3个CPU core,而Spark作业有40个task,这样每个Executor分配到的task个数是2个,这就使得每个
Executor有一个CPU core空闲,导致资源的浪费。
理想的并行度设置,应该是让并行度与资源相匹配,简单来说就是在资源允许的前提下,并行度要设置 的尽可能大,达到可以充分利用集群资源。合理的设置并行度,可以提升整个Spark作业的性能和运行 速度。
Spark官方推荐,task数量应该设置为Spark作业总CPU core数量的2~3倍。之所以没有推荐task数量与CPU core总数相等,是因为task的执行时间不同,有的task执行速度快而有的task执行速度慢,如果task 数量与CPU core总数相等,那么执行快的task执行完成后,会出现CPU core空闲的情况。如果task数量设置为CPU core总数的2~3倍,那么一个task执行完毕后,CPU core会立刻执行下一个task,降低了资源的浪费,同时提升了Spark作业运行的效率。

4)常规性能调优四:广播大变量
默认情况下,task中的算子中如果使用了外部的变量,每个task都会获取一份变量的复本,这就造成了 内存的极大消耗。一方面,如果后续对RDD进行持久化,可能就无法将RDD数据存入内存,只能写入磁 盘,磁盘IO将会严重消耗性能;另一方面,task在创建对象的时候,也许会发现堆内存无法存放新创建 的对象,这就会导致频繁的GC,GC会导致工作线程停止,进而导致Spark暂停工作一段时间,严重影响
Spark性能。
假设当前任务配置了20个Executor,指定500个task,有一个20M的变量被所有task共用,此时会在500个
task中产生500个副本,耗费集群10G的内存,如果使用了广播变量, 那么每个Executor保存一个副本, 一共消耗400M内存,内存消耗减少了5倍。
广播变量在每个Executor保存一个副本,此Executor的所有task共用此广播变量,这让变量产生的副本数 量大大减少。
在初始阶段,广播变量只在Driver中有一份副本。task在运行的时候,想要使用广播变量中的数据,此时 首先会在自己本地的Executor对应的BlockManager中尝试获取变量,如果本地没有,BlockManager就会 从Driver或者其他节点的BlockManager上远程拉取变量的复本,并由本地的BlockManager进行管理;之 后此Executor的所有task都会直接从本地的BlockManager中获取变量。
5)常规性能调优五:Kryo序列化
默认情况下,Spark使用Java的序列化机制。Java的序列化机制使用方便,不需要额外的配置,在算子中 使用的变量实现Serializable接口即可,但是,Java序列化机制的效率不高,序列化速度慢并且序列化后 的数据所占用的空间依然较大。
Kryo序列化机制比Java序列化机制性能提高10倍左右,Spark之所以没有默认使用Kryo作为序列化类库, 是因为它不支持所有对象的序列化,同时Kryo需要用户在使用前注册需要序列化的类型,不够方便,但 从Spark 2.0.0版本开始,简单类型、简单类型数组、字符串类型的ShuGling RDDs 已经默认使用Kryo序列化方式了。

6)常规性能调优六:调节本地化等待时长
Spark作业运行过程中,Driver会对每一个stage的task进行分配。根据Spark的task分配算法,Spark希望 task能够运行在它要计算的数据算在的节点(数据本地化思想),这样就可以避免数据的网络传输。通 常来说,task可能不会被分配到它处理的数据所在的节点,因为这些节点可用的资源可能已经用尽,此 时,Spark会等待一段时间,默认3s,如果等待指定时间后仍然无法在指定节点运行,那么会自动降
级,尝试将task分配到比较差的本地化级别所对应的节点上,比如将task分配到离它要计算的数据比较 近的一个节点,然后进行计算,如果当前级别仍然不行,那么继续降级。
当task要处理的数据不在task所在节点上时,会发生数据的传输。task会通过所在节点的BlockManager获 取数据,BlockManager发现数据不在本地时,户通过网络传输组件从数据所在节点的BlockManager处获 取数据。
网络传输数据的情况是我们不愿意看到的,大量的网络传输会严重影响性能,因此,我们希望通过调节 本地化等待时长,如果在等待时长这段时间内,目标节点处理完成了一部分task,那么当前的task将有 机会得到执行,这样就能够改善Spark作业的整体性能。

在Spark项目开发阶段,可以使用client模式对程序进行测试,此时,可以在本地看到比较全的日志信息,日志信息中有明确的task数据本地化的级别,如果大部分都是PROCESS_LOCAL,那么就无需进行调 节,但是如果发现很多的级别都是NODE_LOCAL、ANY,那么需要对本地化的等待时长进行调节,通过 延长本地化等待时长,看看task的本地化级别有没有提升,并观察Spark作业的运行时间有没有缩短。
注意,过犹不及,不要将本地化等待时长延长地过长,导致因为大量的等待时长,使得Spark作业的运 行时间反而增加了。
Spark本地化等待时长的设置如代码所示
val conf = new SparkConf().set(“spark.locality.wait”, “6”)

4、ShuGle调优
1) ShuGle调优一:调节map端缓冲区大小
在Spark任务运行过程中,如果shuule的map端处理的数据量比较大,但是map端缓冲的大小是固定的, 可能会出现map端缓冲数据频繁spill溢写到磁盘文件中的情况,使得性能非常低下,通过调节map端缓 冲的大小,可以避免频繁的磁盘IO操作,进而提升Spark任务的整体性能。

map端缓冲的默认配置是32KB,如果每个task处理640KB的数据,那么会发生640/32 = 20次溢写,如果每个task处理64000KB的数据,机会发生64000/32=2000此溢写,这对于性能的影响是非常严重的。
map端缓冲的配置方法如代码清单所示:
val conf = new SparkConf().set(“spark.shuffle.file.buffer”, “64”)

2) ShuGle调优二:调节reduce端拉取数据缓冲区大小
Spark Shuule过程中,shuule reduce task的buuer缓冲区大小决定了reduce task每次能够缓冲的数据量, 也就是每次能够拉取的数据量,如果内存资源较为充足,适当增加拉取数据缓冲区的大小,可以减少拉 取数据的次数,也就可以减少网络传输的次数,进而提升性能。
reduce端数据拉取缓冲区的大小可以通过spark.reducer.maxSizeInFlight参数进行设置,默认为48MB,该 参数的设置方法如代码清单所示:
val conf = new SparkConf().set(“spark.reducer.maxSizeInFlight”, “96”)

3) ShuGle调优三:调节reduce端拉取数据重试次数
Spark Shuule过程中,reduce task拉取属于自己的数据时,如果因为网络异常等原因导致失败会自动进行重试。对于那些包含了特别耗时的shuule操作的作业,建议增加重试最大次数(比如60次),以避免 由于JVM的full gc或者网络不稳定等因素导致的数据拉取失败。在实践中发现,对于针对超大数据量(数十亿~上百亿)的shuule过程,调节该参数可以大幅度提升稳定性。
reduce端拉取数据重试次数可以通过spark.shuule.io.maxRetries参数进行设置,该参数就代表了可以重试 的最大次数。如果在指定次数之内拉取还是没有成功,就可能会导致作业执行失败,默认为3,该参数 的设置方法如代码清单所示:
val conf = new SparkConf().set(“spark.shuffle.io.maxRetries”, “6”)

4) ShuGle调优四:调节reduce端拉取数据等待间隔
Spark Shuule过程中,reduce task拉取属于自己的数据时,如果因为网络异常等原因导致失败会自动进行重试,在一次失败后,会等待一定的时间间隔再进行重试,可以通过加大间隔时长(比如60s),以 增加shuule操作的稳定性。
reduce端拉取数据等待间隔可以通过spark.shuule.io.retryWait参数进行设置,默认值为5s,该参数的设 置方法如代码清单所示:
val conf = new SparkConf().set(“spark.shuffle.io.retryWait”, “60s”)

5) ShuGle调优五:调节SortShuGle排序操作阈值
对于SortShuuleManager,如果shuule reduce task的数量小于某一阈值则shuule write过程中不会进行排序操作,而是直接按照未经优化的HashShuuleManager的方式去写数据,但是最后会将每个task产生的 所有临时磁盘文件都合并成一个文件,并会创建单独的索引文件。
当你使用SortShuuleManager时,如果的确不需要排序操作,那么建议将这个参数调大一些,大于shuule read task的数量,那么此时map-side就不会进行排序了,减少了排序的性能开销,但是这种方式下,依然会产生大量的磁盘文件,因此shuule write性能有待提高。
SortShuuleManager排序操作阈值的设置可以通过spark.shuule.sort. bypassMergeThreshold这一参数进行设置,默认值为200,该参数的设置方法如代码清单所示:
val conf = new SparkConf().set(“spark.shuffle.sort.bypassMergeThreshold”, “400”)

4、JVM调优
对于JVM调优,首先应该明确,full gc/minor gc,都会导致JVM的工作线程停止工作,即stop the world。
1) JVM调优一:降低cache操作的内存占比
(1)静态内存管理机制
根据Spark静态内存管理机制,堆内存被划分为了两块,Storage和Execution。Storage主要用于缓存RDD 数据和broadcast数据,Execution主要用于缓存在shuule过程中产生的中间数据,Storage占系统内存的60%,Execution占系统内存的20%,并且两者完全独立。
在一般情况下,Storage的内存都提供给了cache操作,但是如果在某些情况下cache操作内存不是很紧 张,而task的算子中创建的对象很多,Execution内存又相对较小,这回导致频繁的minor gc,甚至于频繁的full gc,进而导致Spark频繁的停止工作,性能影响会很大。
在Spark UI中可以查看每个stage的运行情况,包括每个task的运行时间、gc时间等等,如果发现gc太频繁,时间太长,就可以考虑调节Storage的内存占比,让task执行算子函数式,有更多的内存可以使用。
Storage内存区域可以通过spark.storage.memoryFraction参数进行指定,默认为0.6,即60%,可以逐级向 下递减,如代码清单所示:
val conf = new SparkConf().set(“spark.storage.memoryFraction”, “0.4”)

(2)统一内存管理机制
根据Spark统一内存管理机制,堆内存被划分为了两块,Storage和Execution。Storage主要用于缓存数据,Execution主要用于缓存在shuule过程中产生的中间数据,两者所组成的内存部分称为统一内存, Storage和Execution各占统一内存的50%,由于动态占用机制的实现,shuule过程需要的内存过大时,会 自动占用Storage的内存区域,因此无需手动进行调节。
2) JVM调优二:调节Executor堆外内存
Executor的堆外内存主要用于程序的共享库、Perm Space、 线程Stack和一些Memory mapping等, 或者类
C方式allocate object。
有时,如果你的Spark作业处理的数据量非常大,达到几亿的数据量,此时运行Spark作业会时不时地报 错,例如shuule output file cannot find,executor lost,task lost,out of memory等,这可能是Executor 的堆外内存不太够用,导致Executor在运行的过程中内存溢出。
stage的task在运行的时候,可能要从一些Executor中去拉取shuule map output文件,但是Executor可能已经由于内存溢出挂掉了,其关联的BlockManager也没有了,这就可能会报出shuule output file cannot find,executor lost,task lost,out of memory等错误,此时,就可以考虑调节一下Executor的堆外内
存,也就可以避免报错,与此同时,堆外内存调节的比较大的时候,对于性能来讲,也会带来一定的提 升。
默认情况下,Executor堆外内存上限大概为300多MB,在实际的生产环境下,对海量数据进行处理的时 候,这里都会出现问题,导致Spark作业反复崩溃,无法运行,此时就会去调节这个参数,到至少1G, 甚至于2G、4G。
Executor堆外内存的配置需要在spark-submit脚本里配置,如代码清单所示:
–conf spark.yarn.executor.memoryOverhead=2048
以上参数配置完成后,会避免掉某些JVM OOM的异常问题,同时,可以提升整体Spark作业的性能。

3) JVM调优三:调节连接等待时长
在Spark作业运行过程中,Executor优先从自己本地关联的BlockManager中获取某份数据,如果本地
BlockManager没有的话,会通过TransferService远程连接其他节点上Executor的BlockManager来获取数 据。
如果task在运行过程中创建大量对象或者创建的对象较大,会占用大量的内存,这回导致频繁的垃圾回 收,但是垃圾回收会导致工作现场全部停止,也就是说,垃圾回收一旦执行,Spark的Executor进程就会 停止工作,无法提供相应,此时,由于没有响应,无法建立网络连接,会导致网络连接超时。
在生产环境下,有时会遇到file not found、file lost这类错误,在这种情况下,很有可能是Executor的
BlockManager在拉取数据的时候,无法建立连接,然后超过默认的连接等待时长60s后,宣告数据拉取 失败,如果反复尝试都拉取不到数据,可能会导致Spark作业的崩溃。这种情况也可能会导致DAGScheduler反复提交几次stage,TaskScheduler返回提交几次task,大大延长了我们的Spark作业的运 行时间。
此时,可以考虑调节连接的超时时长,连接等待时长需要在spark-submit脚本中进行设置,设置方式如 代码清单所示:
–conf spark.core.connection.ack.wait.timeout=300 2
调节连接等待时长后,通常可以避免部分的XX文件拉取失败、XX文件lost等报错

56 . 简述Spark怎么基于内存计算的 ?

试题回答参考思路:

主要了是基于RDD

57 . 简述什么是RDD(对RDD的理解)?RDD有哪些特点?说下知道的RDD算子 ?

试题回答参考思路:

RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,是 Spark 中最基本的数据处理模型。代码中是一个抽象类,它代表一个弹性的、不可变、可分区、里面的元素可并行计算的集合。
RDD特点
RDD表示只读的分区的数据集,对RDD进行改动,只能通过RDD的转换操作,由一个RDD得到一个新的RDD,新的RDD包含了从其他RDD衍生所必需的信息。RDDs之间存在依赖,RDD的执行是按照血缘关系 延时计算的。如果血缘关系较长,可以通过持久化RDD来切断血缘关系。
1、分区
RDD逻辑上是分区的,每个分区的数据是抽象存在的,计算的时候会通过一个compute函数得到每个分 区的数据。如果RDD是通过已有的文件系统构建,则compute函数是读取指定文件系统中的数据,如果RDD是通过其他RDD转换而来,则compute函数是执行转换逻辑将其他RDD的数据进行转换。

2、只读
RDD是只读的,要想改变RDD中的数据,只能在现有的RDD基础上创建新的RDD。
由一个RDD转换到另一个RDD,可以通过丰富的操作算子实现,不再像MapReduce那样只能写map和
reduce了
RDD的操作算子包括两类,一类叫做transformations,它是用来将RDD进行转化,构建RDD的血缘关系; 另一类叫做actions,它是用来触发RDD的计算,得到RDD的相关计算结果或者将RDD保存的文件系统
中。

3、依赖
RDDs通过操作算子进行转换,转换得到的新RDD包含了从其他RDDs衍生所必需的信息,RDDs之间维护 着这种血缘关系,也称之为依赖。如下图所示,依赖包括两种,一种是窄依赖,RDDs之间分区是一一对 应的,另一种是宽依赖,下游RDD的每个分区与上游RDD(也称之为父RDD)的每个分区都有关,是多对多 的关系。

4、缓存
如果在应用程序中多次使用同一个RDD,可以将该RDD缓存起来,该RDD只有在第一次计算的时候会根 据血缘关系得到分区的数据,在后续其他地方用到该RDD的时候,会直接从缓存处取而不用再根据血缘 关系计算,这样就加速后期的重用。如下图所示,RDD-1经过一系列的转换后得到RDD-n并保存到hdfs, RDD-1在这一过程中会有个中间结果,如果将其缓存到内存,那么在随后的RDD-1转换到RDD-m这一过程 中,就不会计算其之前的RDD-0了。

5、CheckPoint
虽然RDD的血缘关系天然地可以实现容错,当RDD的某个分区数据失败或丢失,可以通过血缘关系重
建。但是对于长时间迭代型应用来说,随着迭代的进行,RDDs之间的血缘关系会越来越长,一旦在后续 迭代过程中出错,则需要通过非常长的血缘关系去重建,势必影响性能。为此,RDD支持checkpoint将 数据保存到持久化的存储中,这样就可以切断之前的血缘关系,因为checkpoint后的RDD不需要知道它 的父RDDs了,它可以从checkpoint处拿到数据。
RDD算子
RDD整体上分为Value类型和Key-Value类型
比如map、flatmap等这些,回答几个,讲一下原理就差不多了
map:遍历RDD,将函数f应用于每一个元素,返回新的RDD(transformation算子)。
foreach:遍历RDD,将函数f应用于每一个元素,无返回值(action算子)。 mapPartitions:遍历操作RDD中的每一个分区,返回新的RDD(transformation算子)。
foreachPartition:遍历操作RDD中的每一个分区。无返回值(action算子)。

58 . 简述RDD属性 ?

试题回答参考思路:

DD是一个分布式数据集,顾名思义,其数据应该分部存储于多台机器上。事实上,每个RDD的数据都 以Block的形式存储于多台机器上,下图是Spark的RDD存储架构图,其中每个Executor会启动一个BlockManagerSlave,并管理一部分Block;而Block的元数据由Driver节点的BlockManagerMaster保存。
BlockManagerSlave生成Block后向BlockManagerMaster注册该Block,BlockManagerMaster管理RDD与
Block的关系,当RDD不再需要存储的时候,将向BlockManagerSlave发送指令删除相应的Block
参考答案:
1)一组分区(Partition),即数据集的基本组成单位
RDD数据结构中存在分区列表,用于执行任务时并行计算,是实现分布式计算的重要属性。2)一个计算每个分区的函数
Spark在计算时,是使用分区函数对每一个分区进行计算
3)RDD之间的依赖关系
RDD是计算模型的封装,当需求中需要将多个计算模型进行组合时,就需要将多个RDD建立依赖关系4)一个Partitioner,即RDD的分片函数
当数据为KV类型数据时,可以通过设定分区器自定义数据的分区 5)一个列表,存储存取每个Partition的优先位置(preferred location) 计算数据时,可以根据计算节点的状态选择不同的节点位置进行计算

59 . 简述RDD的缓存级别 ?

试题回答参考思路:

在Spark持久化时,Spark规定了MEMORY_ONLY、MEMORY_AND_DISK等七种不同的存储级别,而存储级 别是以下5个变量的组合

1 class StorageLevel private{
2 private var _useDisk:Boolean, //磁盘
3 private var _useMemory:Boolean, //这里指的是堆内内存
4 private var _useOffHeap:Boolean, //堆外内存
5 private var _deserialized:Boolean, //是否为非序列化
6 private var _replication:Int = 1 //副本个数7 }

Spark中7种存储级别如下:

持久化级别 含义

(1)MEMORY_ONLY 以非序列化的Java对象的方式持久化在JVM内存中。如果内存无法完 全存储RDD所有的partition,那么那些没有持久化的partition就会在下 一次需要使用它们的时候,重新被计算

(2)MEMORY_AND_DISK 同上,但是当某些partition无法存储在内存中时,会持久化到磁盘中。下次需要使用这些partition时,需要从磁盘中读取

(3)MEMORY_ONLY_SER 同MEMORY_ONLY,但是会使用Java序列化方式,将Java对象序列化后 进行持久化。可以减少内存开销,但是需要进行反序列化,因此会加 大CPU开销
(4)MEMORY_AND_DISK_SER 同MEMORY_AND_DISK,但是使用序列化方式持久化Java对象
(5)DISK_ONLY 使用非序列化Java对象的方式持久化,完全存储到磁盘上
(6)MEMORY_ONLY_2、MEMORY_AND_DISK_2等等 如果是尾部加了2的持久化级别,表示将持久化数据复用一份,保存 到其它节点,从而在数据丢失时,不需要再次计算,只需要使用备份数据即可

通过对数据结构的分析,可以看出存储级别从三个维度定义了RDD的Partition(同时也就是Block)的存 储方式:
存储位置:磁盘/堆内内存/堆外内存。如MEMORY_AND_DISK是同时在磁盘和堆内内存上存储,实 现了冗余备份。OFF_HEAP则是只在堆外内存存储,目前选择堆外内存时不能存储到其他位置。存储形式:Block缓存到内存后,是否为非序列化的形式。如MEMORY_ONLY是非序列化方式存储,
OFF_HEAP是序列化方式存储。
副本数量:大于1时需要冗余备份到其他节点。如DISK_ONLY_2需要远程备份1个副本。

60 . 简述Spark广播变量的实现和原理 ?

试题回答参考思路:

Spark官方对广播变量的说明如下:
广播变量可以让我们在每台计算机上保留一个只读变量,而不是为每个任务复制一份副本。例如,可以 使用他们以高效的方式为每个计算节点提供大型输入数据集的副本。Spark也尽量使用有效的广播算法 来分发广播变量,以降低通信成本。
另外,Spark action操作会被划分成一系列的stage来执行,这些stage根据是否产生shuule操作来进行划分的。Spark会自动广播每个stage任务需要的通用数据。这些被广播的数据以序列化的形式缓存起来, 然后在任务运行前进行反序列化。
也就是说,在以下两种情况下显示的创建广播变量才有用:
1)当任务跨多个stage并且需要同样的数据时;
2)当以反序列化的形式来缓存数据时。
从以上官方定义我们可以得出Spark广播变量的一些特性:

1)广播变量会在每个worker节点上保留一份副本,而不是为每个Task保留一份副本。这样有什么好处? 可以想象,在一个worker有时同时会运行若干的Task,若把一个包含较大数据的变量为Task都复制一

份,而且还需要通过网络传输,应用的处理效率一定会受到很大影响。
2) Spark会通过某种广播算法来进行广播变量的分发,这样可以减少通信成本。Spark使用了类似于
BitTorrent协议的数据分发算法来进行广播变量的数据分发,该分发算法会在后面进行分析。
3)广播变量有一定的适用场景:当任务跨多个stage,且需要同样的数据时,或以反序列化的形式来缓 存数据时。

61 . 简述reduceByKey和groupByKey的区别和作用 ?

试题回答参考思路:

reduceByKey:用于对每个 key 对应的多个 value 进行 merge 操作,最重要的是它能够在本地先进行merge 操作,并且 merge 操作可以通过函数自定义
groupByKey:也是对每个 key 进行操作,但只生成一个 sequence , groupByKey 本身不能自定义函数, 需要先用 groupByKey 生成 RDD ,然后才能对此 RDD 通过 map 进行自定义函数操作。
比较发现,使用groupByKey时,spark会将所有的键值对进行移动,不会进行局部merge,会导致集群节 点之间的开销很大,导致传输延时

62 . 简述reduceByKey和reduce的区别 ?

试题回答参考思路:

reduce:是用于一元组,遍历一元组的数据,进行处理。
parallelizeRdd = jsc.parallelize(data);
3 Integer reduceSum = parallelizeRdd.reduce(new Function2() {
4 @Override
5 public Integer call(Integer v1, Integer v2) throws Exception {
6 return v1 + v2; 7 }
8 });

call一次处理为: 1+2=3 ;3+3=9 ;9+4=13 ;13+5 =18 ;18+6 =24
reduceByKey:是用于二元组When called on a dataset of (K, V) pairs, returns a dataset of (K, U) pairs,可以通过function,对一个相同的key的value进行操作,得到一个U类型的值

listKyes = Arrays.asList(“k1”, “k2”, “k3”,“k1”,“k1”,“k2”);
2 JavaRDD keysRDD = jsc.parallelize(listKyes);
3 keysRDD.mapToPair(new PairFunction() {
4 @Override
5 public Tuple2 call(String s) throws Exception {
6 return new Tuple2(s,1); 7 }
8 }).reduceByKey(new Function2() {
9 @Override
10 public Integer call(Integer v1, Integer v2) throws Exception {
11 return v1 + v2; 12 }
13 }).sortByKey(false).foreach(new VoidFunction>() {
//sortByKey(false) 升序
14 @Override
15 public void call(Tuple2 wordCount) throws Exception {
16 System.out.println(wordCount._1 + “=====” + wordCount._2); 17 }
18 });

reduceByKey的call方法对传入的是相同的key的value进行执行

k1:1+1=2, 2+1 =3
2 k2:1+1=2
3 k3:1

63 . 简述使用reduceByKey出现数据倾斜怎么办 ?

试题回答参考思路:

1)自定义分区(继承Partitioner)
2)使用累加器
3) reduceByKey本身有分区内聚合操作,出现数据倾斜可能性较小

64 . 简述Spark SQL的执行原理 ?

试题回答参考思路:

近似于关系型数据库,见下图,Spark SQL语句由Projection(a1,a2,a3)、Data Source(tableA)、
Filter(condition)组成,分别对应SQL查询过程中的Result、Data Source、Operation,也就是说,SQL
语句按指定次序来描述,如Result->Data Source->Operation

执行Spark SQL语句的顺序如下:
1)对读入的SQL语句进行解析(Parse),分辨出SQL语句中的关键词(如SELECT、FROM、Where)、 表达式、Projection、Data Source等,从而判断SQL语句是否规范。
2)将SQL语句和数据库的数据字典(列、表、视图等)进行绑定(Bind),如果相关的Projection、Data Source等都存在的话,就表示这个SQL语句是可以执行的。
3)选择最优计划。一般的数据库会提供几个执行计划,这些计划一般都有运行统计数据,数据库会在 这些计划中选择一个最优计划(Optimize)。
4)计划执行(Execute)。计划执行案Operation->Data Source->Result的次序来执行,在执行过程中有时候甚至不需要读取物理表就可以返回结果,如重新运行执行过的SQL语句,可直接从数据库的缓冲池中 获取返回结果。

65 . 简述Spark checkpoint ?

试题回答参考思路:

1、先看下checkpoint到底是什么?

1) Spark在生产环境下经常会面临Tranformations的RDD非常多(例如,一个Job中包含10 000个RDD)或者具体Tranformation产生的RDD本身计算特别复杂和耗时(例如,计算时常超过1h),此时我们必须考 虑对计算结果数据的持久化。

2) Spark擅长多步骤迭代,同时擅长基于Job的复用,这时如果能够对曾经计算的过程产生的数据进行 复用,就可以极大地提升效率。
3)如果采用persist把数据放在内存中,虽然是最快速的,但是也是最不可靠的。如果放在磁盘上,也不 是完全可靠的。例如,磁盘会损坏,管理员可能清空磁盘等。
4)checkpoint的产生就是为了相对更加可靠地持久化数据,checkpoint可以指定把数据放在本地并且是 多副本的方式,但是在正常的生产情况下是放在HDFS,这就自然地借助HDFS高容错、高可靠的特征完 成了最大化的、可靠的持久化数据的方式。
5)为确保RDD复用计算的可靠性,checkpoint把数据持久化到HDFS中,保证数据最大程度的安全性。
6)checkpoint就是针对整个RDD计算链条中特别需要数据持久化的环节(后面会反复使用当前环节的
RDD)开始基于HDFS等的数据持久化复用策略,通过对RDD启动checkpoint机制来实现容错和高可用

2、checkpoint运行流程
通过SparkContext设置Checkpoint数据保存的目录,RDD调用checkpoint方法,生产
RDDCheckpointData,当RDD上运行一个Job后,就会立即触发RDDCheckpointData中的checkpoint方法, 在其内部会调用doCheckpoint;然后调用ReliableRDDCheckpointData的doCheckpoint;
ReliableCheckpointRDD的writeRDDToCheckpointDirectory的调用;在writeRDDToCheckpointDirectory方法内部会触发runJob,来执行把当前的RDD中的数据写到Checkpoint的目录中,同时会产生
ReliableCheckpointRDD实例。
checkpoint保存在HDFS中,具有多个副本;persist保存在内存中或者磁盘中。在Job作业调度的时候,
checkpoint沿着finalRDD的“血统”关系lineage从后往前回溯向上查找,查找哪些RDD曾标记为要进行
checkpoint,标记为checkpointInProgress;一旦进行checkpoint,RDD所有父RDD就被清空

66 . 简述Spark SQL与DataFrame的使用 ?

试题回答参考思路:

官网描述:Spark SQL is Apache Spark’s module for working with structured data.
也就是说Spark SQL是Spark用于结构化数据(structured data)处理的模块。它能让SQL查询跟Spark程序无缝连接使用。
数据兼容方面:Spark SQL不但兼容Hive,还可以从RDD、parquet文件、JSON文件中获取数据,未来版本甚至支持获取RDBMS数据以及cassandra等NOSQL数据;
性能优化方面:除了采取In-Memory Columnar Storage、byte-code generation等优化技术外、将会引进Cost Model对查询进行动态评估、获取最佳物理计划等等;

组件扩展方面:无论是SQL的语法解析器、分析器还是优化器都可以重新定义,进行扩展。
对于开发人员来讲,Spark SQL可以简化RDD的开发,提高开发效率,且执行效率非常快,所以实际工作中,基本上采用的就是Spark SQL。Spark SQL为了简化RDD的开发,提高开发效率,提供了2个编程抽象:DataFrame和DataSet。
DataFrame
在Spark中,DataFrame是一种以RDD为基础的分布式数据集,类似于传统数据库中的二维表格。
DataFrame与RDD的主要区别在于,前者带有schema元信息,即DataFrame所表示的二维表数据集的每一 列都带有名称和类型。这使得Spark SQL得以洞察更多的结构信息,从而对藏于DataFrame背后的数据源以及作用于DataFrame之上的变换进行了针对性的优化,最终达到大幅提升运行时效率的目标。反观
RDD,由于无从得知所存数据元素的具体内部结构,Spark Core只能在stage层面进行简单、通用的流水线优化。
同时,与Hive类似,DataFrame也支持嵌套数据类型(struct、array和map)。从API易用性的角度上看,
DataFrame API提供的是一套高层的关系操作,比函数式的RDD API要更加友好,门槛更低。
Spark SQL的DataFrame API允许我们使用DataFrame而不用必须去注册临时表或者生成SQL表达式。DataFrame API既有transformation操作也有action操作。
DataFrame优于RDD,因为它提供了内存管理和优化的执行计划。总结为一下两点:
1)自定义内存管理:当数据以二进制格式存储在堆外内存时,会节省大量内存。除此之外,没有垃圾 回收(GC)开销。还避免了昂贵的Java序列化。因为数据是以二进制格式存储的,并且内存的schema 是已知的。
2)优化执行计划:这也称为查询优化器。可以为查询的执行创建一个优化的执行计划。优化执行计划 完成后最终将在RDD上运行执行。
DataSet
DataSet是分布式数据集合。DataSet是Spark1.6中添加的一个新抽象,是DataFrame的一个扩展。它提供 了RDD的优势(强类型,使用强大的lambda函数的能力)以及SparkSQL优化执行引擎的优点。DataSet 也可以使用功能性的转换(操作map,flatMap,filter等等)

67 . 简述HashPartitioner和RangePartitioner的实现 ?

试题回答参考思路:

1、Spark分区器
在Spark中分区器直接决定了RDD中分区的个数,RDD中每条数据经过Shuule过程属于哪个分区以及
Reduce的个数。只有Key-Value类型的RDD才有分区的,非Key-Value类型的RDD分区的值是None的。
在Spark中,存在两类分区函数:HashPartitioner和RangePartitioner,它们都是继承自Partitioner,主要 提供了每个RDD有几个分区(numPartitions)以及对于给定的值返回一个分区ID(0~numPartitions- 1),也就是决定这个值是属于那个分区的。
2、HashPartitioner分区
HashPartitioner分区的原理很简单,对于给定的key,计算其hashCode,并除于分区的个数取余,最后返 回的值就是这个key所属的分区ID。
3、RangePartitioner分区
从HashPartitioner分区的实现原理可以看出,其结果可能导致每个分区中数据量的不均匀。而RangePartitioner分区则尽量保证每个分区中数据量的均匀,而且分区与分区之间是有序的,但是分区内 的元素是不能保证顺序的。sortByKey底层就是RangePartitioner分区器。
首先了解蓄水池抽样(Reservoir Sampling),它能够在O(n)时间内对n个数据进行等概率随机抽取。首先构建一个可放k个元素的蓄水池,将序列的前k个元素放入蓄水池中。然后从第k+1个元素开始,以k/n的概 率来替换掉蓄水池中国的某个元素即可。当遍历完所有元素之后,就可以得到随机挑选出的k个元素, 复杂度为O(n)。
RangePartitioner分区器的主要作用就是将一定范围内的数映射到某一个分区内。该分区器的实现方式主 要是通过两个步骤来实现的,第一步,先从整个RDD中抽取出样本数据,将样本数据排序,计算出每个 分区的最大key值,形成一个Array[KEY]类型的数组变量rangeBounds;第二步,判断key在rangeBounds 中所处的范围,给出该key的分区ID。
RangePartitioner的重点是在于构建rangeBounds数组对象,主要步骤是:
1)计算总体的数据抽样大小sampleSize,计算规则是:(math.min(20.0 * partitions, 1e6)),至少每个分区抽取20个数据或者最多1M的数据量;
2)根据sampleSize和分区数量计算每个分区的数据抽样样本数量sampleSizePrePartition(math.ceil(3.0 * sampleSize / rdd.partitions.length).toInt),即每个分区抽取的数据量一般会比之前计算的大一点);
3)调用RangePartitioner的sketch函数进行数据抽样,计算出每个分区的样本;
4)计算样本的整体占比以及数据量过多的数据分区,防止数据倾斜;
5)对于数据量比较多的RDD分区调用RDD的sample函数API重新进行数据抽取;
6)将最终的样本数据通过RangePartitoner的determineBounds函数进行数据排序分配,计算出
rangeBounds。
RangePartitioner的sketch函数的作用是对RDD中的数据按照需要的样本数据量进行数据抽取,主要调用SamplingUtils类的reservoirSampleAndCount方法对每个分区进行数据抽取,抽取后计算出整体所有分区 的数据量大小;reservoirSampleAndCount方法的抽取方式是先从迭代器中获取样本数量个数据(顺序获 取), 然后对剩余的数据进行判断,替换之前的样本数据,最终达到数据抽样的效果。RangePartitioner的determineBounds函数的作用是根据样本数据记忆权重大小确定数据边界。
4、自定义分区器
可以通过扩展Spark中的默认Partitioner类来自定义我们需要的分区数以及应该存储在这些分区中的内 容。然后通过partitionBy()在RDD上应用自定义分区逻辑

68 . 简述Spark的水塘抽样 ?

试题回答参考思路:

问题定义可以简化如下:在不知道文件总行数的情况下,如何从文件中随机的抽取一行?

首先想到的是我们做过类似的题目吗?当然,在知道文件行数的情况下,我们可以很容易的用C运行库的
rand函数随机的获得一个行数,从而随机的取出一行,但是,当前的情况是不知道行数,这样如何求 ## 呢?我们需要一个概念来帮助我们做出猜想,来使得对每一行取出的概率相等,也即随机。这个概念即 蓄水池抽样(Reservoir Sampling)。
水塘抽样算法(Reservoir Sampling)思想:
在序列流中取一个数,如何确保随机性,即取出某个数据的概率为:1/(已读取数据个数)
假设已经读取n个数,现在保留的数是Ax,取到Ax的概率为(1/n)。
对于第n+1个数An+1,以1/(n+1)的概率取An+1,否则仍然取Ax。依次类推,可以保证取到数据的随机 性。
数学归纳法证明如下:当n=1时,显然,取A1。取A1的概率为1/1。
1 假设当n=k时,取到的数据Ax。取Ax的概率为1/k。
2 当n=k+1时,以1/(k+1)的概率取An+1,否则仍然取Ax。

1)如果取Ak+1,则概率为1/(k+1);
2)如果仍然取Ax,则概率为(1/k)*(k/(k+1))=1/(k+1)
所以,对于之后的第n+1个数An+1,以1/(n+1)的概率取An+1,否则仍然取Ax。依次类推,可以保证取到 数据的随机性。
在序列流中取k个数,如何确保随机性,即取出某个数据的概率为:k/(已读取数据个数)
建立一个数组,将序列流里的前k个数,保存在数组中。(也就是所谓的”蓄水池”)
对于第n个数An,以k/n的概率取An并以1/k的概率随机替换“蓄水池”中的某个元素;否则“蓄水池”数组不 变。依次类推,可以保证取到数据的随机性。

数学归纳法证明如下:
当n=k是,显然“蓄水池”中任何一个数都满足,保留这个数的概率为k/k。
假设当n=m(m>k)时,“蓄水池”中任何一个数都满足,保留这个数的概率为k/m。
当n=m+1时,以k/(m+1)的概率取An,并以1/k的概率,随机替换“蓄水池”中的某个元素,否则“蓄水池”数 组不变。则数组中保留下来的数的概率为:
所以,对于第n个数An,以k/n的概率取An并以1/k的概率随机替换“蓄水池”中的某个元素;否则“蓄水池” 数组不变。依次类推,可以保证取到数据的随机性

69 . 简述DAGScheduler、TaskScheduler、SchedulerBackend实现原理 ?

试题回答参考思路:

Scheduler任务调度器模块作为Spark的核心部件,涉及三个重要的类:
org.apache.spark.scheduler.DAGScheduler org.apache.spark.scheduler.SchedulerBackend org.apache.spark.scheduler.TaskScheduler
1、DAGScheduler
DAGScheduler是面向Stage的高层级的调度器,DAGScheduler把DAG拆分成很多的Tasks,每组的Tasks都是一个Stage,解析时是以Shuule为边界反向解析构建Stage,每当遇到Shuule,就会产生新的Stage,然 后以一个个TaskSet(每个Stage封装一个TaskSet)的形式提交给底层调度器TaskScheduler。
DAGScheduler需要记录哪些RDD被存入磁盘等物化动作,同时要寻求Task的最优化调度,如在Stage内部数据的本地性等。DAGScheduler还需要监视因为Shuule跨节点输出可能导致的失败,如果发现这个Stage 失败,可能就要重新提交该Stage。
2、TaskScheduler
TaskScheduler的核心任务是提交TaskSet到集群运算并汇报结果。

1)为TaskSet创建和维护一个TaskSetManager,并追踪任务的本地性以及错误信息。
2)遇到Straggle任务时,会放到其他节点进行重试。
3)向DAGScheduler汇报执行情况,包括在Shuule输出丢失的时候报告fetch failed错误等信息。

DAGScheduler将划分的一系列的Stage(每个Stage封装一个TaskSet),按照Stage的先后顺序依次提交给 底层的TaskScheduler去执行。
DAGScheduler在SparkContext中实例化的时候,TaskScheduler以及SchedulerBackend就已经先在
SparkContext的createTaskScheduler创建出实例对象了。
虽然Spark支持多种部署模式(包括Local、Standalone、YARN、Mesos等),但是底层调度器
TaskScheduler接口的实现类都是TaskSchedulerImpl。
这里只对Standalone部署模式下的具体实现StandaloneSchedulerBackend来作分析。
TaskSchedulerImpl在createTaskScheduler方法中实例化后,就立即调用自己的initialize方法把
StandaloneSchedulerBackend的实例对象传进来,从而赋值给TaskSchedulerImpl的backend。在
TaskSchedulerImpl的initialize方法中,根据调度模式的配置创建实现了SchedulerBuilder接口的相应的实例对象,并且创建的对象会立即调用buildPools创建相应数量的Pool存放和管理TaskSetManager的实例对 象。实现SchedulerBuilder接口的具体类都是SchedulerBuilder的内部类。

1) FIFOSchedulableBuilder:调度模式是SchedulingMode.FIFO,使用先进先出策略调度。先进先出
(FIFO)为默认模式。在该模式下只有一个TaskSetManager池。
2) FairSchedulableBuilder:调度模式是SchedulingMode.FAIR,使用公平策略调度。
在createTaskScheduler方法返回后,TaskSchedulerImpl通过DAGScheduler的实例化过程设置
DAGScheduler的实例对象。然后调用自己的start方法。在TaskSchedulerImpl调用start方法的时候,会调 用StandaloneSchedulerBackend的start方法,在StandaloneSchedulerBackend的start方法中,会最终注册应用程序AppClient。TaskSchedulerImpl的start方法中还会根据配置判断是否周期性地检查任务的推测执 行。
TaskSchedulerImpl启动后,就可以接收DAGScheduler的submitMissingTasks方法提交过来的TaskSet进行进一步处理。TaskSchedulerImpl在submitTasks中初始化一个TaskSetManager,对其生命周期进行管理, 当TaskSchedulerImpl得到Worker节点上的Executor计算资源的时候,会通过TaskSetManager发送具体的
Task到Executor上执行计算。
如果Task执行过程中有错误导致失败,会调用TaskSetManager来处理Task失败的情况,进而通知
DAGScheduler结束当前的Task。TaskSetManager会将失败的Task再次添加到待执行Task队列中。Spark
Task允许失败的次数默认是4次,在TaskSchedulerImpl初始化的时候,通过spark.task.maxFailures设置该值。
如果Task执行完毕,执行的结果会反馈给TaskSetManager,由TaskSetManager通知DAGScheduler,
DAGScheduler根据是否还存在待执行的Stage,继续迭代提交对应的TaskSet给TaskScheduler去执行,或 者输出Job的结果。
通过下面的调度链,Executor把Task执行的结果返回给调度器(Scheduler)。
(1) Executor.run。
(2) CoarseGrainedExecutorBackend.statusUpdate(发送StatusUpdate消息)。
(3) CoarseGrainedSchedulerBackend.receive(处理StatusUpdate消息)。
(4) TaskSchedulerImpl.statusUpdate。
(5) TaskResultGetter.enqueueSuccessfulTask或者enqueueFailedTask。
(6) TaskSchedulerImpl.handleSuccessfulTask或者handleFailedTask。
(7) TaskSetManager.handleSuccessfulTask或者handleFailedTask。
(8) DAGScheduler.taskEnded。
(9) DAGScheduler.handleTaskCompletion。
在上面的调度链中值得关注的是:第(7)步中,TaskSetManager的handleFailedTask方法会将失败的
Task再次添加到待执行Task队列中。在第(6)步中,TaskSchedulerImpl的handleFailedTask方法在TaskSetManager的handleFailedTask方法返回后,会调用CoarseGrainedSchedulerBackend的reviveOuers方法给重新执行的Task获取资源。
3、SchedulerBackend
这里以Spark Standalone部署方式为例,StandaloneSchedulerBackend在启动的时候构造了
StandaloneAppClient实例,并在该实例start的时候启动了ClientEndpoint消息循环体,ClientEndpoint在启动的时候会向Master注册当前程序。而StandaloneSchedulerBackend的父类CoarseGrainedSchedulerBackend在start的时候会实例化类型为DriverEndPoint(这就是程序运行时的经典 的对象Driver)的消息循环体,StandaloneSchedulerBackend专门负责收集Worker上的资源信息,当
ExecutorBackend启动的时候,会发送RegisteredExecutor信息向DriverEndpoint注册,此时
StandaloneSchedulerBackend就掌握了当前应用程序拥有的计算资源,TaskScheduler就是通过
StandaloneSchedulerBackend拥有的计算资源来具体运行Task的

70 . 简述Spark client提交application后,接下来的流程 ?

试题回答参考思路:

1、Spark on Standalone
1) spark集群启动后,Worker向Master注册信息;
2) spark-submit命令提交程序后,driver和application也会向Master注册信息;

3)创建SparkContext对象:主要的对象包含DAGScheduler和TaskScheduler;
4) Driver把Application信息注册给Master后,Master会根据App信息去Worker节点启动Executor;
5) Executor内部会创建运行task的线程池,然后把启动的Executor反向注册给Dirver;
6) DAGScheduler:负责把Spark作业转换成Stage的DAG(Directed Acyclic Graph有向无环图),根据宽窄依赖切分Stage,然后把Stage封装成TaskSet的形式发送个TaskScheduler;同时DAGScheduler还会处理由于Shuule数据丢失导致的失败;
7) TaskScheduler:维护所有TaskSet,分发Task给各个节点的Executor(根据数据本地化策略分发
Task),监控task的运行状态,负责重试失败的task;
8)所有task运行完成后,SparkContext向Master注销,释放资源。 注:job的失败不会重试

2、Spark on Yarn
yarn是一种统一的资源管理机制,可以通过队列的方式,管理运行多套计算框架。Spark on Yarn模式根据Dirver在集群中的位置分为两种模式。一种是Yarn-Client模式,另一种是Yarn-Cluster模式。

Yarn框架的基本运行流程:
ResourceManager:负责将集群的资源分配给各个应用使用,而资源分配和调度的基本单位是
Container,其中封装了集群资源(CPU、内存、磁盘等),每个任务只能在Container中运行,并且只使 用Container中的资源;
NodeManager:是一个个计算节点,负责启动Application所需的Container,并监控资源的使用情况汇报 给ResourceManager;
ApplicationMaster:主要负责向ResourceManager申请Application的资源,获取Container并跟踪这些
Container的运行状态和执行进度,执行完后通知ResourceManager注销ApplicationMaster,
ApplicationMaster也是运行在Container中。
yarn-client模式,Dirver运行在本地的客户端上

1、Spark on Standalone
1) spark集群启动后,Worker向Master注册信息;
2) spark-submit命令提交程序后,driver和application也会向Master注册信息;
3)创建SparkContext对象:主要的对象包含DAGScheduler和TaskScheduler;
4) Driver把Application信息注册给Master后,Master会根据App信息去Worker节点启动Executor;
5) Executor内部会创建运行task的线程池,然后把启动的Executor反向注册给Dirver;
6) DAGScheduler:负责把Spark作业转换成Stage的DAG(Directed Acyclic Graph有向无环图),根据宽窄依赖切分Stage,然后把Stage封装成TaskSet的形式发送个TaskScheduler;同时DAGScheduler还会处理由于Shuule数据丢失导致的失败;
7) TaskScheduler:维护所有TaskSet,分发Task给各个节点的Executor(根据数据本地化策略分发
Task),监控task的运行状态,负责重试失败的task;
8)所有task运行完成后,SparkContext向Master注销,释放资源。 注:job的失败不会重试

2、Spark on Yarn
yarn是一种统一的资源管理机制,可以通过队列的方式,管理运行多套计算框架。Spark on Yarn模式根据Dirver在集群中的位置分为两种模式。一种是Yarn-Client模式,另一种是Yarn-Cluster模式。

【Yarn框架的基本运行流程】

ResourceManager:负责将集群的资源分配给各个应用使用,而资源分配和调度的基本单位是
Container,其中封装了集群资源(CPU、内存、磁盘等),每个任务只能在Container中运行,并且只使 用Container中的资源;
NodeManager:是一个个计算节点,负责启动Application所需的Container,并监控资源的使用情况汇报 给ResourceManager;
ApplicationMaster:主要负责向ResourceManager申请Application的资源,获取Container并跟踪这些
Container的运行状态和执行进度,执行完后通知ResourceManager注销ApplicationMaster,
ApplicationMaster也是运行在Container中。
yarn-client模式,Dirver运行在本地的客户端上

71 . 简述Spark的几种部署方式(详解) ?

试题回答参考思路:

1、本地模式
Spark Local模式被称为Local[N]模式,是用单机的多个线程来模拟Spark分布式计算,直接运行在本地, 便于调试,通常用来验证开发出来的应用程序逻辑上有没有问题,其中N代表可以使用N个线程,每个线 程拥有一个core。
将Spark应用以多线程的方式直接运行在本地,一般都是为了方便调试,本地模式分三类
local:只启动一个executor
local[k]:启动k个executor
local[*]:启动跟cpu数目相同的executor
通常,Local模式用于完成开发出来的分布式程序的测试工作,并不用于实际生产。

2、Standalone模式
Standalone模式是Spark实现的资源调度框架,其自带完整的服务,可单独部署到一个集群中,无需依赖 任何其他资源管理系统。主要的节点有Client节点、Master节点和Worker节点。其中Driver既可以运行在
Master节点上中,也可以运行在本地Client端。当用spark-shell交互式工具提交Spark的Job时,Driver在
Master节点上运行;当使用spark-submit工具提交Job或者在Eclipse、IDEA等开发平台上使用new
SparkConf().setMaster(“spark://master:7077”)方式运行Spark任务时,Driver是运行在本地Client端上的。
Spark Standalone运行流程:
1) SparkContext连接到Master,向Master注册并申请资源(CPU Core 和Memory);
2) Master根据SparkContext的资源申请要求和Worker心跳周期内报告的信息决定在哪个Worker上分配资 源,然后在该Worker上获取资源,然后启动StandaloneExecutorBackend;
3) StandaloneExecutorBackend向SparkContext注册;
4) SparkContext将Applicaiton代码发送给StandaloneExecutorBackend;并且SparkContext解析
Applicaiton代码,构建DAG图,并提交给DAG Scheduler分解成Stage(当碰到Action操作时,就会催生
Job;每个Job中含有1个或多个Stage,Stage一般在获取外部数据和shuule之前产生),DAG Scheduler
将TaskSet提交给Task Scheduler,Task Scheduler负责将Task分配到相应的Worker,最后提交给
StandaloneExecutorBackend执行;
5) StandaloneExecutorBackend会建立Executor线程池,开始执行Task,并向SparkContext报告,直至
Task完成。
6)所有Task完成后,SparkContext向Master注销,释放资源

3、Spark On Mesos模式
Spark On Mesos模式是很多公司采用的模式,Spark运行在Mesos上会比运行在YARN上更加灵活,更加自然。在Spark On Mesos环境中,用户可选择粗粒度模式和细粒度模式两种调度模式之一运行自己的应用程序。其中细粒度模式于Spark2.0.0版本开始不再使用。
目前在Spark On Mesos环境中,用户可选择两种调度模式之一运行自己的应用程序(可参考Andrew Xia
的“Mesos Scheduling Mode on Spark”):
1)粗粒度模式(Coarse-grained Mode):每个应用程序的运行环境由一个Dirver和若干个Executor组成,其中,每个Executor占用若干资源,内部可运行多个Task(对应多少个“slot”)。应用程序的各个任 务正式运行之前,需要将运行环境中的资源全部申请好,且运行过程中要一直占用这些资源,即使不
用,最后程序运行结束后,回收这些资源。举个例子,比如你提交应用程序时,指定使用5个executor运 行你的应用程序,每个executor占用5GB内存和5个CPU,每个executor内部设置了5个slot,则Mesos需要 先为executor分配资源并启动它们,之后开始调度任务。另外,在程序运行过程中,mesos的master和

slave并不知道executor内部各个task的运行情况,executor直接将任务状态通过内部的通信机制汇报给
Driver,从一定程度上可以认为,每个应用程序利用mesos搭建了一个虚拟集群自己使用

2)细粒度模式(Fine-grained Mode):鉴于粗粒度模式会造成大量资源浪费,Spark On Mesos还提供了另外一种调度模式:细粒度模式,这种模式类似于现在的云计算,思想是按需分配。与粗粒度模式一 样,应用程序启动时,先会启动executor,但每个executor占用资源仅仅是自己运行所需的资源,不需要 考虑将来要运行的任务,之后,mesos会为每个executor动态分配资源,每分配一些,便可以运行一个 新任务,单个Task运行完之后可以马上释放对应的资源。每个Task会汇报状态给Mesos slave和Mesos
Master,便于更加细粒度管理和容错,这种调度模式类似于MapReduce调度模式,每个Task完全独立, 优点是便于资源控制和隔离,但缺点也很明显,短作业运行延迟大

4、Spark On YARN模式
这是一种很有前景的部署模式。但限于YARN自身的发展,目前仅支持粗粒度模式(Coarse-grained
Mode)。这是由于YARN上的Container资源是不可以动态伸缩的,一旦Container启动之后,可使用的资 源不能再发生变化,不过这个已经在YARN计划中了。
Spark on YARN的支持两种模式: yarn-cluster:适用于生产环境
yarn-client:适用于交互、调试,希望立即看到app的输出
yarn-cluster和yarn-client的区别在于yarn appMaster,每个yarn app实例有一个appMaster进程,是为app
启动的第一个container;负责从ResourceManager请求资源,获取到资源后,告诉NodeManager为其启动
container。yarn-cluster和yarn-client模式内部实现还是有很大的区别。如果你需要用于生产环境,那么 请选择yarn-cluster;而如果你仅仅是Debug程序,可以选择yarn-client。
总结:
这几种分布式部署方式各有利弊,通常需要根据实际情况决定采用哪种方案。进行方案选择时,往往要 考虑公司的技术路线(采用Hadoop生态系统还是其他生态系统)、相关技术人才储备等。上面涉及到
Spark的许多部署模式,究竟哪种模式好这个很难说,需要根据你的需求,如果你只是测试Spark
Application,你可以选择local模式。而如果你数据量不是很多,Standalone 是个不错的选择。当你需要统一管理集群资源(Hadoop、Spark等),那么你可以选择Yarn或者mesos,但是这样维护成本就会变 高。
从对比上看,mesos似乎是Spark更好的选择,也是被官方推荐的。
但如果你同时运行hadoop和Spark,从兼容性上考虑,Yarn是更好的选择。
如果你不仅运行了hadoop、spark。还在资源管理上运行了docker,Mesos更加通用。
Standalone对于小规模计算集群更适合。

72 . 简述在Yarn-client情况下,Driver此时在哪 ?

试题回答参考思路:

在哪提交Driver就在哪

73 . 简述Spark的cluster模式有什么好处 ?

试题回答参考思路:

在yarn-cluster模式下,Dirver运行在ApplicationMaster中,也就是说Driver是运行在其它节点的,当用户 提交了作业之后就可以关掉client,此时作业会继续在yarn中运行;
而yarn-client模式下,Dirver运行在本地客户端,client不能离开

74 . 简述Driver怎么管理executor ?

试题回答参考思路:

Spark框架的核心是一个计算引擎,整体来说,它采用了标准 master-slave 的结构。
它展示了一个 Spark执行时的基本结构。图形中的Driver表示master,负责管理整个集群中的作业任务调度。图形中的Executor 则是 slave,负责实际执行任务。

Driver是Spark驱动器节点,用于执行Spark任务中的main方法,负责实际代码的执行工作。
Spark Application的main方法(于SparkContext相关的代码)运行在Driver上,当用于计算的RDD触发
Action动作之后,会提交Job,那么RDD就会向前追溯每一个transformation操作,直到初始的RDD开始, 这之间的代码运行在Executor。
Driver在Spark作业执行时主要负责:
将用户程序转化为作业(job) 在Executor之间调度任务(task) 跟踪Executor的执行情况
通过UI展示查询运行情况
实际上,我们无法准确地描述Driver的定义,因为在整个的编程过程中没有看到任何有关Driver的字眼。 所以简单理解,所谓的Driver就是驱使整个应用运行起来的程序,也称之为Driver类

75 . 简述Spark的map和flatmap的区别 ?

试题回答参考思路:

可迭代类型的函数map与flatMap的区别在于:
map函数会对每一条输入进行指定的操作,然后为每一条输入返回一个对象;而flatMap函数则是两个操 作的集合——正是“先映射后扁平化”:
操作1:同map函数一样:对每一条输入进行指定的操作,然后为每一条输入返回一个对象 操作2:最后将所有对象合并为一个对象
map:List里有小的List
flatmap:是先flat再map,只能压一次,形成一个新的List集合,把原元素放进新的集合里面
map(func)函数会对每一条输入进行指定的func操作,然后为每一条输入返回一个对象;而flatMap(func) 也会对每一条输入进行执行的func操作,然后每一条输入返回一个相对,但是最后会将所有的对象再合 成为一个对象;从返回的结果的数量上来讲,map返回的数据对象的个数和原来的输入数据是相同的, 而flatMap返回的个数则是不同的。

76 . 简述map和mapPartition的区别 ?

试题回答参考思路:

Spark中的map和mapPartitions算子,在处理数据上,一些区别如下:
主要区别:
map是对rdd中的每一个元素进行操作;
mapPartitions则是对rdd中的每个分区的迭代器进行操作。
mapPartitions的优点:
如果是普通的map,比如一个partition中有1万条数据。那么function要执行和计算1万次。
使用MapPartitions操作之后,一个task仅仅会执行一次function,function一次接收所有的partition数据。 只要执行一次就可以了,性能比较高。如果在map过程中需要频繁创建额外的对象(例如将rdd中的数 据通过jdbc写入数据库,map需要为每个元素创建一个链接而mapPartition为每个partition创建一个链
接),则mapPartitions效率比map高的多。
SparkSql或DataFrame默认会对程序进行mapPartition的优化。总结 :MapPartiton 的性能较高
mapPartitions的缺点:

如果是普通的map操作,一次function的执行就处理一条数据;那么如果内存不够用的情况下, 比如处理了1千条数据了,那么这个时候内存不够了,那么就可以将已经处理完的1千条数据从内存里面垃圾回 收掉,或者用其他方法,腾出空间来吧。所以说普通的map操作通常不会导致内存的OOM异常。
但是MapPartitions操作,对于大量数据来说,比如甚至一个partition,100万数据,一次传入一个
function以后,那么可能一下子内存不够,但是又没有办法去腾出内存空间来,可能就OOM,内存溢 出。
总结:MapPartition 有可能会导致内存溢出,数据一次获取过多

77 . RDD的cache和persist的区别?

试题回答参考思路:

cache()调用的persist(),是使用默认存储级别的快捷设置方法
看一下源码

/**
•Persist this RDD with the default storage level (MEMORY_ONLY).
*/
def cache(): this.type = persist()

/**
•Persist this RDD with the default storage level (MEMORY_ONLY).
*/
def persist(): this.type = persist(StorageLevel.MEMORY_ONLY)
通过源码可以看出cache()是persist()的简化方式,调用persist的无参版本,也就是调用persist(StorageLevel.MEMORY_ONLY),cache只有一个默认的缓存级别MEMORY_ONLY,即将数据持久化到内存中,而persist可以通过传递一个 StorageLevel 对象来设置缓存的存储级别。

78 . 简述DataFrame的cache和persist的区别 ?

试题回答参考思路:

官网和上的教程说的都是RDD,但是没有讲df的缓存,通过源码发现df和rdd还是不太一样的:

/**
•Persist this Dataset with the default storage level (MEMORY_AND_DISK).
*
•@group basic
•@since 1.6.0
*/
def cache(): this.type = persist()

/**
•Persist this Dataset with the default storage level (MEMORY_AND_DISK).
*
•@group basic
•@since 1.6.0
*/
def persist(): this.type = {
sparkSession.sharedState.cacheManager.cacheQuery(this)
this
}

/**
•Persist this Dataset with the given storage level.
•@param newLevel One of: MEMORY_ONLY, MEMORY_AND_DISK, MEMORY_ONLY_SER,
•MEMORY_AND_DISK_SER, DISK_ONLY, MEMORY_ONLY_2,
•MEMORY_AND_DISK_2, etc.
*
•@group basic
•@since 1.6.0
*/
def persist(newLevel: StorageLevel): this.type = {
sparkSession.sharedState.cacheManager.cacheQuery(this, None, newLevel)
this
}

def cacheQuery(
query: Dataset[_],
tableName: Option[String] = None,
storageLevel: StorageLevel = MEMORY_AND_DISK): Unit = writeLock
可以到cache()依然调用的persist(),但是persist调用cacheQuery,而cacheQuery的默认存储级别为MEMORY_AND_DISK,这点和rdd是不一样的。

79 . 简述Saprk Streaming从Kafka中读取数据两种方式 ?

试题回答参考思路:

1、Recive
Receive是使用的高级API,需要消费者连接Zookeeper来读取数据。是由Zookeeper来维护偏移量,不用 我们来手动维护,这样的话就比较简单一些,减少了代码量。但是天下没有免费的午餐,它也有很多缺 点:
1)导致丢失数据。它是由Executor内的Receive来拉取数据并存放在内存中,再由Driver端提交的job来处 理数据。这样的话,如果底层节点出现错误,就会发生数据丢失。
2)浪费资源。可以采取WALs方式将数据同步到高可用数据存储平台上(HDFS,S3),那么如果再发生 错误,就可以从中再次读取数据。但是这样会导致同样的数据存储了两份,浪费了资源。
3)可能会导致重复读取数据。对于公司来说,一些数据宁可丢失了一小小部分也不能重复读取,但是 这种由Zookeeper来记录偏移量的方式,可能会因为Spark和Zookeeper不同步,导致一份数据读取了两 次。
4)效率低。因为是分批次执行的,它是接收数据,直到达到了设定的时间间隔,才可是进行计算。而 且我们在KafkaUtils.createStream()中设定的partition数量,只会增加receive的数量,不能提高并行计算的 效率,但我们可以设定不同的Group和Topic创建DStream,然后再用Union合并DStream,提高并行效
率。

2、Direct
Direct方式则采用的是低层次的API,直接连接kafka服务器上读取数据。需要我们自己去手动维护偏移 量,代码量稍微大些。不过这种方式的优点有:
1)当我们读取Topic下的数据时,它会自动对应Topic下的Partition生成相对应数量的RDD Partition,提高了计算时的并行度,提高了效率。
2)它不需要通过WAL来维持数据的完整性。采取Direct直连方式时,当数据发生丢失,只要kafka上的数 据进行了复制,就可以根据副本来进行数据重新拉取。
3)它保证了数据只消费一次。因为我们将偏移量保存在一个地方,当我们读取数据时,从这里拿到数 据的起始偏移量和读取偏移量确定读取范围,通过这些我们可以读取数据,当读取完成后会更新偏移 量,这就保证了数据只消费一次。

3、总结
在spark1.3以后的版本中,direct方式取代了receive方式,当然在公司中,使用的都是direct方式。从上 面对比也能看出receive方式的效率低,而且数据完整性也很让人担忧,当我们采取direct方式时,完全 不用为这两点所担忧,可以根据自己想读取的范围进行读取,底层失败后也能通过副本来进行数据恢 复。

80 . 简述Spark Streaming的工作原理 ?

试题回答参考思路:

1、Spark Streaming介绍
Spark Streaming是一个流式数据处理(Stream Processing)的框架,要处理的数据就像流水一样源源不断的产生,就需要实时处理。
在Spark Streaming中,对于Spark Core进行了API的封装和扩展,将流式的数据切分为小批次(batch, 称之为微批,按照时间间隔切分)进行处理,可以用于进行大规模、高吞吐量、容错的实时数据流的处 理。同Storm相比:Storm是来一条数据处理一条数据,是真正意义上的实时处理
支持从很多种数据源中读取数据,使用算子来进行数据处理,处理后的数据可以被保存到文件系统、数 据库等存储中。
相关概念:

DStream:离散流,相当于是一个数据的集合
StreamingContext:在创建StreamingContext的时候,会自动的创建SparkContext对象
对于电商来说,每时每刻都会产生数据(如订单,网页的浏览数据等),这些数据就需要实时的数据处 理,将源源不断产生的数据实时收集并实时计算,尽可能快的得到计算结果并展示。

2、Spark Streaming组成部分
1)数据源
大多情况从Kafka中获取数据,还可以从Flume中直接获取,还能从TCP Socket中获取数据(一般用于开发测试)
2)数据处理
主要通过DStream针对不同的业务需求使用不同的方法(算子)对数据进行相关操作。
企业中最多的两种类型统计:实时累加统计(如统计某电商销售额)会用到DStream中的算子
updateStateBykey、实时统计某段时间内的数据(如对趋势进行统计分析,实时查看最近20分钟内各个 省份用户点击广告的流量统计)会用到reduceByKeyAndWindow这个算子。
3)存储结果
调用RDD中的API将数据进行存储,因为Spark Streaming是将数据分为微批处理的,所以每一批次就相当于一个RDD。
可以把结果存储到Console(控制台打印,开发测试)、Redis(基于内存的分布式Key-Value数据库)、
HBase(分布式列式数据库)、RDBMS(关系型数据库,如MySQL,通过JDBC)
3、SparkStreaming的运行流程
运行流程
(1)我们在集群中的其中一台机器上提交我们的Application Jar,然后就会产生一个Application,开启一个Driver,然后初始化SparkStreaming的程序入口StreamingContext;
(2) Master(Driver是spark作业的Master)会为这个Application的运行分配资源,在集群中的一台或者 多台Worker上面开启Executer,executer会向Driver注册;

(3) Driver服务器会发送多个receiver给开启的executer,(receiver是一个接收器,是用来接收消息的, 在excuter里面运行的时候,其实就相当于一个task任务)。每个作业包含多个Executor,每个Executor以 线程的方式运行task,Spark Streaming至少包含一个receiver task;
(4) receiver接收到数据后,每隔200ms就生成一个block块,就是一个rdd的分区,然后这些block块就 存储在executer里面,block块的存储级别是Memory_And_Disk_2;
(5) receiver产生了这些block块后会把这些block块的信息发送给StreamingContext;
(6) StreamingContext接收到这些数据后,会根据一定的规则将这些产生的block块定义成一个rdd;

4、Spark Streaming工作原理
接收实时输入数据流,然后将数据拆分成多个batch,比如每收集1s的数据封装为一个batch, 然后将每个batch交给Spark的计算引擎进行处理,最后会生产出一个结果数据流,其中的数据,也是一个个的
batch所组成的。
其中,一个batchInterval累加读取到的数据对应一个RDD的数据
BlockInterval:200ms
生成block块的依据,多久内的数据生成一个block块,默认值200ms生成一个block块,官网最小推荐值
50ms。
BatchInterval:1s
我们将每秒的数据抽象为一个RDD。那么这个RDD里面包含了多个block(1s则是50个RDD),这些block是 分散的存储在各个节点上的。

5、Spark Streaming和Storm对比

1)对比优势

Storm在实时延迟度上,比Spark Streaming就好多了,Storm是纯实时,Spark Streaming是准实时;而且Storm的事务机制,健壮性/容错性、动态调整并行度等特性,都要比Spark Streaming更加优秀。
Spark Streaming的真正优势(Storm绝对比不上的),是它属于Spark生态技术栈中,因此Spark Streaming可以和Spark Core、Spark SQL无缝整合,而这也就意味着,我们可以对实时处理出来的中间数据,立即在程序中无缝进行延迟批处理、交互式查询等操作,这个特点大大增强了Spark Streaming的优势和功能。
2)应用场景Storm应用场景:
建议在那种需要纯实时,不能忍受1s以上延迟的场景下使用,比如金融系统,要求纯实时进行金融 交易和分析;
如果对于实时计算的功能中,要求可靠的事务机制和可靠性机制,即数据的处理完全精准,一条也 不能多,一条也不能少,也可以考虑使用Strom;
如果需要针对高峰低峰时间段,动态调整实时计算程序的并行度,以最大限度利用集群资源,也可 以考虑用Storm;
如果一个大数据应用系统,它就是纯粹的实时计算,不需要在中间执行SQL交互式查询、复杂的
transformation算子等,那么使用Storm是比较好的选择
Spark Streaming应用场景:
如果对上述适用于Storm的三点,一条都不满足的实时场景,即,不要求纯实时,不要求强大可靠 的事务机制,不要求动态调整并行度,那么可以考虑使用Spark Streaming;
考虑使用Spark Streaming最主要的一个因素,应该是针对整个项目进行宏观的考虑,即,如果一个项目除了实时计算之外,还包括了离线批处理、交互式查询等业务功能,而且实时计算中,可能还 会牵扯到高延迟批处理、交互式查询等功能,那么就应该首选Spark生态,用Spark Core开发离线批处理,用Spark SQL开发交互式查询,用Spark Streaming开发实时计算,三者可以无缝整合,给系统提供非常高的可扩展性。

81 . 简述Spark输出文件的个数,如何合并小文件 ?

试题回答参考思路:

当使用spark sql执行etl时候出现了,最终结果大小只有几百k,但是小文件一个分区可能就有上千的情况。
小文件过多的一些危害如下:
hdfs有最大文件数限制
浪费磁盘资源(可能存在空文件)
hive中进行统计,计算的时候,会产生很多个map,影响计算的速度。
解决方案如下:
方法一:通过spark的coalesce()方法和repartition()方法
val rdd2 = rdd1.coalesce(8, true) //(true表示是否shuffle)
val rdd3 = rdd1.repartition(8)
coalesce:coalesce()方法的作用是返回指定一个新的指定分区的Rdd,如果是生成一个窄依赖的结果, 那么可以不发生shuule,分区的数量发生激烈的变化,计算节点不足,不设置true可能会出错。
repartition:coalesce()方法shuule为true的情况。
方法二:降低spark并行度,即调节spark.sql.shuule.partitions
比如之前设置的为100,按理说应该生成的文件数为100;但是由于业务比较特殊,采用的大量的union all,且union all在spark中属于窄依赖,不会进行shuule,所以导致最终会生成(union all数量+1)*100 的文件数。如有10个union all,会生成1100个小文件。这样导致降低并行度为10之后,执行时长大大增加,且文件数依旧有110个,效果有,但是不理想。
方法三:新增一个并行度=1任务,专门合并小文件
先将原来的任务数据写到一个临时分区(如tmp);再起一个并行度为1的任务,类似
insert overwrite 目标表 select * from 临时分区
结果小文件数还是没有减少,经过多次测后发现原因:‘select * from 临时分区’ 这个任务在spark中属于窄依赖;并且spark DAG中分为宽依赖和窄依赖,只有宽依赖会进行shuule;故并行度shuule, spark.sql.shuule.partitions=1也就没有起到作用;
由于数据量本身不是特别大,所以直接采用了group by(在spark中属于宽依赖)的方式,类似
insert overwrite 目标表 select * from 临时分区 group by *
先运行原任务,写到tmp分区,‘dfs -count’查看文件数,1100个,运行加上group by的临时任务
(spark.sql.shuule.partitions=1),查看结果目录,文件数=1,成功。
总结:
1)方便的话,可以采用coalesce()方法和repartition()方法
2)如果任务逻辑简单,数据量少,可以直接降低并行度
3)任务逻辑复杂,数据量很大,原任务大并行度计算写到临时分区,再加两个任务:
一个用来将临时分区的文件用小并行度(加宽依赖)合并成少量文件到实际分区另一个删除临时分区
4)hive任务减少小文件相对比较简单,可以直接设置参数,如: Map-only的任务结束时合并小文件
sethive.merge.mapfiles = true
在Map-Reduce的任务结束时合并小文件:
sethive.merge.mapredfiles= true
当输出文件的平均大小小于1GB时,启动一个独立的map-reduce任务进行文件merge
sethive.merge.smallfiles.avgsize=1024000000

82 . 简述Spark的driver是怎么驱动作业流程的 ?

试题回答参考思路:

SparkContext是通往Spark集群的唯一入口,是整个Application运行调度的核心。
Spark Driver Program(以下简称Driver)是运行Application的main函数并且新建SparkContext实例的程序。其实,初始化SparkContext是为了准备Spark应用程序的运行环境,在Spark中,由SparkContext负责 与集群进行通信、资源的申请、任务的分配和监控等。当Worker节点中的Executor运行完毕Task后,Driver同时负责将SparkContext关闭。通常也可以使用SparkContext来代表驱动程序(Driver)

83 . 简述Spark SQL的劣势 ?

试题回答参考思路:

1)复杂分析,SQL嵌套较多:试想一下3层嵌套的 SQL维护起来应该挺力不从心的吧
2)机器学习较难:试想一下如果使用SQL来实现机器学习算法也挺为难的吧

84 . 简述Spark Streaming和Structed Streaming ?

试题回答参考思路:

Spark在2.*版本后加入Structed Streaming模块,与流处理引擎Spark Streaming一样,用于处理流数据。但二者又有许多不同之处。
Spark Streaming首次引入在0.*版本,其核心思想是利用spark批处理框架,以microbatch(以一段时间的流作为一个batch)的方式,完成对流数据的处理。
Structed Streaming诞生于2.*版本,主要用于处理结构化流数据,除了与Spark Streaming类似的
microbatch的处理流数据方式,也实现了long-running的task,可以"不停的"循环从数据源获取数据并处 理,从而实现真正的流处理。以dataset为代表的带有结构化(schema信息)的数据处理由于钨丝计划 的完成,表现出更优越的性能。同时Structed Streaming可以从数据中获取时间(eventTime),从而可以针对流数据的生产时间而非收到数据的时间进行处理。

【Spark Streaming与Structed Streaming对比】

执行模式
Spark Streaming以micro-batch的模式
以固定的时间间隔来划分每次处理的数据,在批次内以采用的是批处理的模式完成计算
Structed streaming有两种模式: 1)Micro-batch模式
一种同样以micro-batch的模式完成批处理,处理模式类似sparkStreaming的批处理,可以定期(以 固定间隔)处理,也可以处理完一个批次后,立刻进入下一批次的处理

2)Continuous Processing模式
一种启动长时运行的线程从数据源获取数据,worker线程长时运行,实时处理消息。放入queue 中,启动long-running的worker线程从queue中读取数据并处理。该模式下,当前只能支持简单的
projection式(如map,filter,mappartitions等)的操作
API
Spark Streaming、: Sparkstreaming框架基于RDD开发,自实现一套API封装,程序入口是StreamingContext,数据模型是
Dstream,数据的转换操作通过Dstream的api完成,真正的实现依然是通过调用rdd的api完成。
Structed Streaming :
Structed Streaming 基于sql开发,入口是sparksession,使用的统一Dataset数据集,数据的操作会使用
sql自带的优化策略实现。
Job生成与调度
Spark Streaming Job生成:
Structed Streaming job生成与调度 :
Structed Streaming通过trigger完成对job的生成和调度
支持数据源
SparkStreaming出现较早,支持的数据源较为丰富:Socket,filstream,kafka,zeroMq,flume,kinesis Structed Streaming 支持的数据源:Socket,filstream,kafka,ratesource
executed-based
SparkStreaming在执行过程中调用的是dstream api,其核心执行仍然调用rdd的接口
Structed Streaming底层的数据结构为dataset/dataframe,在执行过程中可以使用的钨丝计划,自动代码生成等优化策略,提升性能
Time Based
Sparkstreaming的处理逻辑是根据应用运行时的时间(Processing Time)进行处理,不能根据消息中自带的时间戳完成一些特定的处理逻辑
Structed streaming,加入了event Time,weatermark等概念,在处理消息时,可以考虑消息本身的时间属性。同时,也支持基于运行时时间的处理方式。
UI
Sparkstreaming 提供了内置的界面化的UI操作,便于观察应用运行,批次处理时间,消息速率,是否延迟等信息。
Structed streaming 暂时没有直观的UI

85 . 简述Spark为什么比Hadoop速度快 ?

试题回答参考思路:

Spark SQL比Hadoop Hive快,是有一定条件的,而且不是Spark SQL的引擎比Hive的引擎快,相反,Hive
的HQL引擎还比Spark SQL的引擎更快。
其实,关键还是在于Spark本身快。
1) Spark是基于内存的计算,而Hadoop是基于磁盘的计算;Spark是一种内存计算技术。
所谓的内存计算技术也就是缓存技术,把数据放到缓存中,减少cpu磁盘消耗。Spark和Hadoop的根本 差异是多个任务之间的数据通信问题:Spark多个任务之间数据通信是基于内存,而Hadoop是基于磁盘。Hadoop每次shuule操作后,必须写到磁盘,而Spark在shuule后不一定落盘,可以cache到内存中, 以便迭代时使用。如果操作复杂,很多的shufle操作,那么Hadoop的读写IO时间会大大增加。多个任务 之间的操作也就是shuule过程,因为要把不同task的相同信息集合到一起,这样内存的速度要明显大于 磁盘了。
2) JVM的优化
Hadoop每次MapReduce操作,启动一个Task便会启动一次JVM,基于进程的操作。而Spark每次
MapReduce操作是基于线程的,只在启动Executor是启动一次JVM,内存的Task操作是在线程复用的。
每次启动JVM的时间可能就需要几秒甚至十几秒,那么当Task多了,这个时间Hadoop不知道比Spark慢 了多少。
考虑一种极端查询:Select month_id,sum(sales) from T group by month_id;
这个查询只有一次shuule操作,此时,也许Hive HQL的运行时间也许比Spark还快。
总之,Spark快不是绝对的,但是绝大多数,Spark都比Hadoop计算要快。这主要得益于其对mapreduce 操作的优化以及对JVM使用的优化。
Spark比MapReduce更快的各种原因:
1.在处理过程中,Spark使用RAM存储中间数据,而MapReduce使用磁盘存储中间数据。
2.Spark非常有效地使用底层硬件缓存。除了RAM外,Spark还能有效地使用L1、L2和L3缓存,这些 缓存比RAM更接近CPU。
3.Spark使用内部Catalyst Optimizer来优化查询物理和逻辑计划。
4.Spark使用Predicate Pushdown。
5.Spark拥有自己的垃圾收集机制,而不是在内置的JAVA垃圾收集器中使用。
6.Spark使用其自己的字节码生成器,而不是内置的JAVA生成器。最终的字节码始终在JRE环境中执 行。
7.Spark使用RDD,Transformation和Action进行数据转换,而MapReduce不使用。
8.Spark有自己的专用序列化器和反序列化器,使其更高效

86 . 简述Spark Streaming的双流join的过程,怎么做的 ?

试题回答参考思路:

在构建实时数仓过程中,有时需要将两个实时数据源进行关联,生成大宽表数据,这时就不得不用到双 流join。
1、场景介绍
比如有这样的场景,订单实时数据源,和订单物品实时数据源。订单数据源有订单id,下单时间,订单 金额,收货人,收货地址等信息,订单商品数据源有订。单号id,商品名称,商品品牌,商品价格,商 品成交价格,商品所属商家。订单和订单商品一般是一对多的关系。生成订单大宽表数据,需要将订单 的每一件商品信息都关联上订单详细信息。
2、可能面临问题
我们使用流数据join,不像静态数据那么简单。我们知道静态数据join,两份数据都是确定的,没关联上 就是没关联上,关联上了就是关联上了。此刻关联上的,将来再运行,也必然是关联上的,此刻没关联 上的,将来运行,也是关联不上的。而在流数据join时,由于数据是动态的,两个原本能关联的数据进 入程序的时机未必一致,此时就有可能造成关联数据失去关联,当然没关联上的也有可能是真的关联不 上(join的一方数据缺漏)。
3、解决方案
由于流数据join的特殊性,我们须采取必要的处理方案,使得两份join的流数据尽量都能够关联上,真正 完成流数据join的使命。
订单数据和订单商品数据是一对多的关系,也就是一条订单数据对应着一条或者多条订单商品数据,而 一条订单商品数据只能对应着一条订单数据。所以在join的时候,订单数据无论是否关联上订单商品数 据,都应当保存下来,留作下次继续join使用。而一条订单商品数据,一旦关联上订单数据,即可被废 弃,否则就应当将失配的订单商品数据保存下来,直至下次关联成功。

4、详细说明
为方便说明,假定订单数据A, B, 订单商品数据a1,a2, a3. A可关联a1, a2, a3. B则没有订单商品数据与之关联。下面讨论某一时刻join的各种情况及其应对办法。
1)(A, None), 该情况说明某一时刻,订单数据A到了,能与之关联的a系列数据一个也没到, 此时有2种情况,
a系列数据从未进入过程序, 对于这种情况,我们将A数据保存到redis中,key的结构为 order_info:
. 使得下次a系列的订单商品数据来了之后,有机会进行关联;
还有一种情况是a系列数据先到了,已经保存到了redis中,如情况3的第 2) 种情况所述, 去redis中将
a系列数据查出(可能存在多个订单商品数据), 完成匹配,然后将匹配完的a系列数据,从redis中删除,以免下次join又去redis查询,重复匹配。接着,还需将A数据保存到redis中,因为将来还有可 能有a系列的数据需要匹配。
2)(A, a1), 该情况说明某一时刻,订单数据A到了,能与之关联的a1订单商品数据也到了,正好进行
join。此时需要注意,由于订单数据和订单商品数据是一对多的关系,后续是否还存在能关联A的a系列 订单商品数据,我们并不知道,极有可能是存在的。因此,尽管已经形成了关联,还需要将A保存到
redis中,如1所述,而a1已经完成匹配,不可能再有匹配的机会,无需保存. 另外,还需要注意的是,有可能此时redis中还存有a系列的商品订单数据,因此还需要像情况1的情形 2) 那样,去redis中寻找a系列的商品订单数据,找到后进行关联,关联完要将redis中的a系列商品订单数据删除,避免下次重复匹
配。我们注意到,情况2和情况1的过程几乎差不多,就一个进入程序的A和a1不用查redis就可关联之 外,其他过程几乎一样,因此在程序中,可以将这2种情况合并
3)(None, a2),该情况说明,订单商品数据a2到了,能与之关联的A订单数据却不在。此时我们应当这样处理,先去redis查找A订单数据.
如果找到,则可完成匹配,不用保存a2数据;
如果没有找到A数据,说明A数据还没进入过程序,a2数据应当保存到redis中, key结构为
order_product::, 这样a2数据可以在redis中等待下一次A数据的到来,然后完成匹配
4)(B, None) 其实是和情况1相同,不过实际数据中,不存在能与之关联的订单商品数据,但在程序中我们并不知道这一情况,我们一律将B存到redis中,订单流数据是源源不断的进来,会造成redis数据不断 膨胀,因此我们不能订单数据永久地存于redis中,订单和订单商品是强关联的,关联匹配的两方数据不 可能会相隔太久进入程序。因此我们在redis中设置订单数据的存活时间
需要说明的是,对于没有匹配的订单商品数据,保存到redis中,一旦下次完成匹配,须立刻将其删除, 以免下次join又将其查询出来重复匹配。还有,如果订单商品数据一直没有匹配的订单数据,过一段时 间也应该将其删除,因此同样地,需要在redis中设置订单商品的存活时间

87 . 简述Spark怎么保证数据不丢失 ?

试题回答参考思路:

在Spark Streaming的生产实践中,要做到数据零丢失,需要满足以下几个先决条件:
1、输入的数据源是可靠的/数据接收器是可靠的
对于一些输入数据源(比如Kafka),Spark Streaming可以对已经接收的数据进行确认。输入的数据首先被接收器(receivers )所接收,然后存储到Spark中(默认情况下,数据保存到2个执行器中以便进行容错)。数据一旦存储到Spark中,接收器可以对它进行确认(比如,如果消费Kafka里面的数据时可以 更新Zookeeper里面的偏移量)。这种机制保证了在接收器突然挂掉的情况下也不会丢失数据:因为数 据虽然被接收,但是没有被持久化的情况下是不会发送确认消息的。所以在接收器恢复的时候,数据可 以被原端重新发送。
2、应用程序的metadata被application的driver持久化了(checkpointed )
可靠的数据源和接收器可以让我们从接收器挂掉的情况下恢复(或者是接收器运行的Exectuor和服务器 ## 挂掉都可以)。但是更棘手的问题是,如果Driver挂掉如何恢复?对此引入了很多技术来让Driver从失败 中恢复。其中一个就是对应用程序的元数据进行Checkpoint。利用这个特性,Driver可以将应用程序的重 要元数据持久化到可靠的存储中,比如HDFS;然后Driver可以利用这些持久化的数据进行恢复。
经过上面两点还是可能存在数据丢失的场景如下

1)两个Exectuor已经从接收器中接收到输入数据,并将它缓存到Exectuor的内存中; 2)接收器通知输入源数据已经接收;
3)Exectuor根据应用程序的代码开始处理已经缓存的数据;
4)这时候Driver突然挂掉了;
5)从设计的角度看,一旦Driver挂掉之后,它维护的Exectuor也将全部被kill;
6)既然所有的Exectuor被kill了,所以缓存到它们内存中的数据也将被丢失。结果,这些已经通知数据 源但是还没有处理的缓存数据就丢失了;
7)缓存的时候不可能恢复,因为它们是缓存在Exectuor的内存中,所以数据被丢失了。 为了解决上面提到的糟糕场景,Spark Streaming 1.2开始引入了下面的WAL机制。3、启用了WAL特性(Write ahead log)
但如果Exectuor已经接收并缓存了数据,这个时候挂掉了,这个时候数据是在Exectuor中,数据还是会丢失,启 用了WAL机制,所以已经接收的数据被接收器写入到容错存储中,比如HDFS。由于采用了WAl机制, Driver可以从失败的点重新读取数据,即使Exectuor中内存的数据已经丢失了。在这个简单的方法下, Spark Streaming提供了一种即使是Driver挂掉也可以避免数据丢失的机制。
虽然WAL可以确保数据不丢失,它并不能对所有的数据源保证exactly-once语义。想象一下可能发生在
Spark Streaming整合Kafka的糟糕场景。
1)接收器接收到输入数据,并把它存储到WAL中; 2)接收器在更新Zookeeper中Kafka的偏移量之前突然挂掉了;
3)Spark Streaming假设输入数据已成功收到(因为它已经写入到WAL中),然而Kafka认为数据被没有被消费,因为相应的偏移量并没有在Zookeeper中更新;
4)过了一会,接收器从失败中恢复;
5)那些被保存到WAL中但未被处理的数据被重新读取;
6)一旦从WAL中读取所有的数据之后,接收器开始从Kafka中消费数据。因为接收器是采用Kafka的High- Level Consumer API实现的,它开始从Zookeeper当前记录的偏移量开始读取数据,但是因为接收器挂掉的时候偏移量并没有更新到Zookeeper中,所有有一些数据被处理了2次。
除了上面描述的场景,WAL还有其他两个不可忽略的缺点:
1)WAL减少了接收器的吞吐量,因为接受到的数据必须保存到可靠的分布式文件系统中。
2)对于一些输入源来说,它会重复相同的数据。比如当从Kafka中读取数据,你需要在Kafka的brokers
中保存一份数据,而且你还得在Spark Streaming中保存一份。
Kafka direct API
为了解决由WAL引入的性能损失,并且保证 exactly-once 语义,Spark Streaming 1.3中引入了名为Kafka direct API。
这个想法对于这个特性是非常明智的。Spark driver只需要简单地计算下一个batch需要处理Kafka中偏移量的范围,然后命令Spark Exectuor直接从Kafka相应Topic的分区中消费数据。换句话说,这种方法把Kafka当作成一个文件系统,然后像读文件一样来消费Topic中的数据。
在这个简单但强大的设计中:
不再需要Kafka接收器,Exectuor直接采用Simple Consumer API从Kafka中消费数据。不再需要WAL机制,我们仍然可以从失败恢复之后从Kafka中重新消费数据; exactly-once语义得以保存,我们不再从WAL中读取重复的数据

88 . 简述Spark SQL如何使用UDF ?

试题回答参考思路:

用户自定义函数(UDF)是大多数SQL环境的一个关键特性,其主要用于扩展系统的内置功能。UDF允许开发人员通过抽象其低级语言实现在更高级语言(如SQL)中应用的新函数。Apache Spark也不例外,其为UDF与Spark SQL工作流集成提供了各种选项。

Spark_SQL的UDF使用
用户自定义函数,也叫UDF,可以让我们使用Python/Java/Scala注册自定义函数,并在SQL中调用。这种方法很常用,通常用来给机构内的SQL用户们提供高级功能支持,这样这些用户就可以直接调用注册的函数而无需自己去通过编程来实现了。

在Spark SQL中,编写UDF 尤为简单。Spark SQL不仅有自己的UDF接口,也支持已有的Apache Hive UDF。我们可以使用Spark支持的编程语言编写好函数,然后通过Spark SQL内建的方法传递进来,非常便捷地注册我们自己的UDF。
在Scala和Python中,可以利用语言原生的函数和lambda语法的支持,而在Java中,则需要扩展对应的UDF类。UDF能够支持各种数据类型,返回类型也可以与调用时的参数类型完全不一样。
UDF简单使用
首先通过代码建立一个测试的DataFrame数据,通过RDD产生,再转换成DataFrame格式,通过写简单的UDF函数,对数据进行操作并输出,例如:

import org.apache.spark.sql.Row
import org.apache.spark.rdd._
import scala.collection.mutable.ArrayBuffer
import org.apache.spark.sql.types.{StructType, StructField, StringType, IntegerType}
// 通过RDD创建测试数据
val rdd: RDD[Row] = sc.parallelize(List(“Michael,male, 29”,
“Andy,female, 30”,
“Justin,male, 19”,
“Dela,female, 25”,
“Magi,male, 20”,
“Pule,male,21”))
.map(_.split(“,”)).map(p => Row(p(0),p(1),p(2).trim.toInt))
// 创建Schema
val schema = StructType( Array( StructField(“name”,StringType, true),StructField(“sex”,StringType, true),StructField(“age”,IntegerType,true)))
// 转换DataFrame
val peopleDF = spark.sqlContext.createDataFrame(rdd,schema)
// 注册UDF函数
spark.udf.register(“strlen”,(x:String)=>x.length)
// 创建临时表
peopleDF.registerTempTable(“people”)
// 选择输出语句,(选择输出列:名字,名字长度,性别从表people中)
spark.sql(“select name, strlen(name) as strlen,sex from people”).show()

创建 DataFrame

scala> val df = spark.read.json(“data/user.json”)
df: org.apache.spark.sql.DataFrame = [age: bigint, username: string]
注册 UDF

scala> spark.udf.register(“addName”,(x:String)=> “Name:”+x)
res9: org.apache.spark.sql.expressions.UserDefinedFunction =
UserDefinedFunction(,StringType,Some(List(StringType)))
创建临时表

scala> df.createOrReplaceTempView(“people”)
应用 UDF

scala> spark.sql(“Select addName(name),age from people”).show()
当Spark SQL的内置功能需要扩展时,UDF是一个非常有用的工具。

89 . 简述Spark实现wordcount ?

试题回答参考思路:

方法一:map + reduceByKey
package com.cw.bigdata.spark.wordcount

import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

/**
•WordCount实现第一种方式:map + reduceByKey
*
•@author 陈小哥cw
•@date 2020/7/9 9:59
/
object WordCount1 {
def main(args: Array[String]): Unit = {
val config: SparkConf = new SparkConf().setMaster("local[
]").setAppName(“WordCount1”)

val sc: SparkContext = new SparkContext(config)

val lines: RDD[String] = sc.textFile(“in”)

lines.flatMap(.split(" ")).map((,1)).reduceByKey(+).collect().foreach(println)
}
}

方法二:使用countByValue代替map + reduceByKey
package com.cw.bigdata.spark.wordcount

import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

/**
•WordCount实现第二种方式:使用countByValue代替map + reduceByKey
*
•根据数据集每个元素相同的内容来计数。返回相同内容的元素对应的条数。(不必作用在kv格式上)
•map(value => (value, null)).countByKey()
*
•@author 陈小哥cw
•@date 2020/7/9 10:02
/
object WordCount2 {
def main(args: Array[String]): Unit = {
val config: SparkConf = new SparkConf().setMaster("local[
]").setAppName(“WordCount2”)

val sc: SparkContext = new SparkContext(config)

val lines: RDD[String] = sc.textFile(“in”)

lines.flatMap(_.split(" ")).countByValue().foreach(println)

}
}

方法三:aggregateByKey或者foldByKey
package com.cw.bigdata.spark.wordcount

import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.rdd.RDD

/**
•WordCount实现第三种方式:aggregateByKey或者foldByKey
*
•def aggregateByKey[U: ClassTag](zeroValue: U)(seqOp: (U, V) => U,combOp: (U, U) => U): RDD[(K, U)]
•1.zeroValue:给每一个分区中的每一个key一个初始值;
•2.seqOp:函数用于在每一个分区中用初始值逐步迭代value;(分区内聚合函数)
•3.combOp:函数用于合并每个分区中的结果。(分区间聚合函数)
*
•foldByKey相当于aggregateByKey的简化操作,seqop和combop相同
*
*
•@author 陈小哥cw
•@date 2020/7/9 10:08
/
object WordCount3 {
def main(args: Array[String]): Unit = {
val config: SparkConf = new SparkConf().setMaster("local[
]").setAppName(“WordCount3”)

val sc: SparkContext = new SparkContext(config)

val lines: RDD[String] = sc.textFile(“in”)

lines.flatMap(.split(" ")).map((, 1)).aggregateByKey(0)(_ + _, _ + _).collect().foreach(println)

lines.flatMap(.split(" ")).map((, 1)).foldByKey(0)(_ + _).collect().foreach(println)

}
}

方法四:groupByKey+map
package com.cw.bigdata.spark.wordcount

import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.rdd.RDD

/**
•WordCount实现的第四种方式:groupByKey+map
*
•@author 陈小哥cw
•@date 2020/7/9 13:32
/
object WordCount4 {
def main(args: Array[String]): Unit = {
val config: SparkConf = new SparkConf().setMaster("local[
]").setAppName(“WordCount4”)

val sc: SparkContext = new SparkContext(config)

val lines: RDD[String] = sc.textFile(“in”)

val groupByKeyRDD: RDD[(String, Iterable[Int])] = lines.flatMap(.split(" ")).map((, 1)).groupByKey()

groupByKeyRDD.map(tuple => {
(tuple._1, tuple._2.sum)
}).collect().foreach(println)

}
}

方法五:Scala原生实现wordcount
package com.cw.bigdata.spark.wordcount

/**
•Scala原生实现wordcount
*
•@author 陈小哥cw
•@date 2020/7/9 14:22
*/
object WordCount5 {
def main(args: Array[String]): Unit = {

val list = List(“cw is cool”, “wc is beautiful”, “andy is beautiful”, “mike is cool”)
/**
•第一步,将list中的元素按照分隔符这里是空格拆分,然后展开
•先map(_.split(" "))将每一个元素按照空格拆分
•然后flatten展开
•flatmap即为上面两个步骤的整合
*/

val res0 = list.map(.split(" ")).flatten
val res1 = list.flatMap(
.split(" "))

println(“第一步结果”)
println(res0)
println(res1)

/**
•第二步是将拆分后得到的每个单词生成一个元组
•k是单词名称,v任意字符即可这里是1
/
val res3 = res1.map((_, 1))
println(“第二步结果”)
println(res3)
/
*
•第三步是根据相同的key合并
/
val res4 = res3.groupBy(_._1)
println(“第三步结果”)
println(res4)
/
*
•最后一步是求出groupBy后的每个key对应的value的size大小,即单词出现的个数
*/
val res5 = res4.mapValues(_.size)
println(“最后一步结果”)
println(res5.toBuffer)
}
}

方法六:combineByKey
package com.cw.bigdata.spark.wordcount

import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.rdd.RDD

/**
•WordCount实现的第六种方式:combineByKey
*
•@author 陈小哥cw
•@date 2020/7/9 22:55
/
object WordCount6 {
def main(args: Array[String]): Unit = {
val config: SparkConf = new SparkConf().setMaster("local[
]").setAppName(“combineByKey”)

val sc: SparkContext = new SparkContext(config)

val lines: RDD[String] = sc.textFile(“in”)

val mapRDD: RDD[(String, Int)] = lines.flatMap(.split(" ")).map((, 1))

// combineByKey实现wordcount
mapRDD.combineByKey(
x => x,
(x: Int, y: Int) => x + y,
(x: Int, y: Int) => x + y
).collect().foreach(println)

}
}

90 . 简述Spark Streaming怎么实现数据持久化保存 ?

试题回答参考思路:

1、缓存与持久化机制
与RDD类似,Spark Streaming也可以让开发人员手动控制,将数据流中的数据持久化到内存中。对
DStream调用persist()方法,就可以让Spark Streaming自动将该数据流中的所有产生的RDD,都持久化到内存中。如果要对一个DStream多次执行操作,那么,对DStream持久化是非常有用的。因为多次操作, 可以共享使用内存中的一份缓存数据。
对于基于窗口的操作,比如reduceByWindow、reduceByKeyAndWindow,以及基于状态的操作,比如
updateStateByKey,默认就隐式开启了持久化机制。即Spark Streaming默认就会将上述操作产生的
Dstream中的数据,缓存到内存中,不需要开发人员手动调用persist()方法。
对于通过网络接收数据的输入流,比如socket、Kafka、Flume等,默认的持久化级别,是将数据复制一 份,以便于容错。相当于是,用的是类似MEMORY_ONLY_SER_2。
与RDD不同的是,默认的持久化级别,统一都是要序列化的。
2、Checkpoint机制
Checkpoint机制概述
每一个Spark Streaming应用,正常来说,都是要7 * 24小时运转的,这就是实时计算程序的特点。因为要持续不断的对数据进行计算。因此,对实时计算应用的要求,应该是必须要能够对与应用程序逻辑无 关的失败,进行容错。
如果要实现这个目标,Spark Streaming程序就必须将足够的信息checkpoint到容错的存储系统上,从而让它能够从失败中进行恢复。有两种数据需要被进行checkpoint:
1)元数据checkpoint——将定义了流式计算逻辑的信息,保存到容错的存储系统上,比如HDFS。当运行Spark Streaming应用程序的Driver进程所在节点失败时,该信息可以用于进行恢复。元数据信息包括
了:
配置信息——创建Spark Streaming应用程序的配置信息,比如SparkConf中的信息。
DStream的操作信息——定义了Spark Stream应用程序的计算逻辑的DStream操作信息。未处理的batch信息——那些job正在排队,还没处理的batch信息。
2)数据checkpoint——将实时计算过程中产生的RDD的数据保存到可靠的存储系统中。
对于一些将多个batch的数据进行聚合的,有状态的transformation操作,这是非常有用的。在这种
transformation操作中,生成的RDD是依赖于之前的batch的RDD的,这会导致随着时间的推移,RDD的依 赖链条变得越来越长。
要避免由于依赖链条越来越长,导致的一起变得越来越长的失败恢复时间,有状态的transformation操作 执行过程中间产生的RDD,会定期地被checkpoint到可靠的存储系统上,比如HDFS。从而削减RDD的依 赖链条,进而缩短失败恢复时,RDD的恢复时间。
一句话概括,元数据checkpoint主要是为了从driver失败中进行恢复;而RDD checkpoint主要是为了,使用到有状态的transformation操作时,能够在其生产出的数据丢失时,进行快速的失败恢复。

何时启用Checkpoint机制?

1)使用了有状态的transformation操作——比如updateStateByKey,或者reduceByKeyAndWindow操作, 被使用了,那么checkpoint目录要求是必须提供的,也就是必须开启checkpoint机制,从而进行周期性 的RDD checkpoint。
2)要保证可以从Driver失败中进行恢复——元数据checkpoint需要启用,来进行这种情况的恢复。
要注意的是,并不是说,所有的Spark Streaming应用程序,都要启用checkpoint机制,如果即不强制要求从Driver失败中自动进行恢复,又没使用有状态的transformation操作,那么就不需要启用
checkpoint。事实上,这么做反而是有助于提升性能的。

如何启用Checkpoint机制?

1)对于有状态的transformation操作,启用checkpoint机制,定期将其生产的RDD数据checkpoint,是比 较简单的。
可以通过配置一个容错的、可靠的文件系统(比如HDFS)的目录,来启用checkpoint机制,checkpoint 数据就会写入该目录。使用StreamingContext的checkpoint()方法即可。然后,你就可以放心使用有状态 的transformation操作了。
2)如果为了要从Driver失败中进行恢复,那么启用checkpoint机制,是比较复杂的。需要改写Spark
Streaming应用程序。
当应用程序第一次启动的时候,需要创建一个新的StreamingContext,并且调用其start()方法,进行启 动。当Driver从失败中恢复过来时,需要从checkpoint目录中记录的元数据中,恢复出来一个
StreamingContext。
Checkpoint的说明
将RDD checkpoint到可靠的存储系统上,会耗费很多性能。当RDD被checkpoint时,会导致这些batch的处理时间增加。因此,checkpoint的间隔,需要谨慎的设置。
对于那些间隔很多的batch,比如1秒,如果还要执行checkpoint操作,则会大幅度削减吞吐量。而另外 一方面,如果checkpoint操作执行的太不频繁,那就会导致RDD的lineage变长,又会有失败恢复时间过 长的风险。
对于那些要求checkpoint的有状态的transformation操作,默认的checkpoint间隔通常是batch间隔的数倍,至少是10秒。使用DStream的checkpoint()方法,可以设置这个DStream的checkpoint的间隔时长。通 常来说,将checkpoint间隔设置为窗口操作的滑动间隔的5~10倍,是个不错的选择

91 . 简述Spark SQL读取文件,内存不够使用,如何处理 ?

试题回答参考思路:

增加驱动程序内存

92 . 简述Spark的lazy体现在哪里 ?

试题回答参考思路:

Spark通过lazy特性,可以进行底层的spark应用执行的优化

93 . 简述Spark中的并行度等于什么 ?

试题回答参考思路:

Spark作业中,各个stage的task的数量,也就代表了spark作业在各个阶段stage的并行度。也可以说等于RDD的一个分区数

94 . 简述Spark运行时并行度的设置 ?

试题回答参考思路:

1、Spark的并行度指的是什么?

spark作业中,各个stage的task的数量,也就代表了spark作业在各个阶段stage的并行度。
当分配完所能分配的最大资源了,然后对应资源去调节程序的并行度,如果并行度没有与资源相匹配, 那么导致你分配下去的资源都浪费掉了。同时并行运行,还可以让每个task要处理的数量变少(很简单 的原理。合理设置并行度,可以充分利用集群资源,减少每个task处理数据量,而增加性能加快运行速 度)。
假如,现在已经在spark-submit 脚本里面,给我们的spark作业分配了足够多的资源,比如50个executor
,每个executor 有10G内存,每个executor有3个cpu core 。 基本已经达到了集群或者yarn队列的资源上限。
task没有设置,或者设置的很少,比如就设置了,100个task 。50个executor ,每个executor 有3个core
,也就是说** Application 任何一个stage运行的时候,都有总数150个cpu core ,可以并行运行。但是, 你现在只有100个task ,平均分配一下,每个executor 分配到2个task,ok,那么同时在运行的task,只有100个task,每个executor 只会并行运行 2个task。 每个executor 剩下的一个cpu core 就浪费掉了!你的资源,虽然分配充足了,但是问题是, 并行度没有与资源相匹配,导致你分配下去的资源都浪费掉了。合理的并行度的设置,应该要设置的足够大,大到可以完全合理的利用你的集群资源;** 比如上面的例子,总共集群有150个cpu core ,可以并行运行150个task。那么你就应该将你的Application 的并行度,至少设置成150个,才能完全有效的利用你的集群资源,让150个task ,并行执行,而且task增加到
150个以后,即可以同时并行运行,还可以让每个task要处理的数量变少;比如总共 150G 的数据要处理,如果是100个task ,每个task 要计算1.5G的数据。 现在增加到150个task,每个task只要处理1G数据。
2、如何去提高并行度
1)task数量,至少设置成与spark Application 的总cpu core 数量相同(最理性情况,150个core,分配
150task,一起运行,差不多同一时间运行完毕)官方推荐,task数量,设置成spark Application 总cpu
core数量的2~3倍 ,比如150个cpu core ,基本设置 task数量为 300~ 500. 与理性情况不同的,有些task 会运行快一点,比如50s 就完了,有些task 可能会慢一点,要一分半才运行完,所以如果你的task数量,刚好设置的跟cpu core 数量相同,可能会导致资源的浪费,因为 比如150task ,10个先运行完了, 剩余140个还在运行,但是这个时候,就有10个cpu core空闲出来了,导致浪费。如果设置2~3倍,那么一个task运行完以后,另外一个task马上补上来,尽量让cpu core不要空闲。同时尽量提升spark运行效率和速度。提升性能。

2)如何设置一个Spark Application的并行度?

spark.defalut.parallelism:默认是没有值的,如果设置了值比如说10,是在shuule的过程才会起作用
(val rdd2 = rdd1.reduceByKey( + ) //rdd2的分区数就是10,rdd1的分区数不受这个参数的影响)

3)如果读取的数据在HDFS上,增加block数,默认情况下split与block是一对一的,而split又与RDD中的
partition对应,所以增加了block数,也就提高了并行度。
4) RDD.repartition,给RDD重新设置partition的数量
5) reduceByKey的算子指定partition的数量
6) val rdd3 = rdd1.join(rdd2)
rdd3里面partiiton的数量是由父RDD中最多的partition数量来决定,因此使用join算子的时候,增加父
RDD中partition的数量。
7)设置spark sql中shuule过程中partitions的数量

95 . 简述Spark SQL的数据倾斜解决方案 ?

试题回答参考思路:

聚合源数据:Spark Core和Spark SQL没有任何区别过滤导致倾斜的key:在sql中用where条件
提高shuule并行度:groupByKey(1000),spark.sql.shuule.partitions(默认是200) 双重groupBy:改写SQL,两次groupBy
reduce join转换为map join:spark.sql.autoBroadcastJoinThreshold(默认是10485760);可以自己将表做成RDD,自己手动去实现map join;SparkSQL内置的map join,默认如果有一个10M以内的小表,会将该表进行broadcast,然后执行map join;调节这个阈值,比如调节到20M、50M、甚至1G。
采样倾斜key并单独进行join:纯Spark Core的一种方式,sample、filter等算子随机key与扩容表:Spark SQL+Spark Core

96 . 简述Spark的RDD和partition的联系 ?

试题回答参考思路:

RDD主要由Dependency、Partition、Partitioner组成,Partition是其中之一。
一份待处理的原始数据会被按照相应的逻辑(例如jdbc和hdfs的split逻辑)切分成n份,每份数据对应到
RDD中的一个Partition,Partition的数量决定了task的数量,影响着程序的并行度
Partition和RDD是伴生的,即每一种RDD都有其对应的Partition实现,分析Partition主要是分析其子类, 两个常用的子类:JdbcPartition和HadoopPartition。
1、决定Partition数量的因素
Partition数量可以在初始化RDD时指定(如JdbcPartition例子),不指定的话,则读取
spark.default.parallelism配置,不同类型资源管理器取值不同。
2、Partition数量的影响
Partition数量太少:资源不能充分利用,例如local模式下,有16core,但是Partition数量仅为8的话,有 一半的core没利用到。
Partition数量太多:资源利用没问题,但是导致task过多,task的序列化和传输的时间开销增大。
3、Partition调整
1) repartition
reparation是coalesce(numPartitions, shuule = true),repartition不仅会调整Partition数,也会将Partitioner
修改为hashPartitioner,产生shuule操作。
2) coalesce
coalesce函数可以控制是否shuule,但当shuule为false时,只能减小Partition数,无法增大。

97 . 简述Spark 3.0特性 ?

试题回答参考思路:

Spark 3.0增加的新特性包括动态分区修剪(Dynamic Partition Pruning)、自适应查询执行(Adaptive Query Execution)、加速器感知调度(Accelerator-aware Scheduling)、支持 Catalog 的数据源API(Data Source API with Catalog Supports)、SparkR 中的向量化(Vectorization in SparkR)、支持Hadoop 3/JDK 11/Scala 2.12 等等。
1、动态分区裁剪(Dynamic Partition Pruning)
在3.0以前,spark是不支持动态分区的,所谓动态分区就是针对分区表中多个表进行join的时候基于运行 时(runtime)推断出来的信息,在on后面的条件语句满足一定的要求后就会进行自动动态分区裁减优化。

2、自适应查询执行(Adaptive Query Execution)
自适应查询是指对执行计划按照实际数据分布和组织情况,评估其执行所消耗的时间和资源,从而选择 代价最小的计划去执行。一般数据库的优化器有两种,一种是基于规则的优化器(RBO),一种是基于代 价的优化器(CBO),自适应查询指的就是对CBO的优化,Spark SQL的运行流程主要有:
1)将SQL语句通过词法和语法解析生成未绑定的逻辑执行计划;
2)分析器配合元数据使用分析规则,完善未绑定的逻辑计划属性转换成绑定的逻辑计划;
3)优化器使用优化规则将绑定的逻辑计划进行合并、裁减、下推生成优化的逻辑计划;
4)规划器使用规划策略把优化的逻辑计划转换成可执行的物理计划;
5)进行preparations规则处理,构建RDD的DAG图执行查询计划。
Spark以前的调度规则是执行计划一旦确定,即使发现后续执行计划可以优化,也不可更改,而自适应 查询功能则是在执行查询计划的同时,基于表和列的统计信息,对各个算子产生的中间结果集大小进行 估算,根据估算结果来动态地选择最优执行计划
下面举一个简单的例子,执行的是两个表之间的join查询。包含一个key和Filter,如t2.col2 LIKE
‘9999%’。
在基于cost的模型中是不可能准确的知道Filter能排除多少行的,这种情况下Spark通过谓词下推,将各个 条件先应用到对应的数据上,而不是根据写入的顺序执行,就可以先过滤掉部分数据,降低join等一系 列操作的数据量级。
在没有动态实时运行信息的时候,保守估计判断只能用SortMergeJoin。
当收集到运行时信息后会发现某个Filter事实上已经去掉了表中绝大多数行,完全可以采用
BroadcastHashJoin,如果上层parent也有这种情况,就可以大大提升查询效率

3、加速器感知调度(Accelerator-aware Scheduling)
如今大数据和机器学习已经有了很大的结合,在机器学习里面,因为计算迭代的时间可能会很长,开发 人员一般会选择使用 GPU、FPGA 或 TPU 来加速计算。
在 Apache Hadoop 3.1 版本里面已经开始内置原生支持 GPU 和 FPGA 了。作为通用计算引擎的 Spark 肯定也不甘落后,来自 Databricks、NVIDIA、Google 以及阿里巴巴的工程师们正在为 Apache Spark 添加原生的 GPU 调度支持,该方案填补了 Spark 在 GPU 资源的任务调度方面的空白,有机地融合了大数据处理和 AI 应用,扩展了 Spark 在深度学习、信号处理和各大数据应用的应用场景。
为了让 Spark 也支持 GPUs,在技术层面上需要做出两个主要改变:
1)在 cluster manager 层面上,需要升级 cluster managers 来支持 GPU。并且给用户提供相关 API,使得用户可以控制 GPU 资源的使用和分配。
2)在 Spark 内部,需要在 scheduler 层面做出修改,使得 scheduler 可以在用户 task 请求中识别 GPU
的需求,然后根据 executor 上的 GPU 供给来完成分配。
因为让 Apache Spark 支持 GPU 是一个比较大的特性,所以项目分为了几个阶段。
1)在 Apache Spark 3.0 版本,将支持在 standalone、 YARN 以及 Kubernetes 资源管理器下支持 GPU,并且对现有正常的作业基本没影响。
2)对于 TPU 的支持、Mesos 资源管理器中 GPU 的支持、以及 Windows 平台的 GPU 支持将不是这个版本的目标。
3)而且对于一张 GPU 卡内的细粒度调度也不会在这个版本支持;Spark 3.0 版本将把一张 GPU 卡和其内存作为不可分割的单元。

4、更好的API扩展(BetterAPI-Extensions-DataSourceV2)
Data Source API 定义如何从存储系统进行读写的相关 API 接口,比如 Hadoop 的
InputFormat/OutputFormat,Hive 的 Serde 等。这些 API 非常适合用户在 Spark 中使用 RDD 编程的时候使用。使用这些 API 进行编程虽然能够解决我们的问题,但是对用户来说使用成本还是挺高的,而且
Spark 也不能对其进行优化。
为了解决这些问题,Spark 1.3 版本开始引入了 Data Source API V1,通过这个 API 我们可以很方便的读取各种来源的数据,而且 Spark 使用 SQL 组件的一些优化引擎对数据源的读取进行优化,比如列裁剪、过滤下推等等。
Data Source API V1 为我们抽象了一系列的接口,使用这些接口可以实现大部分的场景。但是随着使用的用户增多,逐渐显现出一些问题:
1)部分接口还是太过于依赖SQLContext和DataFrame 2)缺乏对列式数据库存储的读取支持
3)写操作不支持事务
4)不支持流式处理,不能进行增量跌打
5)缺乏分区和排序信息
6)扩展能力有限,难以下推其它算子
为了解决 Data Source V1 的一些问题,从 Apache Spark 2.3.0 版本开始,社区引入了 Data Source API V2,在保留原有的功能之外,还解决了 Data Source API V1 存在的一些问题,比如不再依赖上层 API,扩展能力增强。虽然这个功能在 Apache Spark 2.x 版本就出现了,但是不是很稳定,所以社区对 Spark DataSource API V2 的稳定性工作以及新功能分别开了两个 ISSUE:SPARK-25186 以及 SPARK-22386。
Spark DataSource API V2 最终稳定版以及新功能将会随着年底和 Apache Spark 3.0.0 版本一起发布,其也算是Spark 3.0版本的一大新功能。
5、更好的ANSI-SQL兼容(ANSI SQL Compatible)
因为Spark原来的SQL语法和函数跟ANSI标准还是存在一些差异,因此这次版本更新将缩小和ANSI标准之 间的差异,包括增加一些ANSI SQL的函数、区分SQL保留关键字以及内置函数等。
6、SparkR 中的向量化(Vectorization in SparkR)
Spark 是从 1.4 版本开始支持 R 语言的
每当使用 R 语言和 Spark 集群进行交互,需要经过JVM ,这也就无法避免数据的序列化和反序列化操作,这在数据量很大的情况下性能是十分低下的!而Spark 已经在许多操作中进行了向量化优化
(vectorization optimization),例如,内部列式格式(columnar format)、Parquet/ORC 向量化读取、
Pandas UDFs 等。向量化可以大大提高性能。
SparkR 向量化允许用户按原样使用现有代码,但是当他们执行 R 本地函数或将 Spark DataFrame 与 R
DataFrame 互相转换时,可以将性能提高大约数千倍。这项工作可以看下 Spark-26759
可以看出,SparkR 向量化是利用 Apache Arrow,其使得系统之间数据的交互变得很高效,而且避免了数据的序列化和反序列化的消耗,所以采用了这个之后,SparkR 和 Spark 交互的性能得到极大提升。

98 . 简述Spark计算的灵活性体现在哪里 ?

试题回答参考思路:

Spark提供了不同层面的灵活性。
在实现层,它完美演绎了Scala trait动态混入(mixin)策略(如可更换的集群调度器、序列化库)。在原语(Primitive)层,它允许扩展新的数据算子(operator)、新的数据源(如HDFS之外支持
DynamoDB)、新的language bindings(Java和Python)。
在范式(Paradigm)层,Spark支持内存计算、多迭代批量处理、即席查询、流处理和图计算等多种范 式。

99 . 简述什么是 RDD 沿袭 ?

试题回答参考思路:

RDD Lineage(RDD operator graph或RDD dependency graph)是包含一个 RDD 的所有父 RDD
val r00 = sc.parallelize(0 to 9)
val r01 = sc.parallelize(0 to 90 by 10)
val r10 = r00.cartesian(r01)
val r11 = r00.map(n => (n, n))
val r12 = r00.zip(r01)
val r13 = r01.keyBy(_ / 20)
val r20 = Seq(r11, r12, r13).foldLeft(r10)(_ union _)
当我们对一个 RDD 应用不同类型的转换时,RDD 沿袭被创建,创建一个所谓的逻辑执行计划。
谱系图包含有关调用操作时需要应用的所有转换的信息。
逻辑执行计划从最早的RDD开始,到RDD结束,产生调用action的最终结果

100 . 简述解释 Spark 中的 Accumulator 共享变量 ?

试题回答参考思路:

累加器是只读的共享变量。它们仅通过关联和交换操作“添加”。它们可用于实现计数器(如在 MapReduce 中)或求和。Spark 原生支持数字类型的累加器,您还可以添加对新类型的支持。

累加器是增量变量。在节点上运行的任务可以添加到它,而驱动程序可以读取该值。在不同机器上运行的任务可以递增它们的值,并且此聚合信息可返回给驱动程序。

;