Spark SQL 离线任务性能调优实战
以少读少传少倾斜为核心目标,从执行计划分析倒推 SQL 改写、AQE 参数与资源配比的完整优化路径
Spark SQL 离线任务在数据量膨胀后,最典型的症状是:长尾 Task 拖垮整体完成时间、Shuffle 数据量暴涨导致磁盘 I/O 打满、单个 Stage 因数据倾斜直接 OOM。这些问题的根源可以归结为三个方向——少读、少传、少倾斜。围绕这三个目标,从执行计划分析倒推到 SQL 改写和参数配置,是一条可复制的调优路径。
用 EXPLAIN 找到优化切入点
调优的第一步不是改参数,而是读懂执行计划。对任意 SQL 执行 EXPLAIN FORMATTED,重点关注三处:全表扫描是否被谓词下推过滤、SortMergeJoin 是否应该换成 BroadcastJoin、以及 Shuffle 分区数是否合理。如果执行计划中出现 Scan parquet 但没有 Filter 节点,说明谓词没有下推——大概率是数据类型不匹配(如分区键是字符串但查询用了数字)导致的。修正类型后,扫描数据量可能直接缩小一个数量级。
另一个常见问题:两表 JOIN 时如果一张表足够小(小于 spark.sql.autoBroadcastJoinThreshold,默认 10MB),执行计划却仍然走了 SortMergeJoin。检查阈值设置是否被任务级参数覆盖,或者小表是否因为嵌套结构导致序列化后体积膨胀。改用 BroadcastJoin 可以省掉一次 Shuffle。
少读:谓词下推与列裁剪
"少读"的含义不只是少读数据行,还包括少读列。Parquet 和 ORC 格式天然支持列裁剪(column pruning),但前提是 SELECT 中不写 SELECT *。在宽表场景下,只读需要的 5-10 列 vs 全表 200 列,I/O 差距可达 10 倍以上。
分区裁剪是另一个大杀器。确保 WHERE 条件中包含分区键,并且分区键的值与表定义类型一致。一个常见的坑:Hive 表的分区键是 STRING 类型,但查询时写成了 WHERE dt = 20250101(没有引号),导致分区裁剪失效,退化为全表扫描。
离线开发 BatchWorks
支持多模态、覆盖集成开发调度运维的一站式离线数据开发平台
少传:Shuffle 优化与资源配比
Shuffle 是 Spark SQL 最昂贵的操作之一。减少 Shuffle 的核心手段有三个:调整 spark.sql.shuffle.partitions(默认 200)、使用 Bucket 表、以及开启 AQE(Adaptive Query Execution)。
默认 200 个分区对于小数据量来说太多——每个 Task 的数据量太小,Task 调度开销反而成了瓶颈;对于大数据量来说又太少——单个 Task 处理的数据过大导致 OOM。一个经验法则是:让每个 Task 处理的数据量控制在 128MB-256MB 之间。你可以先跑一次任务,看 Stage 的 Shuffle Write 总量,再除以 256MB 来估算合理的分区数。
AQE 从 Spark 3.0 开始默认启用,它会在运行时动态调整分区数(合并过小的分区)和 Join 策略(将 SortMergeJoin 动态转为 BroadcastJoin)。确保 spark.sql.adaptive.enabled=true 和 spark.sql.adaptive.coalescePartitions.enabled=true 已开启。
资源配比方面,每个 Executor 的 CPU 核心数建议设为 2-4。核心数太少会导致并发 Task 不足,太多则增加 GC 压力。Executor 内存设为 4GB-8GB 较为合适,但要注意 num-executors × executor-memory 不能超过队列总内存。Driver 内存通常 1GB 就够,除非你需要 collect 大量数据到 Driver 端。
数据治理
统一标准、质量、安全与资产管理,沉淀高质量数据集,支撑经营决策、监管合规与 AI 应用。
少倾斜:数据倾斜的三种解法
数据倾斜是最棘手的问题——某些 Key 的数据量远超其他 Key,导致少数 Task 处理的数据量是其他的几十倍甚至上百倍。
解法一:加盐打散。对倾斜 Key 追加随机后缀(如 key + "_" + random(0,10)),使原本集中在一个 Task 的数据分散到多个 Task 处理。聚合后再去掉后缀做二次聚合。适用于单表 GROUP BY 场景。
解法二:两阶段聚合。先按 Key 的子集(如日期)做预聚合,减少中间结果量,再做最终聚合。适用于可以容忍近似结果的场景。
解法三:BroadcastJoin 替代。如果倾斜发生在 JOIN 场景,且其中一张表较小,直接将小表广播出去可以彻底避免 Shuffle,倾斜问题自然消失。
判断是否倾斜的最直观方法是看 Spark UI 中 Stage 页面的 Task 耗时分布——如果 90% 的 Task 在 10 秒内完成,但有几个 Task 跑了 5 分钟以上,几乎可以确认是倾斜。
常见失败模式
过度序列化反而变慢:有些团队为了省内存,把 spark.serializer 改成了 Kryo 但没有注册自定义类,导致每次序列化都需要反射查找类信息,性能反而下降。
executor-cores 设太高:设为 8 核以上时,单个 Executor 内并发运行 8 个 Task,GC 时所有 Task 同时停顿,长尾效应更明显。
忽略广播表大小阈值:把 spark.sql.autoBroadcastJoinThreshold 调到 1GB 甚至更大,希望所有 JOIN 都走 Broadcast——但如果 Executor 内存放不下广播表,会直接 OOM。
在 BatchWorks 离线开发平台上,执行计划分析和 AQE 参数都可以通过图形化界面一键配置和对比,省去了反复提交 Spark 作业验证参数效果的时间。对于日均数百个离线任务的团队,这个效率提升是实打实的。
Spark SQL 调优没有银弹,但"读 EXPLAIN、算数据量、定分区数、查倾斜"这四步,覆盖了 80% 以上的性能问题。先用 EXPLAIN 排除执行计划层面的低级错误,再用 AQE 兜底运行时的动态调整,最后针对性地处理数据倾斜——这是性价比最高的调优顺序。


