订阅更新

Spark 为什么惰性执行?多个 Action 会重复计算吗?

面试问 232 约 3 分钟读完

本文目录
  1. 面试怎么回答
  2. 惰性执行让系统先看到计算关系
  3. 变量保存的是计算对象,不是计算结果
  4. 缓存需要触发,缓存也可能失效
  5. 一个典型场景:为了打印数量多跑一遍
  6. 重算也会暴露非确定性与副作用
  7. 参考资料

面试怎么回答

Spark 的转换主要构建计算依赖,Action 才触发求值。同一 DataFrame 变量不代表结果已经缓存,多个 Action 可能重读和重算;cache 还需要首次计算填充,缓存丢失也会重新计算。我会按复用次数、计算代价和容量决定是否持久化,并用 UI 验证扫描与缓存命中。转换中的外部副作用必须考虑多次执行。

先把这段讲顺;面试官追问时,再展开下面的原理和细节。

详细讲解

写完一连串 map、filter 后,Spark UI 里没有新的计算任务;加了一句 count,任务才开始跑。紧接着再写出结果,又看见同一数据源被扫描了一次。惰性执行与结果复用是两个需要一起讲清的概念。

Transformation 构建依赖,Action 触发求值;多次使用同一变量不等于缓存,是否复用取决于持久化、执行计划和已物化数据是否仍可用。

惰性执行让系统先看到计算关系

对 RDD 来说,map、filter 等转换通常生成新的依赖描述,等到 count、collect 或保存结果等 Action 才执行所需计算。Spark SQL 还可以利用逻辑计划优化过滤、列选择和其他表达式。

这给调度与优化留下了空间,避免每写一行转换就立即读取并落地全部数据。但读取 Schema、元数据推断等准备动作可能提前发生,不能把惰性理解成 Action 之前任何网络或存储请求都绝对不存在。

变量保存的是计算对象,不是计算结果

把清洗后的 DataFrame 赋给 clean,随后执行 clean.count() 和 clean.write,两个动作都需要得到 clean 的结果。如果没有可复用的持久化结果,就可能再次读取源数据和执行转换。

这种情况下,代码层面复用变量只是复用了计算描述。实际执行是否重用某些 Exchange 或中间产物,受计划和引擎机制影响,不能据此承诺任意两个独立 Action 都会共享完整输出。

缓存需要触发,缓存也可能失效

调用 cache 或 persist 通常先声明持久化意图,第一次需要相应分区的计算才填充缓存。对 RDD 使用 take 等只读部分数据的动作时,也不应推断所有分区都已经完成物化。

即使曾经命中过缓存,块被驱逐、Executor 丢失或显式 unpersist 后仍可能重算。是否缓存要比较重复计算的耗时、数据大小和复用次数;一次性使用的大中间表长期占内存,反而可能压缩后续计算空间。

一个典型场景:为了打印数量多跑一遍

比如清洗逻辑包含昂贵解析,开发在写出前增加 count 记录日志,整体耗时明显上升。最初判断是日志服务慢,查看 SQL 与 Stage 才发现解析链被两个 Action 分别触发。

原监控只统计整份应用耗时,没有对应到每个 Action 的扫描量。可以评估是否真的需要预先计数,或在确有复用价值时持久化清洗结果并及时释放。优化后对比两个动作的实际输入和执行时间,验证收益。

重算也会暴露非确定性与副作用

如果转换内部读取当前时间、访问随时变化的接口,或者直接向外部数据库发请求,不同 Action 或 Task 重试可能得到不同结果。惰性和容错意味着计算函数不应依赖“肯定只调用一次”。

建议把不稳定外部输入固定为批次数据,把写入放到支持幂等或事务语义的出口。面试中说明 Action、Job、Stage 也不总是一对一关系更准确:一个动作内部可能触发多个 Job,应该以执行记录为准,而不是按代码行数判断。

参考资料

刷题进度保存在当前浏览器