☰
Spark WebUI监控指南:从页签到性能问题定位实操
2026/10/16 3:36:47 网站建设 项目流程

第一次点开Spark WebUI的人,基本都是同一个反应:任务要么已经跑完了,要么卡得纹丝不动,但页面上那一整排Jobs、Stages、Storage、Environment、Executors、SQL页签,外加各种数字、色块和时间条,根本不知道从哪看起。说实话,Spark的监控界面并不是复杂到学不会,它真正缺的是一份“阅读顺序”。今天这篇就把WebUI的入口、每个页签该看什么、怎么用页面里的指标反过来定位性能问题、以及新手最常踩的坑一次性串起来,适合刚跑通第一个Spark任务、还没有系统看过监控页面的开发者。

1. 先搞清楚入口和页签:Spark WebUI到底能看什么

1.1 不同运行模式下怎么找到WebUI

先说一个很多新手第一次就卡住的点:WebUI到底从哪里打开,不同运行模式下入口完全不一样。

如果你是在本地用IDEA或终端跑PySpark / Spark Shell,启动SparkSession之后,控制台日志里通常会打印一行带URL的提示。默认地址就是http://localhost:4040。如果4040被其他程序占了,Spark会自动尝试4041、4042……一直往上递增。所以你会发现,有时候明明同一个脚本,上一次是4040,这次却跑到了4041。想固定端口,可以在启动前设置spark.ui.port,例如:

spark = SparkSession.builder \ .appName("webui_demo") \ .config("spark.ui.port", "4040") \ .master("local[4]") \ .getOrCreate()

也可以通过代码直接拿到当前UI地址,运行后把它打印出来,省得去翻日志:

print(spark.sparkContext.uiWebUrl)

集群模式就复杂一点。Standalone模式下,打开Master的Web页面,在Running Applications列表里找到你的应用,点击对应的“Application UI”链接;YARN模式下,需要先去ResourceManager页面找到你的Application ID,再点Tracking URL跳转到Spark自己的WebUI。很多新手在这里会犯一个错误:以为集群部署后还是访问localhost,结果当然连不上。

这里要特别提醒一句:默认Spark WebUI只在应用运行期间有效,应用一结束,页面就再也打不开了。想事后复盘历史任务,必须提前开启Event Log并配合History Server,否则任务跑完,什么都没留下。

1.2 顶部页签是按什么逻辑排列的

打开页面后,你会看到顶部的几个页签,它们不是随便排的,背后是一套非常清晰的执行模型。

  • Jobs:站在用户Action的视角,一个job对应一次count()、collect()、save()这类触发计算的动作。
  • Stages:站在调度器的视角,一个job会被宽依赖拆成一个或多个stage。
  • Storage:查看曾经被persist()或cache()过的数据,以及它们在内存和磁盘中的分布。
  • Environment:展示当前Spark进程里所有生效的配置项、环境变量和Classpath。
  • Executors:展示每个Executor的资源、内存、GC时间、任务数等运行状态。
  • SQL:展示通过SQL或DataFrame API触发的查询,以及对应的执行计划。
  • Streaming:如果你的任务接入了Structured Streaming,还会出现这个页签。

如果你理解了Job、Stage、Task这三层关系,后面所有指标都不难懂。可以把Job想象成你点的外卖订单,Stage是后厨里“洗菜”“切菜”“炒菜”这几个流程,Task则是每个厨师手上具体处理的那一份菜品。WebUI里的绝大多数页面,就是在帮你观察这三个层级上到底哪个环节出了问题。

2. 逐页签拆解:从Jobs到SQL的指标到底怎么读

2.1 Jobs页:Action和任务生命线

Jobs页是整个WebUI的总入口,也是我建议新手每次打开后先看的第一屏。页面顶部会列出当前应用运行过的所有Job,每行包括Job ID、Description、Submitted时间、Duration、Stages Result和Tasks Total。

Description那一列很有用,它会直接告诉你这个Job是被哪个Action触发的,比如collect at test.py:123。当你怀疑某个代码卡住时,第一件事就是看看最新的Job卡在哪个Action,再去对应的代码行排查。

点进一个Job详情,你会看到DAG图、Event Timeline和每个Stage的统计信息。DAG图展示的是RDD血缘关系,方框是Stage,圆框是RDD。新手不需要逐行看懂DAG,只需要确认两件事:一是Stage数量是否正常,二是哪个Stage耗时最长。Event Timeline则是把整个job的Executor分配、任务计算、序列化等过程按时间画出来,可以用来判断是计算本身慢,还是调度延迟高。

