做 Spark 练习题的尴尬我太熟悉了:题库刷了一堆,答案对完好像都会,一上真环境或者被问一句“为什么这么写”,立刻卡壳。这套 Spark 练习测试题(答案仅供参考)就是针对这个痛点整理的,不考单纯的背诵,而是把进阶 Spark 时最容易混淆、最容易踩坑的知识点,揉进十几道判断、代码分析和场景设计题里。无论是初学者自测、准备面试,还是带新人做入职摸底,都能直接用。闭卷做一遍大概 45 分钟,做完照着解析复盘,比闷头刷二十套题更有用。
题目按 Spark Core、Spark SQL、流处理、调优排障四个方向展开,每一道都给了参考答案和解析。需要提醒一下,Spark 版本迭代很快,任何“标准答案”都可能被新特性推翻,所以遇到与你的版本行为不一致的地方,以官方文档和实际执行结果为准。
1. Spark 练习题到底在测什么
1.1 出题视角:为什么不是单纯考概念
市面上很多 Spark 题目是“RDD 有哪些特点”“说说宽窄依赖”这类概念题,背住了就能答,但背住不代表会用。我整理这套题时有一个明确原则:每个知识点都要落在一个能实际执行的场景里。因为 Spark 的难点从来不在 API 记没记住,而在执行模型清不清楚。
比如reduceByKey和groupByKey,概念上都知道一个做了 map 端合并、一个没做。但放到一个 wordcount 任务里,哪个对磁盘 IO 影响更大?为什么集群里跑同样的逻辑,一个几十秒跑完,另一个要几分钟?大多数人只记得“groupByKey 慢”,说不出慢在哪个环节,更解释不了 shuffle 数据量怎么估算。这类题才是真正能区分“背过”和“理解”的题。
所以这套题里,概念题我会故意加一个执行层面的追问,代码分析题则要求你推演 stage 划分和结果。做题时如果发现自己只能写出结论、写不出推导过程,那这个知识点大概率还是夹生的,建议回头对着 Spark UI 里的 DAG 图再走一遍。
1.2 题量与难度分布参考
整套题建议按以下方式使用:
- 题目数量:共 12 道,覆盖 Core、SQL、Streaming、调优四个方向。
- 难度分布:基础题约 6 道,场景题约 4 道,开放设计题约 2 道。
- 建议时长:闭卷 45 分钟,开放题写思路即可,不需要完整代码。
- 适用人群:学完 Spark 想自测的学习者、准备技术面试的开发者、需要给新人出摸底题的团队。
为什么这样分配?Spark 知识体系非常庞杂,但真正高频使用的只有核心执行模型、SQL 优化、流处理语义和资源调优。覆盖四个方向,是为了避免“只懂离线批处理、一问流处理就空白”的偏科。基础题占一半,是因为很多高级问题是基础概念的组合,基础不牢,后面分析再多也像空中楼阁。
2. Spark 核心考点练习题与参考答案
2.1 Spark Core 算子与执行模型题
题目 1:reduceByKey和groupByKey在 wordcount 场景下的执行过程有何区别?哪个更推荐?
参考答案:
groupByKey会把所有键值对先通过网络全部 shuffle 到下游分区,再在 reduce 端完成分组和聚合。reduceByKey在 shuffle 之前,会在 map 端对相同 key 先做一次本地 combine,只把合并后的中间结果发到下游,再在下游做最终合并。
在 wordcount 这种“按 key 聚合”的场景里,reduceByKey明显更优。它减少了网络传输和 reduce 端的内存压力,尤其是输入数据中存在大量重复 key 时,收益非常明显。但要注意,reduceByKey的 map 端合并依赖于函数的结合律和交换律;如果聚合逻辑不满足这个条件,就不能简单套用。至于groupByKey,并不是完全没有用,当你需要保留每组完整元素列表,而不是做聚合时,就必须用它。
解析与易错点:
- 很多人只答“groupByKey 慢”,但说不出慢在 shuffle 数据量放大。如果一条数据 1KB,重复 key 有 100 条,
groupByKey会把 100KB 全传过去,reduceByKey可能只传 1KB。 - 如果 key 分布严重倾斜,
reduceByKey也扛不住,因为热点 key 所在的单个分区仍然可能爆掉。这时候要先做倾斜处理,而不是无脑换成groupByKey。 - 在新手阶段,判断用哪个算子有一个简单标准:目的是聚合出一个值,优先查有没有对应的
*ByKey算子;目的是收集集合,才考虑groupByKey。
题目 2:执行下面这段代码,collect的结果是什么?整个作业有几个 stage?
val rdd = sc.parallelize(1 to 10, 2) val result = rdd.map(x => (x % 2, x)).reduceByKey(_ + _) result.collect()参考答案:
结果是一个(Key, Sum)形式的数组,内容大致为Array((0, 30), (1, 25))。具体顺序不保证。计算过程很简单:偶数 remainder 为 0,求和是 2 + 4 + 6 + 8 + 10 = 30;奇数 remainder 为 1,求和是 1 + 3 + 5 + 7 + 9 = 25。
reduceByKey会产生 shuffle,因此整个作业被分成两个 stage。第一个 stage 执行的是初始 RDD 的创建、map 转换,以及reduceByKey在 map 端的本地合并;第二个 stage 执行的是下游分区合并结果,然后触发collect输出。
解析与易错点:
- 很多初学者以为
collect是转换算子,实际上它是行动算子。只有遇到行动算子,Spark 才会真正提交作业。 parallelize(1 to 10, 2)表示把数据分成 2 个分区,但分区数量和最终 shuffle 后的分区数量不是一回事。shuffle 后的分区数由spark.default.parallelism或reduceByKey的第二个参数决定。- 如果把
reduceByKey换成groupByKey,stage 划分是一样的,但 shuffle 数据量会明显变大。通过 Spark UI 可以直观看到这种差异,这也是建议你实际跑一遍的原因。
题目 3:cache、persist和checkpoint有什么区别?一个会被多个 job 复用的中间 RDD,你会怎么选?
参考答案:
cache是persist的一种特例,默认把数据缓存在内存,存储级别是MEMORY_ONLY,执行依赖于某个 action 触发。persist可以指定更多存储级别,比如内存加磁盘、副本数、序列化方式。checkpoint会把 RDD 的数据真正写到可靠存储里,并截断血缘关系。
对于“多个 job 复用同一份中间结果”的场景,我的优先级是:数据量能放进内存且血缘不长,用cache;血缘特别长、失败重算代价高,用checkpoint;数据量超出内存,但希望尽量快,用persist(MEMORY_AND_DISK)。checkpoint不是缓存的替代品,它更强调的是容错和血缘优化。
解析与易错点:
cache是惰性的,不会在调用cache()那一刻立刻缓存数据。很多人写了个rdd.cache()就以为缓存好了,结果第一个 action 还是从源头开始算。checkpoint会触发一次额外计算,因为它需要先把数据算出来再写存储。如果某个结果本来就要算很久,checkpoint的开销也要算进去。- 使用
checkpoint前通常要设置一个可靠的 checkpoint 目录,否则作业会直接报错。
2.2 Spark SQL 与 DataFrame 设计题
题目 4:DataFrame 相比 RDD,核心优化点在哪里?
参考答案:
DataFrame 携带了 schema 信息,Spark 能基于它做逻辑计划和物理计划的优化。Catalyst 优化器负责谓词下推、列剪枝、常量折叠、join 重排序等优化;Tungsten 则用更高效的内存布局和代码生成来减少 JVM 对象开销。RDD 里数据是 JVM 对象的黑盒,Spark 不知道里面是什么结构,自然没法做这些深度优化。
解析与易错点:
- 不要回答“DataFrame 比 RDD 快”就结束,要能说出快在优化器、内存布局和代码生成三个层面。
- 反过来也要明白,DataFrame 不是万能钥匙。涉及极其复杂的自定义逻辑、需要操作非结构化数据或逐条精细控制时,RDD API 反而更灵活。
- 实际项目中,可以通过
df.explain(true)查看执行计划,观察谓词下推和列剪枝有没有生效,这是排查 SQL 性能问题最直接的手段。
题目 5:假设有两张表,订单表orders(order_id, user_id, amount, ts)和用户表users(user_id, city)。统计 2024 年各城市的订单总额和订单数,按总额降序保留前 5 个城市。
参考答案:
SELECT u.city, COUNT(*) AS order_cnt, SUM(o.amount) AS total_amount FROM orders o JOIN users u ON o.user_id = u.user_id WHERE o.ts >= '2024-01-01' AND o.ts < '2025-01-01' GROUP BY u.city ORDER BY total_amount DESC LIMIT 5;如果用 DataFrame API,类似这样:
orders.filter(col("ts").between("2024-01-01", "2025-01-01")) .join(users, Seq("user_id"), "inner") .groupBy("city") .agg(count("*").as("order_cnt"), sum("amount").as("total_amount")) .orderBy(desc("total_amount")) .limit(5)解析与易错点:
- 时间过滤条件不要写成
YEAR(ts) = 2024,这类函数包裹列的表达会让分区裁剪和谓词下推失效。直接用时间范围比较,扫描数据量会小很多。 - 订单量大的场景,join 的 key 是
user_id,如果某些用户订单量特别大,会产生数据倾斜。查询结果没问题,但执行时单个 task 可能拖垮整个 stage。 - 小表
users如果不大,可以考虑用广播 join 提示,避免 shuffle join 的大规模数据传输。
题目 6:每天写入数万任务后,结果表每个分区有几千个小文件,下游读取越来越慢。这是什么原因,怎么解决?
参考答案:
小文件多的原因主要有几个:写数据时 shuffle 分区数设置过多、动态分区写入时每个分区数据量不均匀、任务并发数太高且每个 task 单独写文件、以及没有在写入前做分区合并。大量小文件会让文件系统元数据压力变大,下游读取时每个文件会启动一个 task,任务数爆炸,调度和 IO 开销都很大。
解决方案按优先级排列:
- 写数据前用
coalesce或repartition把数据重分布到合理的分区数,避免一个 task 写一个文件。 - 开启 Spark 3.x 的 AQE,让它根据 shuffle 数据量自动合并小分区。
- 合理设置
spark.sql.shuffle.partitions,不要默认 200 就完全不管。 - 写文件时设置
spark.sql.files.maxRecordsPerFile,控制单个文件的最大记录数,避免数据倾斜导致个别文件过大,但小文件多的场景这个参数不解决根本问题。
解析与易错点:
coalesce减少分区数时默认不触发 shuffle,只做分区合并,代价低;但如果有数据倾斜,减分区后也可能把数据都压到一个 task 上,所以要观察分区内数据量是否均匀。- 小文件问题和数据倾斜经常一起出现,排查时先从 Spark UI 看每个 task 的 shuffle 写记录数,再决定是单纯合并分区还是要处理热点 key。
- 在写入前做一次
repartition比写入后再跑一次 compact 作业更省事,这个习惯建议从一开始就养成。
2.3 流处理与结构化流题
题目 7:Structured Streaming 的 append 模式配合 watermark,能保证迟到 10 分钟的数据仍然参与统计,同时及时输出结果?请说明 watermark 在这里的作用。
参考答案:
使用事件时间列设置 watermark,例如:
streamDF .withWatermark("ts", "10 minutes") .groupBy(window(col("ts"), "5 minutes"), col("word")) .count()watermark 告诉引擎“最多等多久迟到数据”。引擎维护当前最大的事件时间,并允许事件时间大于最大事件时间 - watermark的数据继续更新对应的聚合状态。在 append 模式下,窗口结果只有在 watermark 越过了窗口结束时间之后才会输出,这样既保证了迟到的数据可以被计入,又避免了长时间不输出结果。
解析与易错点:
- watermark 必须基于事件时间,不能基于处理时间。如果用系统时间戳当事件时间,那这个语义就彻底错了。
- append 模式下,聚合结果不会逐条更新输出,它更接近“窗口关闭后输出一次”。如果你希望新数据一到就更新结果,应该考虑 update 模式。
- watermark 不是无限缓存。超过 watermark 的迟到数据会被丢弃,所以业务上对延迟的容忍上限要评估清楚。
- 生产环境里,流任务挂掉后从 checkpoint 恢复是常见操作,watermark 的状态也会被 checkpoint 保存,这点在测试时很容易忽略。
题目 8:Spark Structured Streaming 的 exactly-once 语义由哪些机制保证?单独靠 Spark 就能实现端到端 exactly-once 吗?
参考答案:
Spark 内部通过 checkpoint 定期保存 offset、状态和配置,保证任务从故障恢复时不会丢失数据进度。配合事务性 sink 或幂等写入,才能做到端到端 exactly-once。
答案的关键是:单独靠 Spark 不能保证端到端 exactly-once。因为 Spark 能控制的是“从数据源读取到计算引擎内部”这部分的进度和状态,但数据写到外部系统时,如果外部系统不支持幂等或事务,重复写入就无法避免。
解析与易错点:
- 很多人一听到 exactly-once 就回答“用 checkpoint”,这只说对了一半。没有幂等 sink,checkpoint 只能保证你“重新算一遍”,但算出来的结果可能已经重复写了。
- 实际设计时,常见的配套方案是 Kafka 幂等生产者、HDFS 写入的原子 rename、或者基于主键的去重表。
- 在面试或项目中提到 exactly-once 时,一定要区分“引擎内部正好一次”和“端到端正好一次”,这是两个层次。
2.4 调优与运维排障题
题目 9:两个大表 join 时,某个 key 的数据量特别大,任务最后只剩一两个 task 在跑,其他 task 都结束了。怎么定位和处理这种数据倾斜?
参考答案:
定位方法:
- 打开 Spark UI,看具体 stage 中每个 task 的耗时和 shuffle read 数据量。倾斜的任务通常耗时极长,且 shuffle read 数据量是其他 task 的好几倍。
- 对 key 做统计,例如
df.groupBy("key").count().orderBy(desc("count")).limit(10),找出热点 key 和它对应的数据量占比。
常见处理手段:
- 过滤异常值:如果热点 key 是空字符串、null 或者纯噪声数据,直接过滤掉,或者填充随机后缀打散。
- 加盐两阶段聚合:对热点 key 的每条数据附加一个随机前缀,先把数据打散到多个分区做局部聚合,再去掉前缀做全局聚合。这个方法对 group by 型倾斜很有效。
- 广播 join:如果其中一张表很小,强制广播小表,避免 shuffle join 的倾斜。
- 拆分处理:把热点 key 单独取出来,用广播或自定义逻辑处理,再和正常 key 的结果合并。
解析与易错点:
- 提高并行度只能缓解不能根治倾斜。分区数变多,单个 task 的数据量会下降,但热点 key 的数据仍然会集中到少量分区里。
- 加盐方案要注意聚合函数的正确性。类似
count、sum这种可结合函数没问题,但如果涉及去重或求中位数,就需要额外设计。 - 倾斜的定位一定要先看 UI,不要上来就猜。有一次我排查一个任务,以为是大 key 倾斜,结果打开 DAG 发现是读取端某个分区数据本身特别大,根本不是 join 引起的。
题目 10:提交 Spark 任务后,executor 频繁报 OOM,可能是什么原因?完整的排查思路是什么?
参考答案:
Executor OOM 的原因大致有几类:单个分区数据量过大、shuffle 读数据超过可用内存、数据倾斜导致个别 task 内存暴涨、广播变量过大、序列化对象占用空间异常。
排查思路按顺序来:
- 先打开 Spark UI 看 OOM 出现在哪个 stage。如果发生在读取阶段,多半是分区数据量过大;如果发生在 shuffle 读阶段,重点看数据倾斜和内存配置。
- 看当前 executor 内存配置和每个 task 处理的数据量。一个分区几百 MB 和几 GB,内存压力完全不同。
- 检查是否在算子中使用了
collect或大量broadcast。 - 分析代码里的数据类型,比如把很大的对象塞进 RDD,又不做序列化,内存消耗会翻倍。
- 根据定位结果调整分区数、内存比例、广播阈值或序列化方式。
解析与易错点:
- 不要一看到 OOM 就下意识把
spark.executor.memory调到最大。很多 OOM 是分区不合理造成的,加内存治标不治本。 - Driver OOM 和 Executor OOM 要分开看。Driver OOM 常见原因是
collect()拉回了太大结果集,或者广播变量过多,不能混为一谈。 - 内存相关的常用配置只列两个必须理解的:
spark.memory.fraction表示统一内存占 executor 总内存的比例;spark.memory.storageFraction表示 cache 和 shuffle 之间内存的划分线。乱调这两个参数很容易把 shuffle 或缓存挤到溢写甚至 OOM。
题目 11:RDD 算子中调用了一个自定义类的方法,报NotSerializableException,怎么排查和解决?
参考答案:
RDD 的计算会分发到各个 executor 上执行,算子中的闭包、引用的外部对象都要能序列化。报这个错,通常是因为闭包里引用了某个类的实例,而这个类没有实现Serializable接口,或者类里有不可序列化的成员,比如数据库连接、流对象。
解决思路:
- 让相关类实现
java.io.Serializable。 - 改用 Kryo 序列化,并注册用到的类。
- 如果对象本身不适合序列化,比如连接池,就不要放在闭包里直接引用。可以在 executor 端使用懒加载,比如在算子内部用
lazy val或单例创建一个连接。 - 对于大的静态数据,比如机器学习模型或配置表,用
broadcast变量广播到 executor,而不是在闭包里捕获。
解析与易错点:
- 这块的实际场景很多。我在维护一个算法作业时,遇到过把 Redis 客户端直接塞进
map函数里,结果每台 executor 都要重新序列化一个客户端对象,不仅序列化报错,就算勉强跑通,性能也极差。 - 判断一个对象是否在闭包中,最直观的方法是看代码里在算子内部到底引用了哪些外部变量。尽量缩小引用范围,避免整个大对象被捕获进去。
- Kryo 序列化快但默认不注册所有类,需要
spark.kryo.registrationRequired配合检查,否则遇到未注册类还是会抛异常。
题目 12:一个任务输入约 1TB 数据,集群有 200 个并发槽位,每个 executor 内存 4GB,vCore 2 个。你如何评估分区数?这是一个开放题,没有唯一的“正确答案”。
参考答案:
评估分区数的核心目标是平衡并行度和调度开销。先把输入数据按常见的 128MB 一个 block 估算,1TB 大约有 8000 个 block。如果没有复杂的 shuffle,分区数保持在 8000 左右就能较好地利用数据局部性;但如果集群只有 200 个槽位,每个 task 串行处理,分区数太多也会增加调度和序列化开销。
一个更实用的评估办法是让每个 task 处理约 100MB 到 300MB 的数据。按 100MB 算,1TB 需要约 10000 个分区;按 300MB 算,大约需要 3500 个分区。所以这个场景下,分区数定在 4000 到 10000 之间都是合理的。之后还要看是否有 shuffle 聚合,以及 key 的分布情况。如果聚合后的 key 很少,每个 key 的数据量巨大,就要考虑加盐或调整后续聚合策略。
解析与易错点:
- 这类题的真正考点不是算出某个精确数字,而是展示你的推导过程:知道数据量、知道并发数、知道单 task 处理能力、知道 shuffle 数据量和 key 分布会影响结论。
- 不要机械地把分区数设成并发槽位的整数倍。槽位只是下限参考,分区数比槽位少会导致资源浪费,比槽位多很多则调度会变慢。
- 开 AQE 之后,Spark 可以自动调整 shuffle 分区数,但输入读取阶段的分区数还是由文件大小和 split 策略决定,所以分区数的评估仍然要会手动做。
3. 这些题目背后真正想让你掌握的细节
3.1 为什么“答案仅供参考”而不是唯一标准解
这套题里有一部分答案几乎是确定的,比如题目 2 的计算结果、reduceByKey和groupByKey的本质区别;但另一部分,比如题目 10 和题目 12,本质上没有标准答案,完全取决于集群规模、数据分布和业务语义。
Spark 版本演进也会改变“最佳实践”。举例来说,Spark 2.x 时代大家习惯手动调spark.sql.shuffle.partitions,到了 Spark 3.x,AQE 可以在运行过程中合并小分区,参数含义和执行策略都不同了。所以当你拿着旧答案去套新版本,很容易得出错误结论。
还有一个更现实的原因:练习题的答案往往来自不同人的经验总结。一个人说“要调大内存”,另一个人说“要调小内存”,可能他们面对的场景完全不同。答案仅供参考,真正的标准是你能不能根据现场现象一步步推导出合理方案。这也是我最建议的刷题方式:不看解析,先用自己的话说清楚逻辑,再对照答案找漏洞。
3.2 判分者视角的高频丢分点
我帮团队出过很多次摸底题,也看过不少人的作答,发现几个反复出现的丢分点。
第一,概念混淆。有人会写“RDD 是把数据存在内存里的弹性分布式数据集”,这句话本身没错,但容易让人忽略 RDD 的抽象本质。实际上 RDD 更像是对数据和处理流程的抽象描述,计算是惰性的,数据可以存在内存也可能落盘,关键看存储级别和运行状态。
第二,只答结论不答代价。问groupByKey和reduceByKey的区别,写“groupByKey 慢”就能拿一部分分,但真正的分差在于能不能说明白 map 端合并减少了多少网络 IO、为什么groupByKey在数据量大时更容易引发 OOM。
第三,参数滥用。调优题里最容易出现“把并行度调到 5000”“把 executor 内存调到最大”这类暴力答案。这种答案的实际危害比不调还大:资源浪费、调度延迟、甚至任务更不稳定。
第四,挂概念不落地。喜欢写“用 AQE 优化”,但说不清 AQE 会做什么、在哪个阶段做、依赖什么条件才能生效。这种答案在纸面上看不出问题,一追问细节就崩了。
4. 用这套题做一次自测与复盘
4.1 45分钟闭卷自测流程
建议按下面的流程做,比直接看题看答案效果好得多。
第一步,准备一个空白文档,把每道题的答案写下来,包括开放题的分析思路。不要边做题边翻答案,那样只会产生“我看过了所以我懂了”的错觉。
第二步,给自己计时 45 分钟。先做有把握的题,开放题先写主干思路,不要卡在细枝末节。时间到了就停笔,哪怕没做完也先停。
第三步,对照答案给自己的每一题打分,可以分为“完全正确”“方向对但不严谨”“不会/错误”三档。打分的时候不要只盯结论,要看你写出来的理由和推导步骤。
第四步,把错题和“方向对但不严谨”的题目单独列出来,每个知识点找对应的官方文档章节或源码入口再看一遍。这一步是这套题价值最大的地方。
4.2 用一张查漏表定位薄弱环节
做完题之后,可以用下面这张表对照自己的掌握程度:
| 知识模块 | 自检标准 | 你现在的状态 | 下一步行动 |
|---|---|---|---|
| RDD 执行模型 | 能说清转换算子和行动算子的区别,能手动推演一个简单作业的 stage 数 | 判断自己能否不看 UI 讲出执行过程 | 用 spark-shell 跑一个含 join 或 reduce 的作业,观察 DAG 和 stage |
| Shuffle 与宽窄依赖 | 能解释宽依赖为什么会触发 shuffle,map 端 combine 发生在哪一步 | 是否清楚 shuffle write 和 shuffle read 的数据流向 | 对比reduceByKey和groupByKey的 UI 指标 |
| SQL 与 Catalyst 优化 | 能用explain分析谓词下推和列剪枝 | 是否理解 DataFrame 比 RDD 快的三个支点 | 写两个 join 查询,对比物理计划差异 |
| 流处理语义 | 能说明 watermark 和输出模式的关系,能解释端到端 exactly-once 的组成 | 是否把 checkpoint 和幂等 sink 分开理解 | 用本地 Socket 或 Kafka 写一个带 watermark 的计数任务 |
| 调优排障 | 面对 OOM 或倾斜,能按 UI 现象、数据分布、方案选择三步排查 | 遇到线上问题是否还是靠猜 | 构造一份热点 key 数据,实验加盐聚合前后耗时 |
这张表打印出来,每周做一次自我复查。你会发现有些知识点当时以为懂了,过两周再看又变模糊。这非常正常,Spark 涉及的概念密度高,间隔性复盘比一次性猛学有效得多。
4.3 把错题变成动手实验
错题是最好的实验素材。比如题目 9 不会,就自己生成一份带热点 key 的数据,写一个 join 或 groupBy,先看 task 耗时分布,然后加盐、广播、过滤几种方案逐个试。题目 7 不懂,就写一个伪造事件时间的流式 wordcount,故意输入几条延迟数据,观察 append 和 update 模式下的输出差异。题目 2 算不清楚 stage 数,就在环境里加一行rdd.toDebugString,把血缘打印出来和 UI 对照。
这些实验不需要大数据量,本地一个小集群足够。重点不是数据规模,而是你能不能从现象倒推出原理。把每一道错题都变成“我能解释、能演示、能复现”的实证题,比多刷三套题有用得多。
5. 我在实际 Spark 练习与答疑中的几点体会
带新人时我习惯让他们先做这套题,每周讲一遍其中一道题的原理。讲过几次之后会发现,能“讲清楚”的人,写代码的水平也会跟着提高,因为讲明白意味着要对执行链路负责。这也是我为什么把答案标注为“仅供参考”:与其背标准答案,不如建立自己的分析框架。做技术题从来不应该是“对答案”,而是拿问题当镜子,照出自己的短板。
最后再分享一个小技巧:做题时遇到任何拿不准的执行细节,不要停在脑内推理,直接在 spark-shell 里跑一段最小示例。跑完用spark.ui.enabled打开 Web UI,看 stage 数、shuffle 量、task 耗时,你的理解会被这些真实数据狠狠校正一次。这套 Spark 练习测试题(答案仅供参考)的价值不在于让你记住多少知识点,而是让你不断验证自己的判断,直到形成可靠的技术直觉。