一、概述
Spark UI是Apache Spark内置的Web监控界面,为开发者和运维人员提供对Spark应用程序执行过程的实时、可视化洞察。它以直观的方式展示作业(Jobs)、阶段(Stages)、任务(Tasks)、SQL执行计划、Executor资源使用、存储状态及运行时环境等关键信息。通过Spark UI,用户可以快速定位性能瓶颈(如数据倾斜、Shuffle 开销、调度延迟)、分析执行计划、监控资源利用率,并进行有效的调优与故障排查。无论是开发调试还是生产运维,Spark UI都是理解和优化Spark应用不可或缺的核心工具。
二、Spark UI 一级入口
打开Spark UI,就会看到当前的一个Jobs页面,这个页面会记录当前作业中数据的移动,读取等相关动作,除此之外,一级入口还会包括作业运行时的其他属性与指标,主要包括:Stages、Storage、Environment、Executors、SQL 。
一级入口界面如下所示:
首先,我们由简入繁,从衡量任务的整体指标依次介绍各个入口的的功能与作用,首先我们先看Executors,先对作业整体的一个计算负载进行了解。
Executors
Executors Tab主要包括两部分,Summary和Executors两部分,其中Summary是所有的Executors度量指标的加和,而Executors则是描述每一个Executor的详细信息,粒度会更细,方便对每个Executor的情况进行了解:
下面我们对Spark UI对Executors提供的Metrics进行介绍,方便我们对每个Executor节点的运行情况有更好的了解:
Spark UI中的Executors界面是监控和诊断Spark应用运行状态的核心窗口之一,它从执行器(Executor)粒度展示了整个集群的资源使用、任务负载和数据分布情况,以及它们对CPU、内存与磁盘等硬件资源的消耗。基于这些信息,我们可以看到不同的Executor的状态,是否有个别的Executor存在负载不均衡的情况,从而快速的定位问题,例如数据倾斜等。
Environment
这里说一下Environment,显而易见,这里主要展示我们的任务的一些配置项,它主要包括五大环境信息:
通过查看Environment的信息,我们可以快速获取当前任务的配置信息,这里主要查看Spark Properties信息,来去判断当前任务的配置项是否符合我们的预期,从而作出适当的调整,优化任务的性能。
Storage
Spark UI中的Storage界面 是用于监控和管理 缓存数据(Persisted/Cached Data)的核心窗口。它直观展示了哪些RDD或DataFrame被缓存、缓存在哪里、占用了多少资源,是优化内存使用和避免OOM(Out of Memory)的关键工具。
Cached Partitions与Fraction Cached分别记录着数据集成功缓存的分区数量,以及这些缓存的分区占所有分区的比例。而当Fraction Cached没有达到100%时,说明该数据集未能完全缓存在内存,参照spark内存管理可知,此时会出现数据换入换出的情况,显性的说明此时需要参与的计算量大,执行内存会占用缓存内存Size in Memory与Size in Disk,则更加直观地展示了数据集缓存在内存与硬盘中的分布。
SQL
Spark UI中的SQL页面(SQL Tab)是Spark SQL / DataFrame作业的核心监控与优化入口。它专为结构化查询设计,将逻辑执行计划、物理执行过程、性能瓶颈和数据流动以可视化方式呈现,是诊断慢查询、验证优化策略、理解AQE行为的"驾驶舱"。
Stages
Spark UI的Stages界面 是性能调优和故障诊断的核心入口。它从Stage(阶段)粒度展示了Spark作业的执行细节,帮助你精准定位慢任务、数据倾斜、资源瓶颈等问题。
Jobs
Spark UI的Jobs界面 是整个Spark应用监控体系的顶层入口,它以Job(作业)为单位,提供全局视角的执行概览,帮助你快速判断应用整体健康状况、识别失败作业、定位性能瓶颈起点。
至此,我们已对Spark UI导航栏中的各个页面进行了不同程度的解析。整体来看,这些页面可分为两类:
- "详情型"页面:包括Executors、Environment和 Storage: 它们直接展示集群的系统级状态------如计算资源负载分布、运行时环境配置、缓存数据详情等。开发者无需额外跳转,即可快速获取关键的底层信息。
- "概览 + 下钻型"页面:包括SQL、Jobs 和 Stages: 它们首先以列表形式提供作业或查询的高层汇总视图,若需深入分析执行计划、任务分布、性能瓶颈等细节,则需点击进入对应的二级详情页进行下钻探查。
这种分层设计既支持快速概览,又保留了深度诊断的能力,为开发者提供了从宏观到微观的完整观测路径。
三、Spark UI 二级入口
二级入口指的是需要通过一次超链接点击才能进入的详情页面。对于SQL 、Jobs 和Stages这三个主入口而言,其对应的二级页面通常已包含极为丰富的诊断信息------涵盖查询执行计划、作业生命周期、任务级性能指标等,基本构成了Spark应用的"健康体检报告"核心内容。
接下来,我们将按照SQL → Jobs → Stages的逻辑顺序,依次深入这三个二级详情页,系统性地剖析:
- 全局DAG执行结构(来自SQL页面);
- 作业的完整执行流程与依赖关系(来自Jobs页面);
- 以及各计算阶段(Stage)的资源使用与运行细节(来自Stages页面)。
通过这一层层递进的分析路径,我们将获得对Spark应用执行行为更全面、更深入的洞察。
SQL详情页
通过点击图中的with...可以进入到该作业的详细执行界面:
在数据分析场景中,大部分的操作包括过滤、分组、聚合、关联、排序。所对应的执行计划图中Exchange:代表的是Shuffle操作,Sort:代表的是排序,Aggregate:代表的是数据聚合。
这三类操作是硬件资源(如 CPU、内存、磁盘和网络)的主要消耗者。与此同时,Spark UI也为它们分别提供了丰富的细粒度指标(Metrics),用以精确刻画各类资源的使用情况。接下来,我们将聚焦于这三类操作,深入解析其对应的度量指标,以更好地理解资源消耗模式并指导性能调优。
Exchange
可以看到,针对每一个Exchange操作,Spark UI都提供了全面而细致的指标(Metrics),完整覆盖了Shuffle的整个生命周期------从Shuffle Write到Shuffle Read,从数据规模到处理耗时,关键维度一应俱全。
为了便于理解和参考,我将这些Metrics的含义和作用整理成表格形式:
Sort
可以看到,"Peak memory total"和"Spill size total"这两个数值,足以指导我们更有针对性地去设置spark.executor.memory、spark.memory.fraction、spark.memory.storageFraction,从而使得Execution Memory区域得到充分的保障。
Aggregate
对于Aggregate操作,Spark UI也记录着磁盘溢出与峰值消耗,即Spill size和Peak memory total。这两个数值也为内存的调整提供了依据。
Stages详情页
在所有二级入口中,Stage详情页的信息量可以说是最大的。点进Stage详情页,可以看到它主要包含3大类信息,分别是Stage DAG、Event Timeline与Task Metrics。其中,Task Metrics又分为"Summary"与"Entry details"两部分,提供不同。
Stage DAG
我们首先来看最简单的Stage DAG,如下图所示:
之所以说Stage的DAG相对简单,是因为我们已在SQL页面的二级详情中对完整的执行DAG进行了深入解析。而Stage级别的DAG本质上只是该作业(Job)整体DAG的一个局部片段------它仅对应其中某一个计算阶段。因此,一旦你理解了SQL页面中面向整个Job的完整DAG结构,Stage 层面的DAG自然也就一目了然、无需重复深究了。
Event Timeline
Event Timeline,记录着分布式任务调度与执行的过程中,不同计算环节主要的时间花销。图中的每一个条带,都代表着一个分布式任务,条带由不同的颜色构成。其中不同颜色的矩形,代表不同环节的计算时间。
在理想情况下,Spark任务的时间条带(Timeline)应以绿色部分(Executor Computing Time)为主,这表明大部分时间都用于执行实际的计算逻辑,而系统开销(如调度、I/O、Shuffle 等)被有效控制在较低水平。
然而,实际情况往往更为复杂。你可能会观察到:
- 深蓝色部分(Scheduler Delay)占比过高:说明任务在等待资源调度上耗费了大量时间;
- 黄色(Shuffle Write Time)和橙色(Shuffle Read Time)显著膨胀:表明 Shuffle 阶段成为性能瓶颈。
这些现象说明:性能瓶颈并不在计算本身,而是出在调度或数据交换环节。
如何针对性优化?
1. 针对调度延迟高(Scheduler Delay 大)
可参考以下经验公式进行资源配置调整:DP∼MCPD∼CM
其中:
- DD :数据集大小;
- PP :并行度(Partition 数量);
- MM :每个Executor 的内存;
- CC :每个Executor的CPU核数;
公式含义:每个任务处理的数据量( D/PD/P )应与单个任务可使用的计算资源( M/CM/C,即单位CPU核对应的内存)处于同一数量级。
若D/PD/P远大于M/CM/C,说明任务过重或资源不足,容易导致调度排队;反之则可能资源浪费。通过合理调整并行度PP 、Executor 内存MM和CPU核数CC可有效降低调度开销。
2. 针对 Shuffle 负载重(Shuffle Read/Write 时间长)
当黄色和橙色区域占比过大时,说明任务存在大量跨节点数据交换。此时应考虑:
- 是否可以使用Broadcast Join?
- 若其中一张表较小(通常<100MB),可将其广播到各节点,从而完全避免Shuffle。
- 其他优化手段包括:
- 合理设置spark.sql.adaptive.enabled启用AQE自动合并小分区;
- 调整spark.sql.shuffle.partitions避免过多小文件;
- 使用repartition或coalesce优化数据分布。
Task Metrics
Task Metrics(任务指标)是Spark在每个Task执行完成后收集的一组细粒度性能与资源使用数据,全面刻画了该Task的执行行为。这些指标构成了Spark UI(尤其是 Stages 页面)中各类可视化分析(如时间条带、Shuffle 统计、内存使用等)的底层数据基础,也是进行性能调优、故障排查和资源规划的核心依据。
在分析时,通常先从粗粒度的"Summary Metrics"入手------它是对所有Tasks执行指标的统计汇总,能够快速反映整个Stage的整体表现,随后再深入到细粒度的"Tasks"列表,逐个排查异常或低效的Task,实现精准定位与优化。
Summary Metrics
点击Select All,使当前所有度量值都生效,首先把Metrics整理到表格中:
特别值得关注的是Spill (Memory) 和Spill (Disk) 这两个指标。在Spark执行过程中当用于缓存中间数据的内存结构(如PartitionedPairBuffer或AppendOnlyMap)达到容量上限时,系统会将部分数据"溢出"(spill)到磁盘以释放内存空间。
- Spill (Memory) 表示这些被溢出的数据在内存中的原始大小(即未落盘前的字节数);
- Spill (Disk) 则表示这些数据实际写入磁盘后的大小(通常经过序列化和压缩)。
通过计算两者的比值:
Explosion Ratio≈Spill (Memory)Spill (Disk)Explosion Ratio≈Spill (Disk)Spill (Memory)
我们可以得到一个近似的 "数据膨胀系数"(Explosion Ratio)。该系数反映了:单位磁盘存储所对应的实际内存占用。有了这个比率,当我们知道某份中间数据在磁盘上占用了多少空间时,就能反推出它在内存中大概会消耗多少资源。这为精准评估内存需求、预判OOM风险、合理配置Executor内存提供了重要依据。
例如:
- 若Explosion Ratio = 3.0,意味着磁盘上1GB的spill数据,在内存中实际占用了约3GB;
- 若该值远大于1,说明数据在内存中高度"膨胀",需警惕内存压力;
- 若接近1,则表明序列化/压缩效率高,内存与磁盘占用接近。
Tasks
在介绍完粗粒度的Summary Metrics后,我们进一步深入到更细粒度的Tasks列表。实际上,Tasks表格中展示的许多指标(如 Duration、Shuffle Read Size、GC Time 等)与Summary Metrics中的内容高度一致,其含义完全相同,因此无需重复解释------你可以直接参照前文对Summary Metrics的说明来理解这些字段。
两者的核心区别在于:
- Summary Metrics提供的是对所有Tasks的聚合统计(例如最小值、中位数、最大值等),用于把握整体趋势;
- Tasks列表 则逐行展示每个Task的具体指标值,呈现个体行为。
这种细粒度视图特别适用于定位异常任务,例如:
- 执行时间显著偏长的Task;
- Shuffle 读取数据量异常高的Task;
- 发生磁盘 Spill 或 GC 耗时过长的Task。
通过对比单个Task与整体统计的偏差,可以快速识别性能瓶颈或数据倾斜问题,从而实现精准的故障排查与调优。
新增的指标并不多,其中最值得关注的是 Locality Level(本地性级别) 。
正如调度机制中所讨论的,每个Task在提交时都会携带一个本地性偏好(locality preference) ,用于指示它希望在哪个层级上访问其输入数据------例如是否优先在数据所在的节点、机架或任意节点上执行。Spark调度器会尽可能依据这一偏好,将 Task 分配到靠近其所需数据的 Executor上。
这一机制的核心目标,正是践行Spark的核心设计原则:"数据不动,代码动" 。通过将轻量级的计算逻辑移动到数据所在的位置,而非将大量数据跨网络传输到计算节点,从而显著减少网络 I/O 开销,提升任务执行效率。因此,Locality Level不仅反映了任务调度的亲和性,也是评估数据局部性优化效果的重要依据。
四、实战环节
这里我们选两个典型的例子进行分析调优:
案例一(scan表慢和内存问题)
如上图所示,我们选择运行时间最长的一个Stage进行具体分析调优,点进去进入该Stage内部,如下所示:
观察如上指标会发现两个问题点:
1.这里单个Task处理的数据量为25MB左右,正常来说,一个Task处理的数据量为128-256MB较为合理,这是由于原始表的数据量大,小文件太多,导致分配的Task数据量过大。
2.并且由第二个图可知失败重试的Task的调度等待时间过长,这是由于Task数据量过多,而实际并发只有1000,导致部分Task调度时间太长导致。
对于上述问题,我们可以通过设置表的切片大小缓解问题:
set spark.sql.odps.split.size.du_shucang.dws_traffic_algo_search_keyword_stats_di=512MB添加如上参数后观察效果:
我们第一次将切片大小控制到512MB来减小实际Task数,可以看到相对于上次该Stage会快20min,这里我们还可以继续增大表的切片来迭代看效果,感兴趣的可以自己尝试。
我们在从另外一个角度看待问题,当我们试图减小分配给Stage的Task数量时,那么Task的实际处理数据量就会增大,这里我们可以通过减小"spark.executor.cores"的核数来隐形的增大Task的内存或者调大spark.executor.memory的内存来显性的增大Task的内存,这里由于Task的量级太多,我们选择增大spark.executor.memory来进行优化,效果如下所示:
这里由于本身Task处理的数据量是很小的,所以无法很显著的看到效果,若是出现下图所示的情况,调大内存效果会更显著些:
总结:在Spark中,当读表时由于数据量大,而此时Task处理的数据量又过小时,可以通过set spark.sql.odps. split.size.xxx=xxMB来减小task数量,而通过Spill和Spill Memory Disk来观察其是否需要增大内存或者实际并发量。
案例二(shuffle之后并行度不足)
该case打不开了之前记录的,随便拿了一个还是老样子降序查看最大的。
分析是在Shuffle阶段并行度提升不了。
总结:在Spark中,Join、Group都会在Shuffle阶段产生并行度,这个Shuffle是一个很大的池子,可以利用万能参数进行增加并行度。
- 读取Shuffle数据的目标大小 ;
- 提高AQE shuffle默认分区数;
"spark.sql.adaptive.advisoryPartitionSizeInBytes":"64MB";
"spark.sql.adaptive.coalescePartitions.initialPartitionNum":"1000".
注意:有些时候会发现增加了还是没变,那是因为切分还是太大。1,2切分,3调整限制。
"spark.sql.adaptive.advisoryPartitionSizeInBytes":"8KB",
"spark.sql.adaptive.coalescePartitions.minPartitionSize":"8KB"
"spark.sql.adaptive.coalescePartitions.initialPartitionNum":"3000".
五、总结
总的来说,我们通过Spark UI界面去优化亦或者去定位错误,都离不开内存与并行度这两个大的维度,通过相应的Summary Metrics,我们可以很好的定位到问题所在,是内存过小,还是并行度不够,事实上,内存与并行度并不是独立的,而是相互影响,相互制约的一种关系,并行度决定了"有多少个Task同时运行",而内存决定了"每个 Task能分到多少资源"。
当并行度一定的时候,当我们试图通过调大内存来解决问题而问题并未得到解决的时候,实际上,当内存变大而实际并行度固定时,每个Task所分配的内存就会增大,所带来的额外开销就是做内存穿透的时间增加,从而GC时间增大。
当内存一定时,我们去调大并行度或者增大核数时,每个Task所分配的内存就会变小,就会容易出现内存溢出的风险。
对于一个任务,那我们如何去合理的配置其并行度和内存,这里我们可以看一个任务耗时最长的的几个Stage,假设某个Stage的并行度为5000,而此时所给的参数为executor.memory=12g,executor.cores=4,spark.dynamic
Allocation.maxExecutors=250,此时集群最大并行度250*4=1000,也就是说当前任务需要分5批执行,此时集群中一个Task内存为3g,按照经验论,每Core对应4~8GB内存是合理的,而处理的总并发度通常会是实际并发度的2~3倍为合理,因此,我们将参数设定为 executor.memory=16g, executor.cores=6来进行一个迭代优化。
一个好的Spark作业,Task均衡、无Spill、CPU打满、内存够用,这是我们去做优化的一个理想状态!
往期回顾
1.Sentinel Java客户端限流原理解析|得物技术
2.社区推荐重排技术:双阶段框架的实践与演进|得物技术
3.Flink ClickHouse Sink:生产级高可用写入方案|得物技术
4.服务拆分之旅:测试过程全揭秘|得物技术
5.大模型网关:大模型时代的智能交通枢纽|得物技术
文 /硕
关注得物技术,每周更新技术干货
要是觉得文章对你有帮助的话,欢迎评论转发点赞~
未经得物技术许可严禁转载,否则依法追究法律责任。































