2026/10/19 11:13:36

Spark核心机制自查:RDD依赖、Stage划分与数据倾斜调优

Spark核心机制自查:RDD依赖、Stage划分与数据倾斜调优 这套Spark练习测试题我在团队里用过很多次标题写着“答案仅供参考”是想提醒第一次作答的人别把它当成背诵题库先用自己的话写答案再去对参考点。题目本身不偏、不怪覆盖了日常写Spark作业时最容易踩坑的RDD机制、宽窄依赖、SQL优化和数据倾斜。适合三类人看准备大数据开发面试的、刚学会Spark API但觉得自己只会调接口的、需要给新人出摸底题的。1. 这套Spark练习测试题在测什么四个能力区间1.1 为什么我把题目收敛在几个主线上刚开始帮人准备Spark面试时我收集过大量所谓的高频题结果发现很多是“名词解释”比如“什么是sparkContext”“什么是executor”。单独拎出来都能答但放到一个真实任务里这些概念是散的。后来我把题目重新收敛成四条主线RDD机制与算子边界包括惰性求值、血缘、transform和action的区分。调度与执行模型宽窄依赖、Stage划分、Shuffle发生位置。SQL层的优化手段DataFrame比RDD快在哪里、Catalyst到底优化了什么。生产环境调优与排错数据倾斜、缓存策略、提交参数怎么给。这四点不是我拍脑袋定的而是对应一个完整作业从“编写代码”到“提交运行”再到“出问题排查”的完整链路。只懂API的人会在第一组就露馅只刷过面试题的人会在第二组答得模棱两可真正跑过生产任务的人往往在第四组能给出非常具体的回答。1.2 建议的自测方法先口述再对答案我的建议是拿到题目之后不要立刻写代码验证先用口述的方式回答每题控制在100字以内。如果说不清“为什么”说明这个知识点还没真正长在你的知识体系里。面过不少候选人后发现一个规律能快速写出API调用的人很多能完整讲清楚因果链的人很少。比如“为什么groupByKey性能不如reduceByKey”不少人会背“因为reduceByKey有预聚合”但再追问“预聚合发生在哪一端、什么时候发生、省掉了什么代价”就卡住了。这套题的设计目标就是逼这种细节暴露出来。2. 第一组题目RDD的惰性、血缘与算子边界基础题里最容易被误判2.1 题目1transformation和action的区别并说明collect和foreach为什么值得单独讨论第一题是热身题但翻车率不低我见过好几个人把foreach当成transformation。题目是这样问的什么是延迟计算lazy evaluation下面哪些是actionmap、filter、reduce、collect、foreach、saveAsTextFile同样是actioncollect和foreach的差别是什么参考答案要点transformation只是构建执行计划不会真正触发计算action才会触发job执行。map、filter是transformationreduce、collect、foreach、saveAsTextFile都是action。collect会把数据从所有executor拉回driver端如果结果集很大driver会OOMforeach则是在executor端直接消费数据不会把数据集中到driver。很多生产脚本的错误都出在误用collect去处理本该在executor侧完成的事情。易错点在于不要只背“collect是action”要意识到它和数据规模、driver内存直接挂钩。2.2 题目2同一个RDD被多次使用会发生什么cache和checkpoint为什么都救不了所有场景这道题我特别喜欢因为它能区分“用过RDD”和“理解RDD”的人。题目是一个RDD如果被两个action反复读取第一次和第二次读取之间发生了什么用cache和checkpoint分别解决什么问题什么时候该用checkpoint而不是cache参考答案要点RDD默认无持久化每次action都会根据血缘重新计算一遍所以多次使用同一份数据会重复消耗资源。cache可以把计算结果存入内存或磁盘避免重复计算但cache不会切断血缘如果分区数据丢失还是要靠上游重算。checkpoint会把数据物理写到高可用存储中同时切断血缘之后的任务不再往上追溯。有一类场景很多人没注意到当血缘链特别长、且上游重算代价很高时cache只能救“当前job内重复读”一旦job失败要从头重算checkpoint的价值才真正体现。另外cache的存储级别选择也要结合实际不是MEMORY_ONLY就一定最好数据量大时MEMORY_AND_DISK更稳妥因为内存放不下的部分至少不会被直接丢弃。2.3 题目3map和flatMap的区别以及reduce和fold的初始值陷阱这题看起来太基础但写错的人比想象中多。题目是分别描述map和flatMap的输入输出关系。对一个空RDD执行reduce会怎样fold可以避免吗参考答案要点map是“一对一”一个元素进来返回一个元素flatMap是“一对多”每个元素可以展开成一个集合并把集合拍平到父级RDD。很多人在处理一行文本拆单词时下意识用map结果得到RDD[Array[String]]后续逻辑全被套了一层壳其实拆词应该用flatMap。reduce没有初始值它依赖集合中“至少有一个元素”才能计算遇到空RDD会直接报错。fold可以传初始值空RDD也能返回初始值不会崩。日常写聚合逻辑时如果上游可能被过滤成空使用fold更安全。我见过有人为这事在凌晨跑到群里问为什么作业失败原因就是上游数据全被过滤后reduce直接抛异常。3. 第二组题目宽窄依赖与Stage划分一问就知道有没有看过Spark UI3.1 题目4宽依赖和窄依赖的区别为什么宽依赖会引入Stage边界第二组题目从调度模型切入。题目是什么是窄依赖什么是宽依赖各举两个算子例子。为什么说宽依赖是Stage划分的边界groupByKey和reduceByKey在依赖上有什么不同参考答案要点窄依赖指的是父RDD的每个分区最多被子RDD的一个分区使用map、filter都属于窄依赖宽依赖指的是父RDD的一个分区会被子RDD的多个分区使用groupByKey、join默认场景会产生宽依赖。宽依赖意味着上游分区数据要经过网络跨节点传输给下游多个分区这就是Shuffle。Stage的边界就在宽依赖处遇到宽依赖Spark会把前面所有窄依赖算子打包成一个Stage再为下一个Stage重新启动任务。这也是为什么很多人说“Stage数通常等于Shuffle次数加一”。groupByKey和reduceByKey最关键的差别在shuffle前是否做本地聚合。groupByKey会把所有原始键值对直接打散传输到下游而reduceByKey会在map端先对本地相同key做一次合并减少网络传输量。注意并不是任何场景reduceByKey都完胜但默认情况下能用reduceByKey做聚合就不要用groupByKey。3.2 题目5给一段代码判断Stage数量和Shuffle次数这道题要求现场读代码说清楚执行过程。题目如下val rdd1 sc.textFile(inputPath) val result rdd1 .map(line (line.split(,)(0), line.length)) .filter { case (_, len) len 3 } .groupByKey() .mapValues(v v.size) .join(otherRdd) .collect() result.foreach(println)问题这段代码一共触发几次Shuffle会划分成几个Stage有没有办法减少Shuffle次数参考答案要点map、filter都是窄依赖会合并在一个Stage里groupByKey会引入第一次Shuffle形成新的StagemapValues是窄依赖后面的join默认会引入第二次Shuffle除非参与join的RDD使用相同分区器预先分区或者其中一个表很小可以走Broadcast Join。collect是action会触发整个DAG执行但不会额外引入Shuffle它只是给最后一个Stage加上ResultStage属性。所以多数情况下答案是“两次Shuffle三个Stage”。如果join通过小表广播实现可以把join变成无Shuffle的窄依赖操作那么整个作业可能变成两次Shuffle、两个Stage甚至更少。这里我必须强调“答案仅供参考”的原因这个题在不同Spark版本、不同内存配置、不同RDD缓存状态下执行结果会有微妙差别。比如某个RDD已经被persist过执行计划会在某个节点加一个缓存读但Stage划分依据依然是宽依赖这一点不会变。所以评分时我看的不是数字而是对方有没有理解“Shuffle决定Stage边界”这一底层逻辑以及能不能主动说出Broadcast Join这个优化点。4. 第三组题目DataFrame为什么能更快Catalyst优化器做了什么4.1 题目6RDD、DataFrame、Dataset三者区别谁更适合日常开发第三组开始进入Spark SQL的范畴。第一道题从数据结构、类型安全、优化空间三个角度对比RDD、DataFrame和Dataset。为什么生产上现在很少直接写RDD算子做复杂业务参考答案要点RDD只关心元素本身不关心字段结构开发者用函数控制一切灵活但优化空间小。DataFrame引入了Schema把数据描述成一张表Spark能根据Schema推断字段类型、裁剪列、下推过滤条件Catalyst优化器可以大幅改写执行计划。Dataset在DataFrame基础上提供类型安全编译期能发现字段类型错误但代价是某些序列化环节有额外开销。日常写复杂报表和ETL优先选择DataFrame或Dataset。RDD不是没有价值而是更适合处理非结构化数据、或需要高度自定义逻辑的场景。有个比喻我一直很喜欢RDD像程序员手动安排每件行李怎么搬运DataFrame像告诉快递系统“箱子里是衣服和书”系统自己决定用什么车、走什么路线、怎么装最省空间。4.2 题目7Catalyst优化器到底做了什么写出执行计划的大致流程只回答“DataFrame有Catalyst优化所以快”是不够的还得说出优化器的工作流程否则面试官会认为是背结论。题目是Catalyst处理一条Spark SQL的完整步骤是什么逻辑计划和物理计划分别解决什么问题参考答案要点Catalyst处理流程可以概括为SQL解析成未解析的逻辑计划通过Catalog里的表结构信息做校验和字段绑定变成解析后的逻辑计划执行一批优化规则包括谓词下推、列剪枝、常量折叠等得到优化后的逻辑计划再根据数据统计信息选择物理执行方案比如选择Broadcast Join还是SortMerge Join最后生成可执行的代码Tungsten的WholeStage Codegen会把多个操作合在一起避免每算一行都要在算子间来回传递对象。这里有几个容易被忽略的关键点。逻辑计划回答的是“SQL要算什么”物理计划回答的是“具体怎么算”。同一个Count可以从全表Scan再Count也可以走统计信息直接拿结果同一个Join可以广播小表也可以SortMerge物理计划层面的选择才是真正决定性能的东西。4.3 题目8用一个例子说明谓词下推和列剪枝这道题不要求背概念要求现场设计一个例子。题目是给定一条SQLSELECT a.name FROM order_table a JOIN user_table b ON a.user_id b.id WHERE a.city beijing说说Spark会怎么优化。参考答案要点Catalyst会把WHERE条件里只涉及order_table的过滤条件下推到Scan阶段先过滤掉city不为beijing的行再参与Join减少Join两侧的数据量这叫谓词下推。列剪枝发生在读取数据阶段SELECT只要求nameSpark不会把order_table的整行全读到内存只会读需要的字段减少I/O和内存占用。实际开发中最常见的反面例子是把一个大JSON字段select出来又不使用导致shuffle数据量暴涨。还有人在DataFrame里先join再filter逻辑上没错但如果过滤条件只针对一张表写成先filter再join更稳妥虽然Catalyst部分情况下能自动优化但显式写出更清晰也更容易被其他同事理解。5. 第四组题目倾斜、缓存和提交参数最像生产环境的一组题5.1 题目9任务卡死在一个task上如何判断是不是数据倾斜给出解决办法这组题我会直接给一个现象让答题者扮演一线开发。题目是Spark作业里19个task几秒钟跑完1个task跑了20分钟还没结束怎么定位原因如果确认是数据倾斜有哪些解决办法参考答案要点先不要猜打开Spark UI看活跃task、shuffle读写量、task处理数据条数。如果某个task的Shuffle Read远大于其他task大概率是数据倾斜。还要确认倾斜发生在聚合还是join因为两种场景的解法不完全一样。如果是单key聚合导致的倾斜可以做两阶段聚合先给key加随机盐把大key拆散做第一轮聚合再去掉盐做第二轮聚合。伪代码如下rdd.map(item ((item.key, Random.nextInt(100)), 1)) .reduceByKey(_ _) .map { case ((key, _), count) (key, count) } .reduceByKey(_ _)如果是大表join小表倾斜通常发生在小表比较大、默认走SortMerge时可以尝试把另一张表广播让join变成无Shuffle的Broadcast Join。如果是两张大表join且倾斜key是少数几个可以把倾斜key单独拆出来和正常key分两条路处理最后再合并结果。最容易扣分的回答是只写“加盐”两个字。加盐虽然能解决一部分问题但它改变了key也可能让结果错误如果倾斜发生在join一味加盐反而会让join失去关联关系。评分时我特别看重有没有先做定位再选方案而不是套模板。5.2 题目10给一个生产Spark作业如何配置spark-submit参数这道题没有标准答案但能看出答题者有没有被OOM毒打过。题目是如果executor内存给得很大作业就一定更快吗并行度设置为多少合适写出几个你常用的Spark SQL运行参数并说明理由。参考答案要点executor内存影响单task能处理的数据量但并行度由cores和分区数决定。内存给得很大、cores很少相当于一个大仓库配了两个搬运工仓库再大也快不起来。常见配置思路是每个executor给2到4个核单个executor内存控制在可承受范围内总内存需求根据数据量估算不要拍脑袋翻倍。下面是我常用的参数整理参数作用我的习惯spark.executor.memoryexecutor堆内内存根据单分区数据量和并行度估算spark.executor.coresexecutor可用核数通常2到4不均匀的交叉分配spark.sql.shuffle.partitions聚合/join的shuffle分区数默认200根据数据量调高或调低spark.sql.autoBroadcastJoinThreshold广播join的大小阈值小表场景适当调大但别盲目放大spark.driver.memorydriver端内存只涉及collect或聚合结果汇总时才需要关注期间还要注意Spark执行器的内存并不是全部用来存数据。它分为Reversed内存、Execution内存和Storage内存等配置spark.memory.fraction只影响执行和存储共享部分的比例。很多人只看executor-memory不改fraction内存利用率未必合理。这块我一般建议新人去看Spark官方tuning文档而不是照抄别人的参数。5.3 题目11什么时候用广播变量什么时候用cache两者的区别是什么广播变量和缓存是面试里最容易被混着问的一对概念。题目是广播变量解决什么问题什么数据适合广播cache和广播变量的使用场景有什么不同参考答案要点广播变量是把一份只读数据高效发送到所有executor让每个节点保留一份副本。适合广播的不一定是整个表而是维度较小、被频繁使用的关联数据比如几MB的省份表、商品分类表。使用广播变量可以减少大量shuffle因为它取代了“每个task都拉一份远程数据”的笨办法。cache用于保存某个RDD或DataFrame计算结果避免在同一作业或多个作业中反复重算。两者的核心区别是广播变量是“把公用数据送到节点”cache是“把某个中间结果留在节点省去重新计算”。给一个具体场景如果某个DataFrame在后续十几个操作里被反复复用先cache它如果某张小表要反复和不同的流join优先考虑广播join而不是只做cache。这里有个很实用的经验cache之前先想想复用次数。只被用一次就不要cachecache本身有写入和清理成本滥用缓存反而降低性能。还有一个常见误解是cache后的数据“一定不会丢”实际上数据量大、内存紧张时部分分区会被清理掉下次使用还是要重算。6. 评分思路与参考答案的使用方式为什么标“仅供参考”6.1 我给题目的打分维度这套Spark练习测试题我并不是按“对错”打分而是走四个维度维度考察内容低分表现高分表现机制理解能不能解释为什么只能背名词能画数据流向能说明因果链代价意识有没有考虑资源消耗只给方案给了方案还说清楚省了什么代价落地能力方案能不能执行只谈理论提到Spark UI定位、参数验证、回滚方案边界意识是否知道方案局限性方案万能论主动说明适用条件和失败场景同一个“如何处理数据倾斜”初级答案写“加随机盐两次聚合”中级答案会补充“倾斜发生在聚合还是join方案不同”高级答案则会说“先去UI看输入数据确认key的分布再选择加盐或广播join还要评估加盐对NULL值和结果准确性的影响”。三个答案都不完全错误但深度差别很大。答案仅供参考本质上是因为Spark的执行结果高度依赖数据分布和集群状态不同场景下的最优解不一样。6.2 这套题怎么用才能帮你拉开差距做完题不要急着对答案先把每个题的“卡壳点”写下来。比如看到题目5你不知道该怎么确定Stage边界说明你对宽依赖的理解还停在概念层下一步就去Spark UI找一个真实作业看DAG图把源码里的宽依赖和界面上的Stage对应起来。我自己的经验是大部分人出错不是不知道API而是不知道某个动作会触发什么代价。Map和FlatMap写错最多是结果多套一层Shuffle次数判断错就是整个作业的执行模型没建立起来。这也正是Spark练习测试题里“答案仅供参考”的原因——答案只是告诉你知识点在哪但只有当你理解了“为什么”才能在换一个数据集、换一个集群规模之后依然做对。最后再分享一个带新人时常用的做法每过一段时间我自己也会重新做一遍这套题不看曾经的答案只凭当前的理解口述一遍。如果哪道题说不出细节就意味着我对Spark的某个机制已经退化了。在这个框架迭代飞快的领域能够稳定地解释基础机制才是真正值钱的能力。