这里有一个常见误区:看到某个Job显示“Skipped”,很多人会以为是出了问题。实际上,“Skipped”说明这个Job的数据被缓存或短路跳过了,是好事。比如一张表被cache()之后再次访问,控制台上就可能出现跳过日志,网页里也会显示skipped,不用慌。

2.2 Stages页:每个阶段的性能细节

如果一个Job慢,第二步就是进入Stages页,找到耗时最长或者Task数量最异常的Stage,点进去看详情。

Stage详情页里最重要的DAG可视化下面有Stage耗时、Shuffle Read/Write、Input/Output、任务成功数和失败数这些汇总指标。对于离线任务,Shuffle相关的“Read Size / Records”几乎是你判断性能问题的第一参考。Shuffle数据量大,说明上游和下游之间传输了太多中间结果,往往是分区不合理、Join键分布不均,或者过滤条件推得太晚。

往下翻是Task表格,这个表格可以按列排序,我强烈建议你按Duration倒序排一下。正常情况下,同一Stage里的Task耗时应该是基本接近的,如果出现一个Task耗时几分钟、其他Task几秒钟,那么数据倾斜的概率就非常高了。此外,Task表格里还有GC Time、Shuffle Read Size、Shuffle Write Size等列。只看“平均耗时”很容易被骗,看Task分布才是关键。

2.3 Storage页:缓存容量与存储级别

Storage页是很多DataFrame用户容易忽略的一页,但它对排查内存问题却特别有用。只有当你调用了cache()、persist()、checkpoint()之后,这里才会出现对应的数据块信息。

页签里会列出每个被缓存数据集的Storage Level、缓存的Partition数量、Fraction Cached、内存占用、磁盘占用等。Storage Level通常类似MEMORY_AND_DISK或MEMORY_ONLY,这代表数据在内存和磁盘之间的保存策略。如果Fraction Cached长期小于1,说明部分分区因为内存不够被丢掉了,或者还没被全部缓存起来。

我见过不少新手在cache()之后没有做任何Action,就跑到Storage页里问为什么是空的。这里要明白一个原则:cache()是懒执行的,只有当你真的触发一次Action,数据才会进入缓存。另外,如果Storage页里显示的内存占用特别高,同时Executors页里的GC时间也在飙升,那就说明缓存数据挤占了执行内存,该考虑用spark.catalog.clearCache()或者对临时表执行unpersist()来释放空间了。

2.4 Environment页和Executors页:配置与资源的照妖镜

Environment页本质上就是一份“运行时配置快照”。它分为Spark Properties、System Properties、Classpath Entries几个区。比如你设置了spark.executor.memory,但不确定有没有生效,到这里看一圈,挨个核对你关心的配置项即可。

Executors页则是真正的资源战场。它列出的指标包括Executor ID、Address、RDD Blocks、Max Memory、Memory Used、Disk Used、Active Tasks、Failed Tasks、GC Time、Shuffle Read/Write等。在集群模式下,Driver和Executor会分开显示,本地模式下通常也会看到Driver和Executor条目。

我最常盯的两个地方,一个是Memory Used和Max Memory的对比,另一个是GC Time。如果Memory Used长期贴着Max Memory跑,意味着Executor内存很紧;如果GC Time在任务运行过程中持续增长,甚至占到任务时间的百分之几十,那大概率是内存压力过大或对象序列化太重。此时不要盲目给Executor加内存,先去Storage页看是不是缓存过多,再看看Shuffle时是否有大量临时对象。调参之前先看页面,这个习惯能帮你少走很多弯路。

2.5 SQL页:执行计划比开会还直白

很多人不知道,DataFrame API操作也会出现在SQL页里。只要是通过DataFrame或SQL触发的查询,都会被记录成“Query”,点击进去能看到完整执行计划,以及每个算子实际处理的行数、字节数和耗时。

SQL页最大的价值在于它把优化器的行为“摊开”了。比如两张表Join,你是希望它走BroadcastHashJoin还是SortMergeJoin?你在小表侧设置了合适的spark.sql.autoBroadcastJoinThreshold后,SQL页里就会出现BroadcastExchange节点。又比如某个Filter下推没有生效,执行计划里你也能直观看到过滤发生在哪个位置。

对于新手来说,不需要立刻搞懂物理计划里的每一个算子和表达式。你只要能找到耗时最高的算子,并看懂那个算子读了多少数据、产生了多少Shuffle,就已经比很多只会跑任务不看监控的开发者在排查性能问题上领先一大截了。

