你写的每一行Spark代码,大概率什么都没干。不是报错,是真·什么都没干——没读数据,没跑CPU,没碰硬盘。四行查询语句,零字节处理,这在Pandas里早就崩了,Spark却把它当成设计哲学。
这就是惰性求值(Lazy Evaluation)。2016年Spark 2.0统一DataFrame和SQL引擎时埋下的伏笔,在2026年1月9日发布的4.1.1版本里终于兑现成十年来最大的一次架构跃迁。
代码写了,数据没动
看这段典型代码:
df = spark.read.parquet('s3://uber/trips/') filtered = df.filter(col('country') == 'US') joined = filtered.join(drivers_df, on='driver_id') result = joined.groupBy('city').agg(sum('fare').alias('revenue'))
四行。零执行。Spark只在Driver内存里搭了棵"操作树"——你告诉它要读哪、滤哪、怎么join、怎么聚合,它点头记下了,转头去喝咖啡。没有Executor被唤醒,没有Partition从S3拖下来。
直到你写result.show(20)或result.write.parquet(...),这才叫Action。Action是扳机,之前全是待击发的子弹。
为什么自找麻烦?因为"立即执行"是性能杀手。如果Spark见一行跑一行,它看不到三行之后有个filter能砍掉90%数据,也想不到把三个窄变换(narrow transformation)塞进同一趟扫描。惰性求值给了它"偷看"全剧本的机会——这是所有优化的前提条件。
从操作树到执行图:DAG的暗箱
Spark把惰性攒下来的操作树,翻译成DAG(有向无环图,Directed Acyclic Graph)。节点是操作,边是数据依赖。窄依赖(子分区只依赖父分区一个)和宽依赖(子分区依赖父分区多个,即Shuffle)被标记得清清楚楚。
窄变换可以流水线执行:map完直接filter,内存里过一遍就完。宽变换必须切Stage——每个Shuffle是一道坎,数据要落地、排序、重分区。2016年之前,工程师得手动数Stage猜性能;现在Spark 4.x的UI把DAG画成可交互的火焰图,但核心逻辑没变:Shuffle是性能黑洞,DAG是避坑地图。
这里藏着个反直觉的点:你写的代码顺序,和实际执行顺序,可能毫无关系。Spark会重排操作,把filter往前拽、把select往下压,只要语义等价。这叫谓词下推(Predicate Pushdown)和投影下推(Projection Pushdown)——你的代码是"建议",不是"命令"。
Catalyst:SQL引擎的"编译器思维"
2015年Spark 1.3引入Catalyst优化器,把DataFrame查询当成编译器优化问题来解决。四步走:
1. 分析(Analysis):校验列名、类型,绑定元数据 2. 逻辑优化(Logical Optimization):常量折叠、谓词下推、列剪枝 3. 物理计划(Physical Planning):生成多个候选执行方案,基于成本选最优 4. 代码生成(Code Generation):把计划编译成Java字节码,砍掉虚函数开销
最后一步叫全阶段代码生成(Whole-Stage Code Generation),2015年Spark 2.0的杀手锏。传统火山模型(Volcano Model)每行数据都要虚函数调用,Tungsten项目把它拍平成单条for循环——CPU缓存友好,向量化执行。
「Catalyst让Spark从"解释执行"跳进"编译执行"的门槛」,Databricks联合创始人Reynold Xin在2015年的设计文档里写道。这句话的代价是:你写的Python/R代码,最终被翻译成JVM字节码在跑。PySpark的UDF曾经是性能噩梦,直到Arrow格式和Pandas UDF出现才缓解。
AQE:运行时改剧本
2016年的优化全是"静态"的——计划敲定,执行开始,不再回头。但数据分布可能和统计信息完全脱节:某个key的倾斜让单个任务跑一小时,某个filter的实际选择性比预估好十倍。
Spark 3.0在2020年引入自适应查询执行(Adaptive Query Execution,AQE),2021年3.2版本默认开启。三个核心能力:
- 动态合并Shuffle分区:跑完Stage发现数据量远低于预期?把下游分区数砍半,减少空转 - 动态切换Join策略:广播小表比Shuffle Join快?中途改计划 - 动态优化倾斜Join:检测到倾斜key,自动拆分重试
AQE的触发条件是Shuffle边界——每个Stage结束,Spark偷看一眼实际统计,再决定下一Stage怎么玩。这打破了"一次编译,到处执行"的传统数据库教条,代价是延迟增加(要等上一Stage完),收益是鲁棒性飞跃。
到Spark 4.0,AQE和Catalyst的边界开始模糊。2024年发布的4.0预览版引入"运行时优化器扩展点",允许插件在执行中途注入自定义规则。这意味着第三方可以写自己的倾斜处理策略,或者针对特定存储格式做动态下推。
Spark 4.x:统一API的终局
2026年1月9日的4.1.1版本,标志着一个十年周期的收尾。2016年Spark 2.0把RDD、DataFrame、SQL塞进同一套Catalyst引擎;2020年Spark 3.0用DataFrame API完全覆盖Dataset(Scala专属的类型安全层);2024-2026年的4.x系列,则在抹平流批界限。
Structured Streaming在3.x时代已经是"微批模拟流",4.x引入真正的Continuous Processing Mode(连续处理模式),延迟从百毫秒压到十毫秒级。更隐蔽的变化是API统一:同一个df.writeStream和df.write,底层根据模式自动选引擎,用户无感知。
这对25-40岁的技术从业者意味着什么?你2018年写的Spark SQL作业,2026年可以无缝跑在流模式上,不改代码。但代价是:你得理解惰性求值在流场景下的新陷阱——Watermark和输出模式的交互,让"什么时候触发Action"变得比批处理复杂一个数量级。
RDD没死,只是隐身了。DataFrame的每个操作最终翻译成RDD执行计划,但4.x的优化器越来越倾向于绕过RDD的JVM对象开销,直接用Arrow格式和向量化算子。除非你要做细粒度的分区控制或自定义优化器,否则没必要碰RDD API——这是Spark 4.x文档里的原话,也是2016年2.0时代"RDD已死"宣言的温和版。
为什么现在必须升级
Spark 3.x的支持周期到2025年底结束。4.x的默认配置变了:AQE全开、动态分配资源默认启用、Kubernetes调度器取代YARN成为推荐部署模式。这些不是"新功能",是"新假设"——社区开发新特性时,默认你开了这些开关。
一个具体例子:4.1引入的"内存自适应执行"(Memory Adaptive Execution),在3.x上跑会OOM的查询,4.x能自动 spill 到磁盘并调整内存分数。但前提是AQE开启,且你用的是统一内存管理(Unified Memory Management)——2016年2.0引入、3.x可选、4.x强制的配置。
惰性求值的老哲学没变,但"什么时候触发优化"的答案变了。以前是逻辑计划阶段,现在是"逻辑+物理+运行时"三阶段联调。工程师的学习曲线被拉长,但调优天花板被抬高。
最后留个细节:Spark 4.1.1的发布说明里,有个 buried in the changelog 的改动——spark.sql.adaptive.coalescePartitions.enabled的默认值从false改成了true。这意味着动态分区合并不再是"进阶优化",而是出厂设置。你的旧作业可能突然变快,也可能因为分区数过少而内存爆炸——取决于数据分布。
你上次检查Spark UI里的"Optimized Logical Plan"和"Physical Plan"差异,是什么时候?
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.