Spark这门课期末复习,很多人刚开始都会懵:知识点太多太散,RDD、DAG、shuffle、部署、调优、SQL……好像每章都考过,又好像每章都没学透。我作为带过几届课程设计、也帮不少同学临考突击的人,把平时踩过的坑、课上反复强调的重点,加上你们最关心的那些热搜问题——Spark on YARN 为什么只有1个核、Spark 内存和 OOM、Spark 读取 Redis、数据倾斜和面试题——集中整理成一篇可以照着看的复习总结。这份内容不敢说覆盖所有学校所有考点,但至少把 Spark 课程里最核心的脉络理顺,让大家背得不那么痛苦,理解得深一点,考试时能写出真正有用的答案。
期末考试不是靠死记硬背通过的,尤其是 Spark 这种工程属性很强的课程。很多时候老师出的题并不偏,无非是把部署、调度、内存、RDD 血缘、SQL 优化这些基础概念揉到实际场景里,问你怎么排查、怎么设计、怎么选方案。下面我按“知识体系 → RDD与调度核心 → 部署与资源 → 内存与OOM → 典型案例与外部存储 → 考点问答与答题模板 → 复习与上机建议”的顺序来写。
1. 抓住 Spark 的主线,后面复习就不乱了
1.1 这门课的知识地图,先分清楚四大块
期末复习最忌讳一上来就翻书。Spark 的内容可以分成四条主线,基本上所有题目都能归到这四条线里面:
第一块是编程模型与核心抽象,即 RDD、DataFrame、Dataset 三套 API,以及 transformation 和 action 的区别。期末考试特别喜欢考 RDD 的五大特性、依赖关系、分区和并行度,还会穿插 Scala 代码补充题。
第二块是调度与执行引擎,包括 DAG 如何生成、Stage 怎么划分、宽依赖和窄依赖、shuffle 过程、task 如何分发到 executor。这一块出题通常给一个复杂算子序列,让你画出 Stage 划分图或指出发生了几次 shuffle。
第三块是平台部署与资源管理,比如 Standalone、YARN、Local 几种运行模式的区别,Spark 提交作业时有哪些参数控制 CPU、内存、实例数。从往年题目角度,这块最容易跟“spark on YARN CPU 只能用一个”这种热点排查题结合。
第四块是运行期调优与常见故障,重点是内存模型、OOM 产生的原因和排查路径、数据倾斜的处理方式,以及 Spark SQL 中 Catalyst 优化和常用调优参数。很多学校还会考 Spark 读写外部系统,比如从 Redis 读取数据、将结果写回 Redis。
这四个模块对应的考点权重并不一样,但如果你能按主线去复习,后面所有知识点都能串成一条线:用户代码先被转换成 RDD/DataFrame 的依赖图,再被 SparkContext/DAGScheduler 切成 Stage,然后由 TaskScheduler 分配到 executor 里的多个 task 并行执行,执行过程中由于结果过大、数据倾斜或资源不足产生各种问题,最后通过参数调整和代码重构解决。
1.2 复习策略上,比“看完”更重要的是“能画图”
建议复习时随手画三张图:
- 第一张是 Spark 架构图:Driver、SparkContext、DAGScheduler、TaskScheduler、Executor、Task、BlockManager 分别负责什么。
- 第二张是 RDD 依赖与 Stage 划分图:给定一段 transform 链,能标出哪些是窄依赖、哪些是宽依赖,会在哪里拆 Stage。
- 第三张是内存与资源关系图:executor 的堆内内存各部分如何划分,storage 和 execution 内存如何互相借用,vCore 和 task 之间如何对应。
这三张图能画出来,比起把定义背十遍要有效得多。下面我就按这个主线展开,把高频重点和常被忽略的细节揉进去。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从最常考的 RDD 和 DAG 开始复盘
2.1 RDD 到底是什么,为什么说它是“血统”驱动的数据抽象
RDD(Resilient Distributed Dataset)的教材定义是“弹性分布式数据集”,但考试时如果只写这一句,往往只能拿到一半分。关键在于把四个字拆开讲明白:
- “分布式”意味着数据被切分成多个 partition,每个 partition 可能存放在不同节点上。
- “数据集”意味着它本身不存数据,只存“如何从父 RDD 计算得到自己”的元信息,也就是依赖和分区函数。
- “弹性”意味着当某个 partition 丢失时,可以根据 lineage(血统)重新计算,不需要像传统分布式系统那样必须做副本备份。
- “只读”和“不可变”也是常考的隐藏属性,RDD 一旦生成,不能修改,只能通过 transformation 生成新的 RDD。
RDD 的五大特性在选择题和简答题中都极容易出现,我总结成一句记忆方式:分区、函数、依赖、分区器、首选位置。前两个保证了计算模型完整,第三个决定了容错方式,第四个和第五个影响执行性能和本地性。
有个小的记忆技巧:先记关键词“一个列表 + 一个函数”,然后从“如何切数据、如何算数据、数据丢了怎么恢复、同一个 key 怎么分区、优先在哪个节点算”这五个问题去反问自己。面试题里最喜欢问“RDD 为什么是弹性的?”,这时候要答的不是“有 lineage”,而是一整套恢复机制:窄依赖直接重算父 partition,宽依赖要保留 shuffle 中间文件以便恢复,checkpoint 可以把长血统截断,避免恢复代价过大。
2.2 宽窄依赖和 Stage 划分:DAG 为什么会在 shuffle 处断一刀
记住一个结论:几乎所有 Stage 划分的题目,只要找到算子链上的 shuffle,答案就出来一半了。
窄依赖是指父 RDD 的每个 partition 最多只被子 RDD 的一个 partition 使用,典型算子有 map、filter、union、coalesce(不产生 shuffle 时)。窄依赖不需要跨节点拉数据,多个算子可以在同一个 Stage 内流水线执行,因为父分区的数据在本地就能算完传给下一个算子。
宽依赖是指父 RDD 的一个 partition 会被子 RDD 的多个 partition 使用,也就是多对多的关系,典型算子有 groupByKey、reduceByKey、join(未优化时)、distinct、repartition。宽依赖必然产生 shuffle,也就是说数据要被拉到下游对应分区所在的节点上去。
DAGScheduler 在遇到宽依赖时会切出一个新的 Stage,上游 Stage 的数据先写到本地磁盘或内存(实际是 shuffle write),下游 Stage 再去拉取(shuffle read)。从考试层面,最经典的题目是:
text复制rdd.map(...).filter(...).reduceByKey(...).map(...).saveAsTextFile(...)
这段代码有几个 Stage?不严谨但常考的快速判断:第一次出现 reduceByKey 前是 Stage0,reduceByKey 后到 action 前是 Stage1,所以是 2 个 Stage。注意如果中间还有 repartition、join 等算子,则需要再切一刀。平时复习可以把 Stage 数量看作“shuffle 次数 + 1”。
2.3 从 RDD 到 DataFrame/Dataset:Catalyst 和钨丝计划
这里有一个很常见的理解误区:RDD 是 Spark 的“底层基础”,但不代表 DataFrame 只是做了 API 封装。DataFrame 引入了 Schema,Spark SQL 中的 Catalyst 优化器可以看到你的计算逻辑,并对它们做谓词下推、列剪枝、常量折叠等优化。RDD 内部看不到结构,stage 之间没有任何自动优化空间。
课程实验里经常遇到“同样一份日志统计,用 RDD 写跑很久,换成 DataFrame + Spark SQL 快很多”的现象,原因就在这。
期末题目如果问到“为什么 Spark SQL 比直接写 RDD 算子更高效”,建议答三方面:
- Catalyst 可以对逻辑计划做优化,比如把过滤条件下推到数据源端,减少读取量。
- Tungsten 使用堆外内存和二进制行存储,减少 Java 对象开销。
- 当数据能被 DataFrame 表示时,可以利用更高效的编码与列式存储。
Dataset 则是在类型安全这一点上优于 DataFrame,它是强类型的,编译期就能发现字段名错误;而 DataFrame 行字段错误通常要运行期才报错。这一块不必抠太深,但三个核心类以及“DataFrame = Dataset[Row]”这个关系,属于 Spark 课程考试的高频送分点。
2.4 算子归纳,别等代码题丢分了再背
transformation 是惰性的,action 会触发真正的计算。很多同学会背概念,但代码题里一出现 rdd.map() 和 rdd.collect() 就判断错了返回值类型,非常可惜。
我建议把常见的算子分组记忆:
- 单 value 类型:map、flatMap、filter、mapPartitions、sample、union、distinct、groupBy、coalesce、repartition。
- 键值对类型:reduceByKey、groupByKey、aggregateByKey、foldByKey、sortByKey、mapValues、join、cogroup。
- action 类型:collect、count、first、take、reduce、foreach、saveAsTextFile、countByKey。
- DataFrame/Dataset 常用:select、filter、groupBy、agg、join、withColumn、dropDuplicates、orderBy、limit。
高频考题之一是 reduceByKey 与 groupByKey 的区别。二者都是宽依赖,但 reduceByKey 会在每个分区内先做一次本地聚合,shuffle 传输的数据量大大减少;groupByKey 则是把所有原始 value 全部传到下游,网络开销大。如果场景是求和或统计,优先用 reduceByKey、aggregateByKey,不推荐 groupByKey 后接 map 求和。
同理,coalesce 和 repartition 也是常客:repartition 会产生 shuffle,coalesce 默认不产生 shuffle,一般用于减少分区。如果 coalesce 从少分区增加到多分区,则必须把 shuffle 参数置为 true 才会生效。
3. 部署、提交与资源分配:为什么配置对了一切才会跑得快
3.1 从 Local 到集群,三种运行模式别只背名字
期末实验第一关就是安装和启动 Spark。最直接的方式是用 Local 模式验证 API 对不对,把 conf 里的 master 设为 local[*]。Local 模式一个 JVM 进程里同时跑 Driver 和 Executor,主要用于调试和课程小实验。
考试和实际部署常考的是 Standalone 与 YARN。Standalone 是 Spark 自己提供的资源调度框架,需要启动 Master 和 Worker 进程,客户端通过 spark://master-host:7077 连接。YARN 则是把 Spark 作业交给 Hadoop 的 ResourceManager 管理,优点是集群资源可被多个计算框架共享。
从部署步骤来看,比较常规的过程是:
- 下载对应 Hadoop 版本的 Spark 二进制包,配置
JAVA_HOME与SPARK_HOME。 - 在
spark-env.sh中设置 Java 路径和 Master/Worker 的内存与核数。 - 配置
workers文件,列出所有 Worker 节点。 - 启动集群,用
jps查看 Master、Worker 进程是否正常。 - 如果运行在 YARN 上,还需要设置
HADOOP_CONF_DIR或YARN_CONF_DIR,确保 Spark 能读到 YARN 的资源配置。 - 用 spark-submit 提交测试程序,到 Web UI 或 YARN ResourceManager 页面观察任务运行状态。
经常有同学在一个问题上浪费很久:明明 Standalone 模式能跑,YARN 模式一提交就报连接不到 RM 或 class not found。这类问题通常是环境变量没配齐,或者提交的 jar 里没把依赖打全。检查顺序建议是:ResourceManager 是否正常启动 → YARN 页面能否看到 application → Spark 日志里报错具体是什么 → 是否缺依赖包。
3.2 YARN 上 executor 只用 1 个 vCore?这个热搜问题背后是参数
“spark on yarn cpu只能用1个”可以说是我被问过最多的问题。实际情况通常是:在 YARN 的调度页面上看到每个 Executor 的 vCore 数只有 1,整个过程并行度很低。
先解释一下这个现象背后的参数链。Spark on YARN 模式下,向 ResourceManager 申请 Executor 的 CPU 数量默认由 spark.executor.cores 决定。在 Spark 3.x 中,默认值本质上就是 1,也就是说如果你不显式配置,每个 Executor 只申请 1 个 vCore。每个 task 默认使用 spark.task.cpus 个核,默认也是 1。因此一个 Executor 只有一个 vCore 的时候,同一时刻该 Executor 一般只能运行 1 个 task,自然快不起来。
如果显式设置了 --executor-cores 2 或 spark.executor.cores=2,发现页面上依然是 1,这个时候要从 YARN 侧查:
yarn.scheduler.maximum-allocation-vcores是否限制过小,导致单个容器申请多核被压回。yarn.nodemanager.resource.cpu-vcores是否配置正确,看起来是 8 核或 16 核,但配置成 1。- 多个提交命令之间参数是否被覆盖,比如代码里硬编码了 SparkConf,把提交命令参数覆盖掉。
- 是否在动态资源分配场景下,Executor 刚启动时只分配了最小核数,后续才逐渐增加。
课堂上我会着重强调一个点:vCore 是 YARN 里的抽象 CPU 概念,一个物理核在有超线程时可以对应多个 vCore,所以“申请多个 vCore”不等于“一定能获得整颗物理核的独占性能”,但至少决定了 Spark task 可以并发运行的规模。
这里给出一个偏生产环境的提交样例,课程设计可以参考:
bash复制spark-submit \
--class com.example.WordCount \
--master yarn \
--deploy-mode cluster \
--num-executors 4 \
--executor-cores 2 \
--executor-memory 4g \
--driver-memory 2g \
--conf spark.default.parallelism=8 \
--conf spark.shuffle.partitions=8 \
spark-demo.jar
注意 num-executors、executor-cores、executor-memory 三个参数要配合任务总资源和单节点规格来定。一般经验是 executor-cores 不要超过节点物理核数的一半,否则容易出现 CPU 争抢;executor-memory 要留出系统页缓存和 YARN 自身 overhead 的空间,spark.executor.memoryOverhead 在 YARN 中默认是 executor 内存的 10%,最小 384MB,申请内存时资源管理器会把它算进去。
3.3 集群节点规划与常见部署坑
课程测验里偶尔会出现“给你 3 台 16 核 64G 的机器,你怎么规划 Spark 集群”这类开放题。规划时至少要考虑这几个点:
- 一台机器建议同时跑 Master 和 Worker 吗?如果是课程集群,可以;生产环境更倾向于 Master 单独部署或走 HA。
- 每个 Worker 能启动多少个 Executor?这不取决于 Worker 的数量,而是取决于 Executor 的内存和核数配置以及该节点资源总量。
- Driver 部署在哪里?Client 模式下 Driver 在提交作业的客户端,Cluster 模式下 Driver 在某个 Container 里。期末实验通常用 Client 模式方便看日志,但作业在 YARN 容器里跑时日志查看方式完全不同。
部署坑点方面,最常见的是防火墙没关导致节点间无法通信、hostname 没有按集群配置解析、worker 节点没有同步安装相同版本的 JDK 或 Python 环境。别看这些是基础问题,真到考试前的上机会要求你自己搭集群跑一个简单任务时,很多同学就是卡在这里。
4. Spark 内存模型与 OOM 排查:不要只会说“调大内存”
4.1 内存模型详解:300MB、0.6、0.5 这些数字到底怎么用
期末必考、工作面试也必考的核心概念就是 Spark 内存管理。Spark 1.6+ 使用统一内存管理器,Executor 的 JVM 堆内存大致被切成了三块:
- Reserved Memory:系统预留内存,默认 300MB,用于保存 Spark 内部对象,基本不可配置。
- User Memory:用于存储用户数据结构、RDD 算子的自定义对象等,默认占可用堆内存的 40%。
- Spark Memory:统一管理 storage 和 execution 的内存,默认占可用堆内存的 60%。
这里默认比例可以通过 spark.memory.fraction 调整,默认 0.6;其中 storage 和 execution 的初始比例由 spark.memory.storageFraction 控制,默认 0.5,也就是说 Spark Memory 内部初看是 50% 给 storage、50% 给 execution。
需要理解的核心机制是“统一”二字:Execution 内存不足时可以借用 Storage 内存,Storage 内存不足时也可以反过来借用 Execution 内存。唯一的约束是 Storage 被借走并需要释放缓存时,如果对方正在使用,只能等对方完成或通过 LRU 驱逐部分 RDD 缓存块。这也是为什么一个频繁执行 shuffle、join 的作业会把之前 cache 的 RDD 冲掉,导致 cache 命中率下降。
除开堆内,spark.memory.offHeap.enabled 和 spark.memory.offHeap.size 可以配置堆外内存,但默认关闭。Tungsten 使用堆外内存存储二进制数据能降低 GC 压力,但堆外内存使用过猛容易造成物理内存超卖,需要小心。
4.2 OOM 不是只有一种:把位置和原因对上号
Spark OOM 是我在实验和面试中见过最多的问题版块。一提起大作业跑挂了,第一反应永远是“内存不够,调大点”,但真正高效的做法是先分清 OOM 到底发生在哪一端:
- Driver 端 OOM。常见原因是你执行了
collect、take等把全量数据拉到 Driver 的操作,或者广播变量大到 Driver 发送有压力。解决思路是尽量不要 collect 大数据,改用分区聚合后抽样统计;必须物化结果时考虑写到外部存储而不是回 Driver。 - Executor 堆内 OOM。原因可以是数据倾斜导致单个 executor 的某个 task 处理数据量过大,也可以是单条记录太大、shuffle 拉取数据过多,还可以是缓存了太多 RDD 分区。此时 Java heap 会抛出
OutOfMemoryError: Java heap space。 - Executor 堆外 OOM。YARN 模式下容器可能因为
spark.executor.memoryOverhead不足被 NodeManager 杀,日志里出现Container killed by YARN for exceeding memory limits,这种情况不能只调executor-memory,还要调 overhead。 - 直接内存不足。使用 NIO、Netty 或堆外内存时,可能出现
Direct buffer memory,需要关注spark.executor.extraJavaOptions里的-XX:MaxDirectMemorySize。
考试题经常给一段日志,让你判断是哪种 OOM。判断口诀是:
text复制Container killed by YARN -> 看内存总开销,不只是堆内
Java heap space -> 看堆内对象和缓存
Direct buffer memory -> 看堆外
Driver stack trace 里有 collect -> 看 Driver 端数据回收策略
4.3 常见调优思路与实际排障顺序
如果题目问“怎么解决 Executor OOM”,不建议只写调大内存。满分回答通常分步:
- 在 Spark UI 的 Stages 页面上,看有没有某个 Stage 的 Input/Shuffle Read 远大于其他 Stage。
- 如果存在数据倾斜,先用
sample算子抽样统计 key 分布,确认倾斜的 key。 - 对倾斜 key 加盐拆分,或用两阶段聚合,即先局部聚合再加随机前缀后的 key 做全局聚合。
- 如果是因为 Shuffle 数据量大,考虑增大分区数或开启
spark.shuffle.consolidateFiles等参数(注意不同版本参数差异)。 - 最后才去调整
--executor-memory和spark.memory.fraction。
真实排障中我还遇到过一类诡异问题:代码逻辑里某一步对大数据集做了笛卡尔积,比如 rdd1.cartesian(rdd2),导致内存瞬间被打爆。这种问题调大内存根本解决不了,只能从算法层面换 join 方案或过滤数据。排查时先看 Spark UI 上 Stage 描述和 SQL 计划,往往比闷头调参更快。
4.4 如何让 RDD 缓存真正帮你提速
Spark 课程的实验报告里,很多同学写“程序先用 cache 缓存,再多次 action”。但问起为什么快,却说不出来。需要清楚三点:
- cache 实际上是
persist(StorageLevel.MEMORY_ONLY)的简写,并非单独机制。 - 缓存的作用是在多个 action 或后续 Stage 重复使用同一份 RDD 时,避免从头根据 lineage 重新计算。
- 如果数据量超过内存,
MEMORY_ONLY可能生成部分分区丢失,需要重算;改用MEMORY_AND_DISK可以把放不下的分区溢写到磁盘。
最典型的应用是迭代式算法,比如 KMeans、PageRank 这类需要每轮迭代反复扫描同一份数据的作业。第一次迭代后把训练集或邻接表 cache 到内存中,后续每轮迭代不再从 HDFS 重新读取。这里有一个非常容易被忽略的细节:如果 RDD 的 lineage 很长,或者某个中间结果需要长期复用,建议用 checkpoint 截断血统。checkpoint 会把数据真正写到可靠存储(通常是 HDFS)上,和 cache 是两回事。
5. 一个把 SQL、Redis、实际问题串起来的案例分析
5.1 场景定位:课程设计和面试都喜欢出的统计需求
我先说一个很贴近 Spark 课程的典型题目:有一份订单明细数据,至少包含订单 ID、用户 ID、商品类目 ID、下单金额、下单时间,要求统计所有一级类目的实时销量 Top 10,并把结果写入 Redis 供上层服务读取。
这类题有两个目的:一是考查 Spark SQL 基础能力,二是考查结果如何下沉到 Redis 等外部系统。有些人只写 Spark SQL 统计,最后用 collect 把 Top10 打印到控制台,看起来“功能通了”,但工程上完全不能接受。
5.2 数据读取与处理链路
如果是课程实验,数据通常来自本地文件、HDFS 或达梦等数据库。以读 CSV 为例:
scala复制val df = spark.read
.option("header", "true")
.option("inferSchema", "true")
.csv("/data/orders.csv")
val top10 = df
.groupBy("category")
.agg(sum("amount").as("gmv"))
.orderBy($"gmv".desc)
.limit(10)
但只写到这里并不够。orderBy 在 Spark SQL 中通常会产生全局排序,数据量大时可以考虑先用窗口函数按类目分区排序,或使用近似 TopN 方案,减少 shuffle 数据量。对于“Top 10”这种需求,在 DataFrame 上用 sortWithinPartitions 并不等价于全局 TopN,还是要小心结果是否正确。
如果课程难度高一点,还会要求从 Redis 中读取商品类目的映射关系,再关联到订单明细做统计。这里涉及另一个高频热词:Spark 读取 Redis。
5.3 Spark 读取 Redis:关键在连接如何分配给 Executor
很多新手习惯在 Driver 端创建 Redis 连接,然后传给 map 闭包。这样通常会报 NotSerializableException,因为 Jedis 客户端或连接池对象没有被序列化。即便部分情况下任务提交成功了,也可能演变成所有 executor 共享一个 Driver 端连接、并发极低。
正确做法一般有两种:
- 自写一个可序列化的 RedisSink 类,在类内部进行连接池初始化,通过
mapPartitions或foreachPartition在每个 executor 内创建连接。 - 使用专门的 Spark-Redis 连接器,比如 Redis 官方提供的
spark-redis,配置好 Redis 地址后,可以用spark.read.format("org.apache.spark.sql.redis")读取数据。
我要提醒的是,哪怕用连接器,也要先搞清楚数据在 Redis 中是什么样的存储结构。Redis 的 list、hash、set、string 是不同的数据模型,Spark 读回来得到的 DataFrame 结构完全不同。Hash 类型通常被视为一行记录的多个字段,而 String 类型可能只是一个 key-value 对。
对课程作业而言,我建议把 Redis 连接封装成一个类似这样的套路:每个 executor 内维护一个 JedisPool,foreachPartition 中拿到分区迭代器后,批量写或批量读,再在操作完成后归还连接。这既能让代码跑通,也能在课程报告中讲清楚“为什么不能直接在 map 里 new Jedis”,属于加分项。
5.4 一个从数据倾斜到写的完整思考
假如订单数据中“数码类目”的数据量特别大,groupBy("category") 统计时可能出现个别 executor 处理的数据量远超其他 executor。处理技巧上可以考虑加盐拆分:先给数量大的 key 加一个随机后缀,把一个大 key 拆成多个子 key 做第一次聚合,再去掉后缀做第二次聚合。
写完统计结果后,如果是要缓存到 Redis 给线上接口调用,需要注意两点:一是批量写入时不要把 Top 10 结果一条一条地用单连接同步写,时间开销太大;二是写过期时间,防止数据长期不更新。这里就不再只是 Spark 本身的问题,而是“Spark 写 Redis”的工程经验了。
6. 面试与考试高频问题,背下来不如理解透
6.1 高频概念题与简答思路
结合热搜词和课程大纲,我整理了一份“被反复追问”的清单。这里面重点不仅在于答案,更在于答题结构,考试时最好先给结论,再解释机制,最后给一段简单示例。
-
Spark 相比 MapReduce 快在哪里?
答:1)基于内存计算,中间结果优先放内存,不是每次都落盘;2)DAG 调度能减少无效的 MapReduce 阶段同步开销,窄依赖算子可在一个 Stage 内流水线执行;3)Task 启动比 MR 作业轻量,没有每次都要读取磁盘和串行化的大流程;4)Tungsten 和 Catalyst 让 SQL 作业能利用二进制内存和优化后的执行计划。 -
RDD、DataFrame、Dataset 的区别?
答:RDD 偏向底层函数式编程,无 Schema;DataFrame 引入了 Schema 和 Catalyst 优化,API 以表达式和 SQL 为主,类型不安全;Dataset 在 DataFrame 基础上提供强类型编码。运行效率上,DataFrame/Dataset 使用更高效的内存结构,通常优于手工优化不足的 RDD 代码。 -
为什么 reduceByKey 比 groupByKey 高效?
答:因为 reduceByKey 会在 shuffle 之前对每个分区内相同 key 先做一次聚合,减少跨节点传输的数据量。groupByKey 不会做预聚合,会把所有 key-value 全部传到下游。 -
Spark 作业提交后执行流程?
答:spark-submit 启动 Driver,Driver 内 SparkContext 构建 DAG;DAGScheduler 划分 Stage,TaskScheduler 将 task 分发到 Executor;每个 Executor 中的 ExecutorBackend 启动 TaskRunner 线程池执行任务;任务完成后 Driver 汇总状态并退出。这里最好能画出 DAGScheduler、TaskScheduler、Executor 三者协作关系。 -
什么是 shuffle?为什么 shuffle 代价大?
答:shuffle 是数据在多个分区之间重新布局的过程,父 RDD 的分区数据要按 key 写到下游分区对应节点。代价大的原因有磁盘 IO、网络传输、序列化开销、可能发生的溢写和排序。宽依赖往往伴随这个过程。
每个知识点答完后如果能举一个例子,会比只背定义更有说服力。期末答题时,阅卷老师拿到的大多是不够细的大白话,你如果能把机制链条写完整,分数差距就这样拉开了。
6.2 “说出五个调优参数”这类题怎么准备
调优参数题属于考得快、忘得快的内容。我建议不要试图背几十个参数,而是围绕“并行度、内存、shuffle、动态分配、推测执行”几个维度各记两三个:
- 并行度相关:
spark.default.parallelism、spark.sql.shuffle.partitions。 - 内存相关:
spark.executor.memory、spark.memory.fraction、spark.memory.storageFraction。 - shuffle 相关:
spark.shuffle.compress、spark.shuffle.file.buffer、spark.reducer.maxSizeInFlight。 - 动态分配:
spark.dynamicAllocation.enabled、spark.dynamicAllocation.minExecutors。 - 推测执行:
spark.speculation。
这种题目最好有一句“为什么”,比如开启推测执行在个别节点很慢或故障时,Spark 会重新调度相同任务的副本到其他节点,但会增加集群负载,所以生产中要根据集群稳定性决定是否开启。
我个人观察:老师在期末试卷中并不会问特别偏的参数,更多是给一个“任务跑得慢、时不时 OOM”场景,让你挑出可能的原因或合理的调整方案。这时候能答出“先查看 Spark UI 各 Stage 耗时和数据倾斜情况,再决定加资源还是改代码”才是得分关键。
7. 复习收尾:动手跑通一个小任务,比刷十套题更有效
有些同学背得很熟,一到上机或遇到日志报错就心慌。Spark 课程跟纯理论课的区别是,很多知识点是“只有亲手踩一次才能记住”。比如我在第 3 节讲的 YARN 只用 1 个核的问题,第一次遇到时我也惊讶了很久;后来才意识到 Spark 默认配置里 executor.cores 并不像直觉中那么智能。再比如 Spark 读取 Redis 时 Jedis 连接不能序列化的问题,不跑一遍代码很难体会 Spark 分布式执行和普通单机程序截然不同的编程约束。
所以无论距离期末还有多久,我希望你至少完整跑过一个任务:用 Spark 读取一份文件或一张表,做一次分组聚合,将结果写到外部存储。在跑的过程中有意识地去观察 Web UI 的 Executor 页面、Stage 列表和内存情况。任务结束后回头想想:这个作业一共启动了多个 Executor?每个 Executor 多少个核和多少内存?Shuffle Read 有多大?哪个 Stage 最慢?如果数据量增加十倍,哪些参数最需要改?把这些问题吃透,试卷上任何和 Spark 资源、任务调度、内存故障相关的题目都不会再是难题。