3. 用WebUI抓性能问题:三个新手能上手的实战案例

3.1 数据倾斜怎么从Stage页确认

数据倾斜是Spark任务最常见的性能杀手。它的特征非常简单:同一Stage里,大量Task几十秒就跑完了,某个Task跑了十几分钟甚至更久。

用WebUI确认时这样操作:点开慢Job对应的Stage,按Shuffle Read Size或Duration排序Task列表。倾斜的Task会在Shuffle Read Records列出现一个明显高出同侪的数字,比如别人都只有几MB,它一个人读了几百MB。

确认后,常用解法是给热点Key加随机前缀再做聚合,也就是“加盐”;之后再对聚合结果去掉前缀做二次聚合。如果是Join倾斜,则可以拆分成倾斜数据与非倾斜数据分别处理。你要记住的排查思路是:先在WebUI里锁定是哪个Stage、哪个Task,再去代码里用采样或groupBy统计热点Key,而不是一上来就乱改分区数。

3.2 频繁GC怎么看

GC问题在表现上通常不是“某个Task特别慢”,而是所有Task普遍偏慢,整体任务时间明显加长,但CPU又不像满负荷运转。这时候去Executors页看GC Time列,如果GC Time已经占了Task Time的很大比重,基本可以确认JVM在频繁Full GC或大型Young GC。

我看到过的典型场景是:一张大表被反复cache(),同时另一个Shuffle又需要大量内存来存储Map端输出,两边一挤,整个Executor就在内存回收上疲于奔命。处理步骤是先去Storage页看看哪些缓存可以释放,能unpersist()就立刻释放;然后检查分区大小,尽量避免单个Task产生过大Shuffle Record;最后再考虑调整垃圾回收策略,比如换用G1。不要一上来就把spark.executor.memory调大,内存越大,Full GC发生时的停顿时间往往越长,反而可能雪上加霜。

3.3 资源利用率不高怎么看

资源利用率最直接的表现是“集群明明有几十个核,跑的Task却只有几个”。在Executors页里,你要把每个Executor的Active Tasks加起来和总核数对比。如果Executor有4核,Active Tasks长期只有1或2,就说明并发度不够。

并发度不够的根源通常是分区数太少。对DataFrame和SQL任务,最常见的是spark.sql.shuffle.partitions设置过小;对RDD任务,则要看创建RDD时的切片数以及repartition()的次数。你可以通过将spark.sql.shuffle.partitions从默认值调大,然后重新运行同一段SQL,再去SQL页和Stages页里观察Task数量是否增加、总耗时是否缩短。

但这里也要说句实话,分区数不是越大越好。分区太多会带来额外调度开销,还会产生大量小文件。正确的做法是让分区数尽可能接近可用并行度的2到3倍,而不是盲目往大了调。

4. 跟着做一遍:从提交任务到按指标调参的完整流程

4.1 准备一个可复现的练习任务

光说不练没有用,下面我准备一个可以在本地直接跑的模拟任务。代码会生成一个千万级的数字表,做一次重分区,再按分组统计,最后触发两次Action,方便我们同时看Stages和Storage两个页面。

from pyspark.sql import SparkSession from pyspark.sql.functions import col spark = SparkSession.builder \ .master("local[4]") \ .appName("webui_demo") \ .config("spark.ui.port", "4040") \ .getOrCreate() df = spark.range(0, 1000000) df2 = df.repartition(200, col("id")) df3 = df2.withColumn("grp", col("id") % 10).groupBy("grp").count().cache() # 第一次行动:真正执行计算 df3.collect() # 第二次行动:应该命中缓存,出现 Skipped Job df3.collect()

先在本地跑起来,确认没有报错。运行期间,打开http://localhost:4040,你就能看到实时变化的页面。如果看不到任何数据,先检查Spark进程是否还活着,再检查是不是端口被自动切换了。

4.2 打开WebUI观察每个阶段的指标变化

任务跑起来后,按照这个顺序去页面里找信息:

第一轮观察Jobs页。你会看到至少两个Job,第一次collect()会有完整的Stage执行链,第二次collect()会出现Skipped Job。这个现象本身就是在验证缓存是否生效。

第二轮观察Stages页。找到第一次Job下的Stage详情,把Task表格按Duration排序,你会发现同一批Task耗时存在差异,但因为本地模拟数据分布很均匀,差异不会大到像真实生产中的数据倾斜那样离谱。

