作为一个刚熬过Spark课程期末的过来人,我深知这门课的知识点有多散、多碎。从RDD的机制到Spark SQL的优化,从集群部署的资源分配到各种内存调优参数,每一个板块单独拎出来都够喝一壶的。我整理的这套复习笔记不是简单的概念罗列,而是围绕“Spark到底是怎么把一坨数据拆开、算完、再拼回来”这条主线,把核心原理、实操步骤和考试里那些高频的套路题都串了一遍。无论你是正在准备期末考还是面大数据的岗位,这份总结应该能帮你省下不少瞎翻书的时间。
1. Spark整体运行逻辑与核心架构
1.1 Spark到底是什么,以及它和Hadoop的区别
很多同学期末复习到一半,就开始混淆Spark和Hadoop的职责边界。考试也特别喜欢问这类对比题。一句话讲透:Hadoop里的MapReduce负责计算,但它每一步都要落盘,导致迭代计算和交互式查询慢得让人抓狂。Spark的核心创新就是“基于内存的计算”,把中间结果尽可能留在内存里,通过DAG(有向无环图)调度引擎来减少不必要的磁盘读写和任务启动开销。
从设计哲学上看,Spark的关键在于它给上层应用提供了统一的计算引擎,底层却可以跑在多种资源管理器上。它不只是替代MapReduce的批处理工具,而是一个能同时处理批处理、流计算、交互式查询和图计算的一体化平台。因此在实际项目中,Spark往往和HDFS搭配使用,Spark负责算,HDFS负责存,而Yarn则充当资源调度中枢。
1.2 核心组件与角色分工
Spark程序启动以后,背后会拉起一套完整的进程体系。理解这套体系是分析问题的基础。
- Driver:你的main方法跑在哪里,哪里就是Driver。它负责把用户代码转换成逻辑执行计划,再进一步切割成具体的任务集合,并且负责任务的调度、分发和状态汇总。很多人忽略的一点是,Driver不仅仅是控制中心,它还持有SparkContext这个核心对象,所有RDD的创建、转换和行动操作,都通过SparkContext向集群发起。
- Executor:真正干活的Worker,跑在Worker节点上的JVM进程。它有两个核心职责:执行Driver派发的Task,以及把RDD的缓存数据保存在自己的内存或本地磁盘里。Executor启动时会向Driver反向注册,并周期性上报心跳和执行状态。
- Worker / Slave节点:是集群中物理或虚拟机的角色,负责管理和监控所在节点的资源,并启动Executor进程。
- Task:被送到Executor上执行的最小工作单元。一个Executor默认可以同时跑多个Task,具体并发度取决于给它分配的CPU核心数。
整个流程可以串成一条线:Client提交任务 -> Driver解析代码并构建DAG -> DAG Scheduler根据依赖关系将图拆成多个Stage -> TaskScheduler把Stage中的每个Task分发给具体Executor -> Executor执行任务并回报结果或异常。
1.3 作业、Stage与Task三级调度体系
考试特别喜欢在这里挖坑,比如问“一个Action操作产生了1个Job,为什么这个Job里会包含好几个Stage,而且Stage之间还会出现Shuffle?”
其实原因在于Spark的懒执行模型。遇到transformation算子只要登记血缘关系就行,但遇到action算子(比如count、collect、saveAsTextFile)就必须真正计算了。此时DAG Scheduler会从触发计算的这个RDD出发,沿着血缘依赖一直往前回溯,遇到宽依赖(即父RDD的一个分区可能被子RDD的多个分区使用)就切一刀,新出一个Stage;遇到窄依赖则直接待在当前Stage里合并处理。
Stage划分规则是个考察重点:每个Stage内部的计算都是流水线式执行,尽可能让数据待在内存里按分区逐步算。而Stage之间是“前一个Stage算完才轮到下一个Stage”,中间的交接点就是Shuffle过程。所以一个Job的物理执行形态,就是按Stage执行序展开。TaskScheduler再将这些Stage中的任务集合下发到各Executor上。
1.4 考试里常见的架构细节提问
- Spark的Master和Worker是干什么的? 独立模式下的Master负责资源调度和集群管理,维护整个集群的状态;Worker节点负责执行真正的工作。生产环境多数不需要自己搭Standalone,因为资源回收和分配能力不如生产级资源管理器强。
- 为什么Driver挂了整个任务就断了? Driver是整个应用的大脑,它的状态就是应用状态。一旦Driver进程退出,SparkContext也就失效,所有Executor会被强制回收,作业直接失败,也因此才有了后来各种恢复机制的设计。
- 一个Executor的并发能力怎么决定? 取决于分配给它的CPU核数。假设每个Executor分配2个vcore,那么它最多同时跑2个任务。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. RDD编程模型与运行机制深度拆解
2.1 RDD五大属性及其重要性
RDD(弹性分布式数据集)是Spark中最基础的数据抽象。它的五个关键属性不仅是面试必问,也是理解后面所有优化手段的前提。
- 分区列表:一个RDD由多个分区构成,每个分区对应数据集合的一个子集。分区数量决定了并行度,同时也直接决定产生多少个Task。
- 分区器:对于key-value型的RDD,会根据分区器决定数据按什么规则分发到哪个分区,常见的有HashPartitioner和RangePartitioner。只有key-value类型的RDD才可能被分区,普通RDD的分区器是None。
- 依赖关系:每个RDD都记录了它是从哪些父RDD转换而来的,这些依赖关系构成了DAG的边,也决定了容错和计算方式。
- 首选位置:优先将计算调度到数据所在的节点进行本地读取,从而减少网络传输开销。
- 计算函数:Spark并不真正在每个分区里预先存储具体数据,它保存的是“如何计算得到一个分区数据”的函数,计算动作是惰性的,遇到行动操作才从头算。
考试理解的重点在于,RDD本身不存数据,它是一张“生产数据的图纸”。这也是为什么血缘关系极其重要,因为一旦某个分区数据在计算过程中丢失或损坏,Spark会按血缘关系,从原始数据源重新计算这个分区,而不需要重新计算整个数据集。
2.2 Transformation算子与Action算子的底层差异
对算子进行分类掌握是性价比很高的复习策略。
Transformation算子是懒执行,返回值还是RDD,不触发实际计算。常见的就是map、flatMap、filter、groupByKey、reduceByKey、sortByKey、distinct、union、join、cogroup等。考试需要重点理解的几个例子:
- filter在逻辑上是窄依赖,但如果你在filter之后没有做任何合并分区的操作,就有可能出现大量空分区或数据量极小的分区,从而白白浪费后面Stage的任务调度时间。
- reduceByKey和groupByKey虽然最终效果都是按key做聚合,但reduceByKey会先在map端做一次本地合并,再把精简后的数据通过网络传至reduce端,而groupByKey则直接全量传输原始数据。原因在于reduceByKey利用了一个Map端Combine的优化机制,在数据还没进网络之前先给数据“减重”。
- map和flatMap一个明显的区别就是输出结构。map是“一对一”的映射,每个输入元素产生一个输出元素,哪怕你返回一个空列表,也会保留这个空列表。而flatMap要求每个输入可以产生零到多个输出,比如一行文本被拆成多个单词,输出的元素是展平后的单词流。
Action算子是惰性执行的触发点,返回值不是RDD,它会实际向集群发起作业。常见的是collect、count、first、take、reduce、foreach、saveAsTextFile等。需要注意collect会把全量数据拉到Driver端,如果结果集过大,极易造成Driver OOM,这在实际项目中是大忌。take则相对安全,它只取前n个元素,底层使用了更高效的局部扫描方式。
2.3 宽依赖与窄依赖及Shuffle的代价
面试题几乎都绕不开“请解释宽依赖和窄依赖的区别”这种题。大家没必要死记定义,从血缘角度和实际调度行为去理解就行。
窄依赖是指父RDD的每个分区最多被子RDD的一个分区使用。比如map、flatMap、filter这类算子,父分区与子分区是一对一或一对固定数量的关系。窄依赖最舒服的地方就在于,就算某个分区计算失败,只需要重新计算这个分区对应的那个父分区即可,不需要大动干戈。同时,窄依赖可以在同一个Stage内流水线执行,数据在内存里一路算下去,性能极高。
宽依赖则指父RDD的每个分区可能被子RDD的多个分区使用。比如groupByKey、reduceByKey、join这些需要按key重分区的算子,父分区的数据需要被拉散、重组、分发到下游的多个分区中。这个过程就是Shuffle。宽依赖必然触发Stage切分,同时也是Spark作业中最容易出问题的环节。网络传输量巨大、磁盘I/O频繁、分区不均匀导致的倾斜、以及大量的序列化和反序列化开销,全都在这里发生。
考试还经常考到“Shuffle阶段的数据是怎么传输的”。在Spark里,Shuffle的写端会为每个下游分区生成独立的文件片段(block文件),这些文件最终归并到一个data文件里,同时生成一份索引文件。读端Executor通过网络抓取属于自己的那部分数据,拉到自己内存里进一步聚合归并。这套机制经历了Hash Shuffle、Sort Shuffle等多个版本的演进,核心目的是减少文件数量、降低随机I/O、并提高数据压缩效率。
3. Spark SQL:结构化数据处理的必经之路
3.1 DataFrame为什么比RDD快
在课程后半段,几乎每份作业都用Spark SQL的数据抽象来实现了。核心原因就是DataFrame带有Schema信息,它把“数据的形状”暴露给了优化引擎,从而可以使用Catalyst优化器做自动的查询优化。
你可以把DataFrame理解成一张分布式的表,每行是Row对象,但列的字段是明确标注的。正因为它知道字段类型和字段名,系统就能提前做谓词下推、列剪枝、甚至代码生成等优化。RDD的计算逻辑对Spark是一个黑盒,Spark不知道每一步会筛选掉多少数据,只能老老实实全量计算。而DataFrame的查询计划是可分析的,Spark可以把过滤操作尽量往数据源方向挪,减少加载后的数据处理量,也可以只读需要的列。
3.2 Catalyst优化器的工作过程
Catalyst是Spark SQL的核心。复习Catalyst时,记住四个阶段,考试就比较好拿分。
第一阶段是“逻辑计划解析”。你会把SQL字符串或者DataFrame的DSL操作挂到一棵语法树上,Spark会校验表名、列名、数据类型,然后生成未解析的逻辑计划。第二阶段是“逻辑计划分析与优化”,比如谓词下推、列剪枝、常量折叠。第三阶段是“物理计划生成”,物理计划要选择具体的Join算法,比如BroadcastHashJoin、SortMergeJoin,以及合理的分区策略。第四阶段是“代码生成”,通过编译生产更优的Java字节码来提升执行效率,这就是所谓WholeStageCodegen的威力。
实际学习中,如果能用explain()方法去查看执行计划,对比不同写法的物理计划差异,会掌握得更扎实,甚至能达到肉眼SQL调优的水平。
3.3 考试与面试高频SQL写法及优化
- join倾斜怎么破? 如果某个key的数据量远远大于其他key,就会发生数据倾斜。常给的解决方案是先加盐打散,比如给key拼上随机数做两阶段聚合,或者用广播变量把小表广播到各节点做map join。
- 为什么能用广播小表就不要用常规shuffle join? 广播Join的核心是让每个Executor都持有一份完整的小表数据,在map端直接完成匹配。这样能规避掉大表全量Shuffle的网络开销。Spark 3以后,如果表的大小低于spark.sql.autoBroadcastJoinThreshold(默认10MB),会自动通过统计信息判断并选择广播HashJoin。
- UDF的坑是什么? 如果业务逻辑能用内置函数解决,就不要自己写UDF。UDF让优化器无法深入分析和优化函数内部的逻辑,甚至可能导致某些情况下丢失分区信息,引起不必要的Shuffle。
3.4 Spark SQL如何对接外部数据源
Spark SQL不仅查Hive表和Parquet、ORC文件,还能连接各种数据源。项目实践中有一次我需要让Spark读取Redis里的数据做实时特征拼接,起初愣是想通过JDBC直连,但延迟太大;后来换成了Redis源封装,在RDD层面自己实现分区读取逻辑,才把批量读取性能拉上来。这类问题一般是考察“你是否理解DataFrame的DataSource接口以及自定义读取器的基本流程”,而不是要你去把Redis源码背下来。
另外还要知道JDBC数据源的内置实现:写入时如果建表没指定分区列,很容易出现单分区写入压力过大,因为默认只有一个任务在写目标表。使用partitionColumn、lowerBound、upperBound、numPartitions等参数做并发写入非常关键。
4. Spark集群部署与Yarn资源调度
4.1 从零搭建Spark集群的环境准备
复习集群搭建的过程中,最容易踩坑的是环境准备,很多同学装了四五天集群还没跑起来,就是因为没搞清楚前置条件。搭建Spark集群前,有几项基础工作必须先做完。
- 基础环境:JDK 8/11/17,注意Spark各版本的JDK兼容性;如果是新版Spark 3.5及以上,使用JDK 8还是11要提前确认。
- SSH免密登录:主节点要能免密登录所有从节点,否则启停脚本会失败。
- 配置Hostname和hosts文件:每台机器的IP映射要统一,否则节点之间互相找不到。
- 从官网或镜像站下载与Hadoop版本兼容的Spark二进制包。
配置环境变量时,需要设置JAVA_HOME、HADOOP_CONF_DIR、SPARK_HOME等,并在spark-env.sh里明确指定JAVA_HOME路径。我遇到最多的报错就是NameNode都起来了,但是作业提交失败,最后发现是SPARK_HOME路径写错或版本号不匹配。
4.2 Yarn模式下的提交配置与参数逻辑
实际生产中最常用的是Spark on Yarn模式。它有几个好处:Yarn统一负责资源分配,多个计算引擎共用一套Hadoop集群,而且提交任务时不需要预先部署Spark到所有节点,Yarn会在容器里动态启动Executor。
在Yarn模式下,Driver也运行在Container中(cluster模式),提交命令的关键参数如下:
bash复制spark-submit \
--class com.example.spark.WordCount \
--master yarn \
--deploy-mode cluster \
--driver-memory 2g \
--executor-memory 4g \
--executor-cores 2 \
--num-executors 10 \
/path/to/application.jar
这些参数背后隐藏着一整套资源调度逻辑:
- spark.executor.cores:每个Executor可用的CPU核心数。这个参数直接影响每个Executor能同时运行的Task数量,也影响Yarn为它分配Container资源的大小。
- spark.executor.memory:每个Executor进程的堆内存上限,Yarn申请的实际物理内存通常是它的1.5倍左右(额外包含堆外内存和内存开销)。
- spark.executor.instances:在动态分配关闭时,需要固定的Executor数量。
4.3 为什么Executor分配1个vcore反而是合理的
热词里有一个场景很典型:“在某些Spark作业中,Executor在Yarn上运行时,每个Container只分配了一个vcore,而且怎么调参数都没用”。这个问题最初困扰了我很久,后来排查出原因就跟大家在讨论的一样:不是配置失效,而是资源队列的计算逻辑限制。
需要明白一点,Yarn调度器中的vcore和物理CPU核不是一个概念。当队列中设置的每个Container最大能申请的核心数不足,或者作业提交时的Hadoop客户端把CPU资源需求设定得很低时,Spark注册给Yarn的资源请求就会被限制在1个vcore。
从Spark角度看,每个Executor的任务并行度由spark.executor.cores决定。如果你给Executor设置了1个core,它只能跑1个并行Task。假设你有几十个Executor且每个只有1个core时,并不是特别低效,因为分区会被分散到大量Executor上,每个Executor内只有单任务串行执行。实际中如果发现任务并行度不够且CPU资源充足,可以调大spark.executor.cores,但不要调到6以上,因为会导致HDFS写入出现大量并发写线程,反而增加磁盘I/O压力。
这里有个容易忽略的点:不管你在spark-submit如何设置,如果队列配置了资源上限,或Hadoop的Capacity Scheduler和Fair Scheduler限制了每位用户的最大资源量,最终分配结果会被Yarn强行收敛。查看日志里“Allocated container resources”的描述能明显看到实际分配的情况。
4.4 部署模式如何选择
- local模式:通常用于本地开发和调试。
- Standalone模式:包一套独立的Master-Worker集群,适合小规模部署和学习。
- Yarn模式:生产标准选择,与Hadoop共存,统一调度。
- K8s模式:新版本的集成方式,适合云原生改造和弹性伸缩。
5. 内存管理、RDD持久化与作业性能调优
5.1 Executor内存区域的划分逻辑
Spark OOM是高频搜索词,课程最后阶段老师都会把堆内存结构摊开来教。Executor内存划分为多个区域,新版Spark(3.0以后)引入了统一内存管理模型。
堆内部分主要有三块:Reserved Memory(系统保留)、User Memory(用户数据结构)和Spark Memory(Spark统一管理)。而Spark Memory又分成Storage Memory(缓存RDD/DataFrame数据)和Execution Memory(执行时的Shuffle、聚合等操作使用),两者可以互相抢占。
记忆口诀是:缓存数据和执行计算共用一块内存,平时按比例划分,当一边空闲时另一边可以临时借用。但借用有个代价,如果缓存方需要收回被执行方借用的内存,执行方必须把中间结果溢写到磁盘,这个过程会引发额外性能损耗。
看到Executor的OOM报错时,根据报错位置能大致推断原因:
- 如果报错来自SQL操作里的Aggregation或Join,基本是Execution Memory不足,同时也可能是Shuffle相关数据量过大导致堆外内存不足。
- 如果报错来自“No space left on device”或者“Container killed on request”,多半是磁盘或者超出Yarn容器物理内存了。
- 如果发生在collect阶段,那十有八九是Driver端内存装不下拉回来的结果集。
5.2 persist和cache的“骗局”
cache本质上就是调用persist(StorageLevel.MEMORY_ONLY)。在迭代或复用循环中,RDD会被多次用到,此时如果每一次都从头按血缘关系计算,成本非常高。cache的用意是帮助你把第一步计算完的结果留在内存中,后续所有分支操作共用这份结果。
课程作业里最经典的例子是K-Means迭代。如果不做cache或persist,每一轮迭代都重新做一遍文本解析和数据清洗,效率极其低下。持久化还会遇到一个问题:当你看到Storage tab里有数据被缓存时,它可能是以序列化二进制存储的,虽然节省内存,但是每次读取缓存数据都需要反序列化,CPU开销会有增加。
5.3 Spark调优的核心维度
- 并行度设置:涉及初始分区的数量,读取HDFS时的分区数由block数决定。如果文件较小,考虑使用spark.sql.files.maxPartitionBytes之类的参数控制读取量;如果通过并行度参数控制Shuffle读端的并行度,建议设置为Executors数量乘以cores的2到3倍,这样能让跑得快的Executor去多承担一些任务,以填补网络或计算较慢的成员留下的空白。
- 序列化方式:Kryo比Java序列化更快更精简。虽然Spark很多内置类型对Kryo支持很好,但自定义类需要提前注册。生产环境里,开Kryo并关闭Unsafe系列,通常会带来可感知的性能提升。
- 合并小文件:在最终数仓落盘阶段,如果由大量的低并行度任务输出极小的数据碎片,会严重影响下游读取性能。这时候要考虑用coalesce或repartition来调节文件数量。coalesce可以尽量避免Shuffle,但它只支持减少分区;repartition可以增加或减少分区,但它会触发完整的Shuffle。
- 数据倾斜处理:先定位倾斜的Key,然后采用加盐打散、局部聚合加全局聚合策略。核心思路是让数据量大的key被拆开到更多的分区中去,先在各分区内部完成局部聚合,再将这些局部结果按原始key做二次合并。
5.4 内存参数计算示例
配置Executor内存时,有一个非常关键的换算逻辑:Yarn会给每个Container申请的总内存不只是spark.executor.memory,还包括额外的Java堆外内存。Executor在运行时会启动很多非堆的结构,比如线程栈、元空间、网络缓冲。如果你给executor-memory设为4g,实际Yarn分配的内存往往要达到6g左右。当总申请量超过队列或单节点可分配资源时,任务就卡在ACCEPTED状态不往下跑了。
一个比较严谨的做法是:先考虑每个节点的物理内存和可分配给Yarn的大小,再决定每节点最多放几个Executor。比如一个有128GB物理内存的节点,预留系统约20%,如果Yarn分配100GB给Container,那设置executor-memory=8g时最好加堆外3g左右,最多放7个左右的Executor。这时spark.executor.cores不要设置太大,避免单节点上的多Executor间出现CPU竞争与资源互相挤占。
6. 作业提交流程与常见错误排查实录
6.1 一个完整作业的提交与执行流程
从键入spark-submit命令到最后得到输出,内部经历了好多步。按时间线去理解,问题定位会快很多:
- 第一步:客户端启动并提交Jar和相关配置文件到Yarn ResourceManager。
- 第二步:ResourceManager分配一个Container用来启动ApplicationMaster(AM)。
- 第三步:Spark的AM进程在Container内运行,向RM申请资源用于启动Executor。这里AM就是运行Driver的地方(cluster模式下)。
- 第四步:每个Executor启动完成后,会反向注册到Driver,并建立数据传输通道。
- 第五步:Driver把应用的所有RDD依赖链按作业顺序逐个触发执行。
- 第六步:作业运行完成或失败后,Driver退出并清理所有资源。
在排查“作业为什么一直处于RUNNING但没有任何Task执行”的问题时,我要提醒你自己看一眼是哪个阶段卡住了。大概率是Driver或AM在做资源申请等待。但还有一种常见原因是数据表的读取环节出了状况,比如从SQL里扫描的数据源所在目录做了大量小文件的统计,甚至Hive元数据拉取卡住,导致Driver侧长时间停在那里。
6.2 常见问题排查速查表
下面就是我基于日常作业和开源社区里的高频问答整理出来的问题速查表。你可以打印出来贴在电脑旁。
| 问题现象 | 常见原因 | 解决方案 |
|---|---|---|
| 任务一直ACCEPTED不启动 | 队列资源不足,申请不到Container | 查看队列使用率;调低Executor内存;检查本次作业申请总量是否超过限制 |
| Executor在Yarn上只能拿1个vcore | Yarn队列/调度器最大核数限制或客户端配置过低 | 调整队列配置或提交参数;查看Yarn页面确认实际分配的资源 |
| Container运行一段时间后被kill | 物理内存超过Container限制 | 调大executor-memoryOverhead或整体内存;降低Executors单节点数量 |
| Shuffle过程极慢 | 数据倾斜、分区数过少或磁盘I/O瓶颈 | 增加并行度;调整分区Key;开启Shuffle服务 |
| Stage重试次数很多 | 某些节点节点故障或Executor异常退出 | 查看日志确认原因;开启RDD缓存;检查网络环境 |
| Spark作业慢但CPU没跑满 | 数据本地性级别不佳或分区过大 | 调整spark.locality.wait;增大分区数量 |
| collect拉取结果时Driver OOM | 结果全量回收到Driver,数据量过大 | 改用分区处理或把省数据写回外部存储;调整Driver内存 |
6.3 典型OOM案例复盘
一次离线任务中,遇到一个“Executor Lost”的报错,最开始以为是GC频率太高、长期GC停顿导致Executor被误判为失联。查看监控后发现并非如此,真正的根因是数据量在join时膨胀得太严重,导致执行端内存峰值冲破Yarn分配上限。最后采取的办法是把一个超大RDD的建表逻辑拆分,先按过滤时间缩小数据集,又对join字段做了预聚合,让进入大join的数据量少了近七成;与此同时打开spark.shuffle.service.enabled并提升executor-memoryOverhead容量值,才把任务稳定跑完。
排查OOM,先看是不是数据结构没预估好。因为只要有一步全量收集,数据膨胀倍数可能达到几十倍,无论你申请多大的堆内存都经不起这种指数级扩张。
7. 期末考试高频考点与题型思维导引
7.1 “背概念”型题目的答题逻辑
期末考试中概念题并不可怕,最怕你知道关键词,但不知道怎么组织成得分点。我建议答题按“定义 + 背景/解决什么问题 + 核心机制 + 举例/局限性”的方式分层次展开。
举个例子,若问“请说明Spark中Stage是如何划分的”,标准答法应该是:先写在一个Job内部,DAG Scheduler会从触发计算的最后一个RDD开始回溯DAG;一旦遇到宽依赖(Shuffle依赖),就生成一个新的Stage,上游和下游之间以Shuffle为界;每个Stage内部的算子连接形成的流水线可以连续任务串行执行,直到对应分区计算完成。这种答题思路既体现了原理又兼顾机制细节。
7.2 容易被扣分的细节考点
- Spark Streaming接收数据的机制:分为Receiver方式与Direct方式。Direct方式下,每个批次的offset范围由Driver主动控制,因此容错性更强。考试大概率会问你两者在“精准一次消费”能力上的差异,那是Direct。
- Checkpoint的作用:它除了切断RDD的漫长血缘关系,还能在流计算中做状态管理。课程最后阶段会讲到,实时作业如果要做有状态计算,必须要有Checkpoint机制来保存历史状态。
- Task端和数据本地性的级别:数据本地性分为PROCESS_LOCAL、NODE_LOCAL、RACK_LOCAL和ANY。数据本地性级别从高到低,但并非每个作业都能拿到最高级的本地性,当拿不到时,调度器会等待一个策略级别的时间窗口,这个时间参数可以调。
- 二次排序的实现方式:经典方案是自定义Key,然后实现Ordered接口或手动定义排序比较规则;另一个思路是对value进行嵌套排序,即在sortWith算子前后做map端信息的记录。前者适合面试题深度展开。
7.3 大题实操题型做题顺序建议
期末上机考试基本集中在编写包含多个算子的数据分析案例。见到此类题目,千万别拿到题就急着写代码,先做短暂规划,再动手效率反而更高。
第一步,明确输入数据的格式以及各字段含义。第二步,把业务需求拆解为多个子任务步骤,确定哪些操作需要全局聚合,哪些可以按分区独立完成,依此初步确定是否需要Shuffle去组织数据流转。第三步,根据需求选择执行方式:能用DataFrame的API解决的尽量不写自定的流程逻辑;能用内置函数实现的,不写自定义UDF。第四步,写出代码后再检查是否有不必要的Shuffle,比如先count再filter就应该换成先filter再count。第五步,作业跑通后若数据输出规模大,不要直接collect,要使用take、show或保存文件,防止Driver被撑爆。
上机考试还有一个隐形扣分点是忽略输出格式。很多Spark题目要求你一定格式输出,比如按价格降序排列,保留两位小数,这要求在返回前自行处理排序和格式化,而不是交给后续无关的字符串拼接。
8. 从期末复习延伸到面试与大厂生产实践
8.1 课程重点与面试真题的对应关系
知识如果只是应付期末考,考完交卷就忘记了,挺可惜的。期末复习大纲中的几乎所有经典题目,其实都是从常见面试题里复制过来的。面试题中的高频考点主要集中在Shuffle机制、数据倾斜、Spark内存管理和RDD持久化策略上。
例如两道经常遇到的面试题:
- Spark OOM时应该调整哪些参数? 可以从提高Executor内存、增加分区数、减小单次读取数据量、开启Kryo序列化以降低内存占用等维度来展开回答。
- Spark读取JDBC速度慢,有什么优化方案? 可以设置合理分区数;按数值range分片;并发读取;将大查询拆分成多台并行读取。
8.2 保持对Spark演进趋势的敏感
课程说的都是Spark 3.2或3.3为主,但Spark的发展从来没有停下。新版Spark引入了Adaptive Query Execution(AQE),它可以动态调整Reduce分区数、自动处理数据倾斜、自动把SortMergeJoin转换为BroadcastJoin,让SQL优化不再死板地靠人工。此外Predicate Pushdown和弃用Hive的向量化读取计划也让大批量查询的处理速度有了很大提升。
如果你以后要接触DGX Spark之类的软硬一体方案,本质上仍是熟悉的分布式计算的道理,只是底层硬件架构全面转向GPU加速和RDMA网络。了解本质再迁移,面试官反而会对你刮目相看,因为你是真正理解这套逻辑的人,而不是只会死记API。
8.3 不同岗位方向的学习侧重建议
- 想做数据开发工程师,重心放在Spark SQL、读写优化、调优和故障定位。
- 想做实时计算,重心放在Structured Streaming的状态管理、输出模式和容错机制。
- 想做引擎内核方向,RDD调度逻辑、Shuffle管理器实现、内存模型这些非常细节的源码,必须提前啃。
- 如果想做架构,那么横向扩展能力、数据血缘设计、任务优先级与队列策略,分层治理等更需要深耕。
9. 高效复习方法与考前冲刺策略
9.1 三轮复习法的具体安排
Spark内容杂,只靠考前两三天突击效率并不高。可以按三轮法去推进:
第一轮是快速遍历。对照教学大纲与平时作业,把每个章节的核心概念过一遍,不看细节,只看条目和流程。目标是搞清楚“每章讲什么、哪些知识点之间有关联”。
第二轮是专题整合。按“调度机制”“资源管理”“SQL优化”“内存模型”“流计算”等主题重新整理笔记。整理方式推荐横向表格,比如把Standalone/Yarn/K8s三种部署模式的Driver位置、资源申请流程、适用场景等并排放一起对比。
第三轮是查漏补缺。拿出平时练习中错过和作业报错过的点,把这些真实案例和对应的知识点挂上钩。比如有一次因为Kryo没有注册类导致任务在Executor端退化的案例,这比背十遍序列化章节都管用。
9.2 刷题与上机备考资料建议
复习期末最怕资料太杂,其实只需要保留这几类:
- 官方文档的Programming Guides,尤其是RDD、SQL、调优相关章节。官方文档写得比绝大多数二手资料清楚得多。
- 一份高频面试题集锦,常看常新,除了正文之外也适合碎片时间翻。
- 自己平时练习代码中踩过的坑集合。针对每个错误,给自己罗列出原因、现象、解决方式。
- 对集群资源的分配信息有一个模糊的核算回忆,考场做题会更踏实。
9.3 考前一周的复习节奏
最后一周我不建议再看新知识了,建议把精力放在这三件事上。
第一件事,把Spark程序“从提交到运行结束”的整体流程,完整口述出来,说给自己听。第二件事,把常见的几个Java/Scala操作示例代码从头默写一遍。第三件事,把关键参数和提交命令默写一遍,尤其是spark-submit常见参数和Spark启动时配置的核心参数。考场上只要核心流程理清了,很多推导性的问题自然能解决。
我复习到后半阶段最大的体会是,用“资源分配与执行流程”这条主线去理解Spark,远比背参数列表要深刻得多。内存怎么分?Executor和Driver各干什么?作业分几个Stage执行?这些环节都有因果关系。一旦把这些底层逻辑理清了,应付考试绰绰有余,而且对未来的大项目也有很大帮助。
10. 最后分享一个亲身复习技巧
期末复习过程中最让我受益的,其实不是照搬任何人的总结,而是自己动手把“一个带Shuffle的Word Count从提交代码到结果返回”的完整过程画了下来,画出DAG、标出Stage切分点,再将每个Stag对应的Executor做的工作列出来。这幅图一画出来,很多题目就豁然开朗了。
最后再分享一点:Spark那些诸如executor内存等参数,如果平时不亲自改一改、不刻意调小到让任务失败一次,考试前你可能很难体会到内存参数对运行效率的影响有多严重。遇到问题日志不要怕,日志里已经写明了绝大多数出错原因,Debug能力也是Spark课程核心素养的一部分。
