性能诊断:Spark UI、Event Log 与调优思路
上一篇解决了动态资源分配和调度策略。这一篇进入性能诊断——当 Spark 作业慢了,怎么找到瓶颈。
性能调优的第一步不是改配置,是定位问题。Spark UI 提供了五个关键页面(Jobs、Stages、Storage、Environment、Executors),每个页面指向不同类型的性能问题。Event Log 提供了离线分析的完整数据。
本文只抓一个问题:面对一个慢作业,如何从 Spark UI 的指标出发,沿着诊断路径找到根因。
Spark UI 五个页面
1 | |
四条诊断路径
1 | |
诊断路径一:数据倾斜
数据倾斜是 Spark 作业最常见的性能问题。表现为:某个 Stage 的大部分 Task 在几秒内完成,但有一两个 Task 运行了几十分钟。
在 Stage 详情页的 Summary Metrics 中:
1 | |
如果 Max 和 Median 差距超过 10 倍,基本可以确认数据倾斜。点击 Max 对应的 Task 可以看到它处理的 Shuffle Read 数据量和具体的执行时间线。
应对策略:使用 AQE 的 Skew Join 优化(自动);对倾斜 key 加盐(salting)后分散到多个分区;对小表用广播 Join 避免 Shuffle。
诊断路径二:Shuffle 过多
Shuffle 是 Spark 中最昂贵的操作。如果一个作业有过多的 Shuffle Stage,或者 Shuffle 数据量远超输入数据量,需要检查是否有不必要的 Shuffle。
在 Stages 页面关注 Shuffle Write 和 Shuffle Read 两列:
1 | |
另一个指标是 Shuffle Spill (Disk):如果大于 0,说明 Shuffle 数据量超过了内存缓冲区,溢写到了磁盘,增加了额外的 I/O。
诊断路径三:内存不足
GC 压力是 Spark 作业变慢的常见原因。在 Executors 页面查看每个 Executor 的 GC Time:
1 | |
GC Time 占总执行时间的 10% 以上就需要关注。30% 以上说明 Executor 的堆内存严重不足。
应对策略:增加 Executor 内存(–executor-memory);使用 MEMORY_ONLY_SER 减少对象数量;切换到 Kryo 序列化减少内存占用;增加分区数减少单个 Task 的内存消耗。
诊断路径四:序列化瓶颈
在 Stage 详情页的 Summary Metrics 中,Task Deserialization Time 表示 Task 从网络接收到反序列化完成的耗时:
1 | |
如果反序列化时间占比超过 10%,考虑切换到 Kryo 序列化(spark.serializer=org.apache.spark.serializer.KryoSerializer)。Kryo 比 Java 默认序列化快 10 倍左右,序列化后的数据大小也更小。
Event Log 离线分析
Event Log 记录了 Spark 作业执行过程中的所有事件(Job 开始/结束、Stage 提交/完成、Task 执行信息等),写入到 spark.eventLog.dir 配置的目录。
1 | |
History Server 启动后(sbin/start-history-server.sh),在 Web 界面可以浏览所有已完成作业的 Spark UI,功能和运行时 UI 完全相同。
实验:诊断一个倾斜作业
1 | |
模式提炼
Spark 的性能诊断体现了指标驱动的调优模式:不是凭直觉改配置,而是先观测指标、定位瓶颈、针对性优化、验证效果。
这个模式在所有性能工程中通用。数据库调优看 EXPLAIN ANALYZE 的执行计划和 I/O 统计。JVM 调优看 GC 日志和堆转储。网络调优看 RTT、丢包率和带宽利用率。Web 性能调优看 Chrome DevTools 的 Waterfall 图和 Lighthouse 报告。
核心原则不变:measure first, optimize second。
工程迁移表
| Spark 诊断工具 | 数据库对应 | JVM 对应 | Web 性能对应 |
|---|---|---|---|
| Spark UI | pgAdmin / MySQL Workbench | VisualVM | Chrome DevTools |
| Stage 详情(Task 分布) | EXPLAIN ANALYZE | 线程转储 | Request Waterfall |
| Event Log + History Server | 慢查询日志 | GC 日志 | Access Log |
| Shuffle Read/Write 指标 | I/O 统计(pg_stat_io) | 堆内存分配速率 | 网络传输大小 |
| GC Time | 无直接对应 | GC Pause Time | Time to Interactive |
常见误解
误解一:“Task 数等于并行度,增加 Task 数总能加速”。Task 数增加到超过可用 CPU 核心数后,额外的 Task 只是排队等待。而且 Task 太多会增加调度开销。最优 Task 数约等于 Executor 总核心数的 2-3 倍。
误解二:“GC Time 高就加内存”。GC 问题的根因可能不是内存总量不足,而是对象数量过多。如果大量短生命周期的小对象导致 Young GC 频繁,增加 Executor 内存不会改善——改用序列化存储(MEMORY_ONLY_SER)或减少中间对象分配更有效。
误解三:“Spark UI 显示的 Duration 就是实际计算时间”。Duration 包含了调度等待时间、反序列化时间、GC 暂停时间和实际计算时间。需要结合 Executor Computing Time、GC Time、Task Deserialization Time 几个子指标才能分辨瓶颈所在。
练习
-
对同一个作业分别用 Java 序列化和 Kryo 序列化执行,在 Spark UI 中对比 Task Deserialization Time 和 Shuffle Write 数据大小。
-
构造一个内存不足的场景(executor-memory 设很小),观察 Executors 页面的 GC Time 占比和 Stage 详情的 Spill 指标。然后逐步加大内存,记录 GC Time 的下降曲线。
-
在 Spark History Server 中找一个之前执行的作业,定位最慢的 Stage 和该 Stage 中最慢的 Task。记录该 Task 的 Shuffle Read 数据量、Duration 和 GC Time,判断瓶颈类型。
系列导航
| 序号 | 主题 | 状态 |
|---|---|---|
| 00 | 导读:为什么 Spark 的核心是一张 DAG | 已完成 |
| 01 | RDD:弹性分布式数据集的五大属性 | 已完成 |
| 02 | 宽依赖与窄依赖:Stage 是怎样划分出来的 | 已完成 |
| 03 | DAG Scheduler 与 Task Scheduler:从逻辑计划到物理执行 | 已完成 |
| 04 | Shuffle 机制:数据跨分区交换的代价与优化 | 已完成 |
| 05 | 内存管理与 Tungsten:堆外内存、序列化与代码生成 | 已完成 |
| 06 | 存储体系:Block Manager、广播变量与累加器 | 已完成 |
| 07 | 容错机制:Lineage、Checkpoint 与推测执行 | 已完成 |
| 08 | Spark SQL 与 Catalyst:从 SQL 文本到物理算子树 | 已完成 |
| 09 | DataFrame 与 Dataset:类型安全与性能的折中 | 已完成 |
| 10 | Adaptive Query Execution:运行时改写执行计划 | 已完成 |
| 11 | 数据源 API 与谓词下推:让存储层帮忙过滤 | 已完成 |
| 12 | Structured Streaming:把流当成无界表 | 已完成 |
| 13 | 水位线与状态管理:流处理的时间语义 | 已完成 |
| 14 | Spark on YARN / K8s:资源管理与部署模式 | 已完成 |
| 15 | 动态资源分配与作业调度:多租户集群的资源博弈 | 已完成 |
| 16 | 性能诊断:Spark UI、Event Log 与调优思路 | 本篇 |
| 17 | Spark 与 Flink:两种计算引擎的设计选择 | 下一篇 |
参考资料
- Apache Spark 官方文档:Monitoring and Instrumentation. https://spark.apache.org/docs/latest/monitoring.html
- Apache Spark 官方文档:Tuning Spark. https://spark.apache.org/docs/latest/tuning.html
- Databricks Blog: How to Read Spark UI. https://www.databricks.com/blog
- Bill Chambers, Matei Zaharia. Spark: The Definitive Guide. O’Reilly. Chapter 18: Monitoring and Debugging.