第三轮观察Storage页。df3被.cache()之后,数据块应该出现在Storage列表里,Storage Level类似MEMORY_AND_DISK,Fraction Cached接近1。如果你看到Fraction Cached低于完整缓存数值,说明缓存过程还没结束,或者部分分区在内存里被替换了。缓存是否生效直接决定第二次Action是不是真的大部分被跳过,这是理解Spark“惰性求值”概念最直观的一课。

4.3 调整参数后,用WebUI验证效果

接下来,在同一个Session里做一次参数调整实验,看看WebUI怎么反映变化:

# 将shuffle分区数从默认200调小到8 spark.conf.set("spark.sql.shuffle.partitions", "8") df4 = spark.range(0, 1000000).withColumn("grp", col("id") % 10) df4.groupBy("grp").count().collect()

重新跑完后,去Stages页看这一次新Job。你会发现和之前的Stage相比,Shuffle产生的Task数量发生了明显变化,分区数从200个级别变成个位数级别。如果数据量只有100万条,分区数调小后耗时反而可能更短,因为每个文件碎片少,调度开销降低。

这里想强调的是,调参不是拍脑袋,WebUI的价值就在于它能把“参数影响”可视化给你看。我平时在生产环境做参数验证,流程完全一样:先在测试环境跑一份基准数据,记录WebUI里的Task数、Shuffle量、耗时、GC Time,然后改一个参数,再跑一遍,只看变化的那几个关键指标,不贪多。

5. 常见问题排查手册:打不开、看不懂、不会看怎么办

5.1 打不开或页面刷新不出来

这是新手遇到最多的问题,常见的排查链条也不复杂:先确认应用是否还在运行,如果已经跑完,默认页面自然就关了;再检查端口,netstat或lsof看一眼4040是否被其他进程占用;然后确认访问地址对不对,集群模式下一定不要直接访问Driver所在机器的任意端口,而是要通过ResourceManager或Master页面的链接跳转。

另外,如果你在本地用Docker或远程服务器跑任务,不要忘了把端口映射出来,或者在容器启动参数里固定spark.ui.port。我之前帮一个同学排查,页面打不开的原因就是Spark自动拿不到它想要的hostname,绑定地址出了错。这时候可以在启动参数里显式设置spark.driver.host和spark.driver.bindAddress。

5.2 页签显示不全或指标为空

不同版本的Spark,WebUI页签会有细微差别。比如早期版本里SQL页只记录真正的SQL,新版DataFrame操作也归入SQL;Structured Streaming会多出Streaming页签。所以你看到别人截图里的页签比自己的多,先对比一下版本,不要急着卸载重装。

指标为空也常见。Storage页为空,大概率是你没有调用persist(),或者调用了但还没触发Action;SQL页里没内容,说明还没有SQL或DataFrame查询执行过;Executors页里某些字段为空,则可能是版本差异或该Executor还没有运行过任务。遇到这种情况,先让任务跑起来,再刷新页面,多数指标会在执行过程中出现。

5.3 新手容易踩的坑

最后整理一些我自己踩过、也帮别人踩过的坑。

第一,WebUI数据并不是实时到秒的,特别是大集群下,页面UI更新有延迟。你看到的时间差并不一定代表系统卡住,先等等刷新再看,不要一看到数字不动就觉得“死锁”了。

第二,不要用WebUI替代日志,二者是配合关系。WebUI告诉你“哪个阶段慢”,日志告诉你“为什么慢”。排查问题时,先定位Stage,再去Executor日志里找Exception和WARN。

第三,千万不要贪多求全。新手最容易犯的毛病是打开页面后想把每个指标都研究透,结果看完一圈什么都没记住。我自己的习惯是每次只看一个核心问题:任务慢就盯Stages,内存不够就盯Storage和Executors,配置不生效就盯Environment,保持单调,反而更能看出异常。

第四,History Server要提前配。默认配置下WebUI随应用结束而消失,如果你想保留历史指标,必须在启动应用之前就开好spark.eventLog.enabled和spark.eventLog.dir,任务结束后再用History Server查看。临时想起来配,历史任务是捞不回来的。

最后说一点个人体会。我现在写Spark任务,基本不会等出现问题才打开WebUI,而是在第一次跑通后,就把Executors页的GC Time和Stages页最耗时Stage的Task分布截个图存下来,当作后续调参的基线。等改了代码或加了数据量,再拿新页面和基线对比。这个习惯看起来很笨,但比任何“经验之谈”都可靠。WebUI不是用来吓唬新手的复杂仪表盘,它是一个让你亲眼确认自己代码到底怎么被调度执行的窗口,用得越多,你对Spark的理解就会越扎实。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询