Spark面试八股文

Blog Author · 2 分钟 阅读时间
发布于 2025年12月10日

1. Spark 的运行流程?

具体运行流程如下:

  • SparkContext 向资源管理器注册并向资源管理器申请运行 Executor
  • 资源管理器分配 Executor,然后资源管理器启动 Executor
  • Executor 发送心跳至资源管理器
  • SparkContext 构建 DAG 有向无环图
  • 将 DAG 分解成 Stage(TaskSet)
  • 把 Stage 发送给 TaskScheduler
  • Executor 向 SparkContext 申请 Task
  • TaskScheduler 将 Task 发送给 Executor 运行
  • 同时 SparkContext 将应用程序代码发放给 Executor
  • Task 在 Executor 上运行,运行完毕释放所有资源
  • 2. Spark 有哪些组件?

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

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

    RDD 是 spark 提供的核心抽象,全称为弹性分布式数据集。

    RDD 在逻辑上是一个 hdfs 文件,在抽象上是一种元素集合,包含了数据。它是被分区的,分为多个分区,每个分区分布在集群中的不同结点上,从而让 RDD 中的数据可以被并行操作(分布式数据集)

    比如有个 RDD 有 90W 数据,3 个 partition,则每个分区上有 30W 数据。RDD 通常通过 Hadoop 上的文件,即 HDFS 或者 HIVE 表来创建,还可以通过应用程序中的集合来创建;RDD 最重要的特性就是容错性,可以自动从节点失败中恢复过来。即如果某个结点上的 RDD partition 因为节点故障,导致数据丢失,那么 RDD 可以通过自己的数据来源重新计算该 partition。这一切对使用者都是透明的。

    RDD 的数据默认存放在内存中,但是当内存资源不足时,spark 会自动将 RDD 数据写入磁盘。比如某结点内存只能处理 20W 数据,那么这 20W 数据就会放入内存中计算,剩下 10W 放到磁盘中。RDD 的弹性体现在于 RDD 上自动进行内存和磁盘之间权衡和切换的机制。

    4. RDD 中 reduceBykey 与 groupByKey 哪个性能好,为什么?

    reduceByKey:reduceByKey 会在结果发送至 reducer 之前会对每个 mapper 在本地进行 merge,有点类似于在 MapReduce 中的 combiner。这样做的好处在于,在 map 端进行一次 reduce 之后,数据量会大幅度减小,从而减小传输,保证 reduce 端能够更快的进行结果计算。

    groupByKey:groupByKey 会对每一个 RDD 中的 value 值进行聚合形成一个序列(Iterator),此操作发生在 reduce 端,所以势必会将所有的数据通过网络进行传输,造成不必要的浪费。同时如果数据量十分大,可能还会造成 OutOfMemoryError。

    所以在进行大量数据的 reduce 操作时候建议使用 reduceByKey。不仅可以提高速度,还可以防止使用 groupByKey 造成的内存溢出问题。

    5. 介绍一下 cogroup rdd 实现原理,你在什么场景下用过这个 rdd?

    cogroup:对多个(2~4)RDD 中的 KV 元素,每个 RDD 中相同 key 中的元素分别聚合成一个集合。

    与 reduceByKey 不同的是:reduceByKey 针对一个 RDD中相同的 key 进行合并。而 cogroup 针对多个 RDD中相同的 key 的元素进行合并。

    cogroup 的函数实现:这个实现根据要进行合并的两个 RDD 操作,生成一个 CoGroupedRDD 的实例,这个 RDD 的返回结果是把相同的 key 中两个 RDD 分别进行合并操作,最后返回的 RDD 的 value 是一个 Pair 的实例,这个实例包含两个 Iterable 的值,第一个值表示的是 RDD1 中相同 KEY 的值,第二个值表示的是 RDD2 中相同 key 的值。

    由于做 cogroup 的操作,需要通过 partitioner 进行重新分区的操作,因此,执行这个流程时,需要执行一次 shuffle 的操作(如果要进行合并的两个 RDD 的都已经是 shuffle 后的 rdd,同时他们对应的 partitioner 相同时,就不需要执行 shuffle)。

    场景:表关联查询或者处理重复的 key。

    6. 如何区分 RDD 的宽窄依赖?

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

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

    7. 为什么要设计宽窄依赖?

  • 对于窄依赖
  • 对于宽依赖
  • 8. DAG 是什么?

    DAG(Directed Acyclic Graph 有向无环图)指的是数据转换执行的过程,有方向,无闭环(其实就是 RDD 执行的流程);

    原始的 RDD 通过一系列的转换操作就形成了 DAG 有向无环图,任务执行时,可以按照 DAG 的描述,执行真正的计算(数据被操作的一个过程)。

    9. DAG 中为什么要划分 Stage?

    并行计算

    一个复杂的业务逻辑如果有 shuffle,那么就意味着前面阶段产生结果后,才能执行下一个阶段,即下一个阶段的计算要依赖上一个阶段的数据。那么我们按照 shuffle 进行划分(也就是按照宽依赖就行划分),就可以将一个 DAG 划分成多个 Stage/阶段,在同一个 Stage 中,会有多个算子操作,可以形成一个 pipeline 流水线,流水线内的多个平行的分区可以并行执行。

    10. 如何划分 DAG 的 stage?

    对于窄依赖,partition 的转换处理在 stage 中完成计算,不划分(将窄依赖尽量放在在同一个 stage 中,可以实现流水线计算)。

    对于宽依赖,由于有 shuffle 的存在,只能在父 RDD 处理完成后,才能开始接下来的计算,也就是说需要要划分 stage。

    11. DAG 划分为 Stage 的算法了解吗?

    核心算法:回溯算法

    从后往前回溯/反向解析,遇到窄依赖加入本 Stage,遇见宽依赖进行 Stage 切分。

    Spark 内核会从触发 Action 操作的那个 RDD 开始从后往前推,首先会为最后一个 RDD 创建一个 Stage,然后继续倒推,如果发现对某个 RDD 是宽依赖,那么就会将宽依赖的那个 RDD 创建一个新的 Stage,那个 RDD 就是新的 Stage 的最后一个 RDD。 然后依次类推,继续倒推,根据窄依赖或者宽依赖进行 Stage 的划分,直到所有的 RDD 全部遍历完成为止。

    具体划分算法请参考:AMP 实验室发表的论文

    12. 对于 Spark 中的数据倾斜问题你有什么好的方案?

  • 前提是定位数据倾斜,是 OOM 了,还是任务执行缓慢,看日志,看 WebUI
  • 解决方法,有多个方面:
  • 避免不必要的 shuffle,如使用广播小表的方式,将 reduce-side-join 提升为 map-side-join
  • 分拆发生数据倾斜的记录,分成几个部分进行,然后合并 join 后的结果
  • 改变并行度,可能并行度太少了,导致个别 task 数据压力大
  • 两阶段聚合,先局部聚合,再全局聚合
  • 自定义 paritioner,分散 key 的分布,使其更加均匀
  • 13. Spark 中的 OOM 问题?

  • map 类型的算子执行中内存溢出如 flatMap,mapPatitions
  • 原因:map 端过程产生大量对象导致内存溢出:这种溢出的原因是在单个 map 中产生了大量的对象导致的针对这种问题。
  • 解决方案:
  • 增加堆内内存。
  • 在不增加内存的情况下,可以减少每个 Task 处理数据量,使每个 Task 产生大量的对象时,Executor 的内存也能够装得下。具体做法可以在会产生大量对象的 map 操作之前调用 repartition 方法,分区成更小的块传入 map。
  • shuffle 后内存溢出如 join,reduceByKey,repartition。
  • shuffle 内存溢出的情况可以说都是 shuffle 后,单个文件过大导致的。在 shuffle 的使用,需要传入一个 partitioner,大部分 Spark 中的 shuffle 操作,默认的 partitioner 都是 HashPatitioner,默认值是父 RDD 中最大的分区数.这个参数 spark.default.parallelism 只对 HashPartitioner 有效.如果是别的 partitioner 导致的 shuffle 内存溢出就需要重写 partitioner 代码了.
  • driver 内存溢出
  • 用户在 Dirver 端口生成大对象,比如创建了一个大的集合数据结构。解决方案:将大对象转换成 Executor 端加载,比如调用 sc.textfile 或者评估大对象占用的内存,增加 dirver 端的内存
  • 从 Executor 端收集数据(collect)回 Dirver 端,建议将 driver 端对 collect 回来的数据所作的操作,转换成 executor 端 rdd 操作。
  • 14. Spark 中数据的位置是被谁管理的?

    每个数据分片都对应具体物理位置,数据的位置是被blockManager管理,无论数据是在磁盘,内存还是 tacyan,都是由 blockManager 管理。

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

  • 输入数据有很多 task,尤其是有很多小文件的时候,有多少个输入 block 就会有多少个 task 启动;
  • spark 中有 partition 的概念,每个 partition 都会对应一个 task,task 越多,在处理大规模数据的时候,就会越有效率。不过 task 并不是越多越好,如果平时测试,或者数据量没有那么大,则没有必要 task 数量太多。
  • 参数可以通过 spark_home/conf/spark-default.conf 配置文件设置:
  • 针对 spark sql 的 task 数量:spark.sql.shuffle.partitions=50

    非 spark sql 程序设置生效:spark.default.parallelism=10

    16. 介绍一下 join 操作优化经验?

    这道题常考,这里只是给大家一个思路,简单说下!面试之前还需做更多准备。

    join 其实常见的就分为两类: map-side join 和 reduce-side join

    当大表和小表 join 时,用 map-side join 能显著提高效率。

    将多份数据进行关联是数据处理过程中非常普遍的用法,不过在分布式计算系统中,这个问题往往会变的非常麻烦,因为框架提供的 join 操作一般会将所有数据根据 key 发送到所有的 reduce 分区中去,也就是 shuffle 的过程。造成大量的网络以及磁盘 IO 消耗,运行效率极其低下,这个过程一般被称为 reduce-side-join。

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

    在大数据量的情况下,join 是一中非常昂贵的操作,需要在 join 之前应尽可能的先缩小数据量。

    对于缩小数据量,有以下几条建议

  • 若两个 RDD 都有重复的 key,join 操作会使得数据量会急剧的扩大。所有,最好先使用 distinct 或者 combineByKey 操作来减少 key 空间或者用 cogroup 来处理重复的 key,而不是产生所有的交叉结果。在 combine 时,进行机智的分区,可以避免第二次 shuffle。
  • 如果只在一个 RDD 出现,那你将在无意中丢失你的数据。所以使用外连接会更加安全,这样你就能确保左边的 RDD 或者右边的 RDD 的数据完整性,在 join 之后再过滤数据。
  • 如果我们容易得到 RDD 的可以的有用的子集合,那么我们可以先用 filter 或者 reduce,如何在再用 join。
  • 17. Spark 与 MapReduce 的 Shuffle 的区别?

  • 相同点:都是将 mapper(Spark 里是 ShuffleMapTask)的输出进行 partition,不同的 partition 送到不同的 reducer(Spark 里 reducer 可能是下一个 stage 里的 ShuffleMapTask,也可能是 ResultTask)
  • 不同点:
  • MapReduce 默认是排序的,spark 默认不排序,除非使用 sortByKey 算子。
  • MapReduce 可以划分成 split,map()、spill、merge、shuffle、sort、reduce()等阶段,spark 没有明显的阶段划分,只有不同的 stage 和算子操作。
  • MR 落盘,Spark 不落盘,spark 可以解决 mr 落盘导致效率低下的问题。
  • Share: