当你面对Spark应用的性能问题时,Web UI中的Job/Stage/Task视图就像是一张藏宝图,隐藏着无数优化线索。本文将带你像侦探一样,从这些看似复杂的数据中抽丝剥茧,找到性能瓶颈的关键所在。
Spark的执行模型基于三个核心概念:Job、Stage和Task。理解它们的划分机制是性能调优的基础。
这种分层结构直接影响执行效率。例如,一个简单的join操作:
scala复制val df1 = spark.range(0, 10000000).repartition(5)
val df2 = spark.range(5000000, 15000000).repartition(3)
val result = df1.join(df2, "id")
result.count()
这个操作会产生:
在Job页面,重点关注以下指标:
| 指标 | 正常范围 | 异常表现 | 可能原因 |
|---|---|---|---|
| Duration | 与数据量成正比 | 显著长于预期 | 数据倾斜、资源不足 |
| Stages | 根据逻辑确定 | 数量异常多 | 不必要的Shuffle |
| Tasks | 与分区数匹配 | 数量异常 | 分区设置不当 |
典型问题场景:
Stage视图提供了更细粒度的执行信息。重点关注:
Shuffle读写大小:
Task执行时间分布:
GC时间占比:
提示:点击"Event Timeline"可以直观看到Task执行的时间分布,是发现长尾Task的利器
Task视图揭示了执行的最小单元状态:
一个典型的数据倾斜案例:
code复制Task 1: Duration 10s, Input 1.2GB
Task 2: Duration 12s, Input 1.1GB
Task 3: Duration 2min, Input 15.3GB ← 明显异常
数据倾斜是Spark作业最常见的性能杀手。通过Web UI可以快速识别:
识别方法:
解决方案:
scala复制val skewedKeys = Seq("hot_key1", "hot_key2")
val saltedDF = df.withColumn("salted_key",
when(col("key").isin(skewedKeys:_*),
concat(col("key"), lit("_"), floor(rand() * 10)))
.otherwise(col("key")))
Shuffle是Spark中最昂贵的操作之一。优化策略包括:
调整分区数:
scala复制spark.conf.set("spark.sql.shuffle.partitions", "200") // 默认200
合理值应满足:数据总量/分区数 ≈ 100-200MB
选择合适的Shuffle管理器:
bash复制--conf spark.shuffle.manager=sort # 或tungsten-sort
优化序列化:
scala复制spark.conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
通过Task视图可以评估资源利用效率:
| 问题现象 | 优化方向 | 配置参数 |
|---|---|---|
| Task执行时间过短 | 减少分区数 | spark.default.parallelism |
| Executor利用率低 | 调整core数 | spark.executor.cores |
| 频繁GC | 增加executor内存 | spark.executor.memory |
一个资源优化的配置示例:
bash复制spark-submit \
--executor-memory 8G \
--executor-cores 4 \
--num-executors 10 \
--conf spark.executor.memoryOverhead=2G \
--conf spark.default.parallelism=400 \
your_application.jar
原始作业:
优化措施:
优化结果:
问题场景:
优化方案:
scala复制// 合并小文件
spark.conf.set("spark.sql.adaptive.enabled", "true")
spark.conf.set("spark.sql.adaptive.coalescePartitions.enabled", "true")
spark.conf.set("spark.sql.adaptive.advisoryPartitionSizeInBytes", "128MB")
// 广播小维度表
val smallDF = spark.table("dim_advertiser").as("dim")
val largeDF = spark.table("fact_clicks").as("fact")
val joinedDF = largeDF.join(broadcast(smallDF),
$"fact.advertiser_id" === $"dim.id", "left")
优化效果: