做数据开发的第二年,我才开始认真看Spark WebUI。第一年基本属于“作业出错就百度报错,不报错就撒手不管”的状态,等集群开始频繁出现凌晨跑不完的任务时,才被逼着打开这个页面。实话讲,挺后悔的:要是刚接触Spark时就有人带我把这几个Tab过一遍,至少少加两周班。这篇手册就送给刚开始用Spark的新手,目标非常具体——让你能端着一个网页,看得懂任务跑到哪里、卡在哪里、资源怎么用的,并且能顺着页面给出的线索往下排查。
这份手册不打算扯太多分布式理论,我是照着“拿到一个页面先看什么、再看什么、最后看什么”的思路来写的。你会搞清楚Jobs、Stages、Executors、Storage这些页面的实际用途,也会拿到一份可以直接照着做的排查流程。最后还会整理几个新手一定会遇到的经典问题。
1. 先搞懂Spark WebUI是什么,再看教程才有意义
1.1 新手最容易犯的误区:把UI当“监控大屏”
很多人第一次打开Spark WebUI,下意识以为它和监控大屏一样,实时滚动着各种炫酷指标,数值一变就代表集群出事了。这种理解不能说全错,但方向偏了。Spark WebUI的本质不是集群监控工具,而是单个Spark应用内部执行过程的可视化界面,它展示的是你这个应用从提交到执行再到结束的完整过程。集群里有几台机器、CPU整体占用多少,这些信息它其实不太关心,它关心的是你这一个应用内部的Job、Stage、Task执行情况。
我见过不少团队新人拿着UI截图来问“集群是不是挂了”,结果打开一看,只是某个Task因为网络抖动重试了两次,整个应用照样稳定跑完。反过来,真正经验丰富的人打开UI,第一件事是定位当前应用的瓶颈发生在哪个Stage,而不是盯着总耗时唉声叹气。这个视角转变很重要:UI是帮你“诊断”应用自身的,不是帮你“监控”机器健康的。
还有个容易混淆的地方:Spark WebUI和Spark History Server(历史服务器)不是一回事。前者在当前应用运行期间实时打开,应用一结束页面就没了;后者负责加载历史事件日志,让你跑完之后还能翻旧账。两者看到的页面结构几乎一样,但数据来源不同,使用场景也不同。后面我会专门讲History Server怎么用。
1.2 这些页面分别在讲什么
Spark WebUI默认提供几个核心Tab,顺序一般是:
| 页面名称 | 主要作用 | 新手重点关注程度 |
|---|---|---|
| Jobs | 展示当前应用里所有Job,即每个Action算子触发的一次完整计算 | 高 |
| Stages | 展示每个Job内部被拆成的Stage,以及每个Stage下Task的分布和执行情况 | 最高 |
| Storage | 展示被持久化到内存/磁盘的RDD、DataFrame或表的相关信息 | 中 |
| Environment | 展示当前应用运行环境,包括Spark参数、系统变量、Classpath等 | 中 |
| Executors | 展示当前应用分配的Executor列表,以及每个Executor的资源使用、任务完成情况 | 高 |
| SQL | 如果你用Spark SQL跑查询,这里能看到SQL的执行计划、物理计划和对应作业 | 高 |
| Streams | 如果你跑Structured Streaming实时任务,这里能看到流式批次处理情况 | 低(按需) |
我把“新手重点关注程度”这一列标出来,是有实际原因的。你在排查80%的慢作业、死作业、OOM问题时,基本都在Stages、Executors和Jobs三个页面之间切换。Storage和SQL这两个页面在特定场景下非常关键,但不用一上来就抱着啃。Streams页面只有写结构化流任务的人才会经常用到,批处理开发可以先放一放。
记住一个基本对应关系:一个Action算子(如count、save、collect)对应一个Job,一个Job被划分成一个或多个Stage,一个Stage里包含多个Task。这个关系你只要在UI里点过几次就能体会到,千万别死记硬背。
2. 核心页面逐个拆解,每个字段都给你翻译成人话
2.1 Jobs页:看清一个Action对应的一次计算
进入应用页面后,默认落在Jobs Tab。页面上半部分是一张Event Timeline时间轴图,下面是一张Job列表。
Job列表里的字段很直白:
- Job Id:这个应用内第几个Job。
- Description:对应的是哪个代码操作,比如count at xxx.scala:123。看到这个字段就知道这个Job是哪行代码触发的。
- Submitted:提交时间。
- Duration:整个Job从第一个Task开始到最后一个Task完成的耗时。
- Stages:Succeeded、Total,比如2/3表示3个Stage里成功了2个。
- Tasks:Succeeded、Total,Succeeded/Total同理。
新手拿到这个页面,最该训练的能力是把Job和代码逻辑对上号。举个例子,你在代码里写了一个df.groupBy(...).count().collect(),UI里必然会多出一个Job,Description会显示类似collect at的字样。如果同一段代码里连续调用了多个Action,Job列表就会依次出现多个条目。有些初学者总是分不清“我的程序卡在哪里”,其实先数一下Job列表,看看哪个Job耗时最长,位置基本就定位到代码行了。
Event Timeline这张图很多人直接跳过,其实里面有信息量。不同颜色代表不同状态:调度中、运行中、成功、失败,时间轴能直观反映某个Job是刚启动就不动了,还是跑了一部分才失败。结合Duration字段一起看,能快速区分“任务压根没跑起来”和“任务跑到一半卡死”。
2.2 Stages页:真正干活的地方
如果说Jobs页是项目概览,Stages页就是施工现场。点进任何一个Job,你就能看到它被拆分成了多个Stage。Stage之间通过Shuffle划分,也就是数据需要跨分区重分布的地方。这也是Spark相对MapReduce更灵活的地方,一个Job可以包含多个Stage,而不是固定Mapper加Reducer两段。
Stages页面分为两大部分:上半部分是DAG可视化图,下半部分是Stage列表。
DAG图是新手必须学会看的东西。矩形框代表RDD或DataFrame的转换操作,圆角框代表对每个分区执行的计算。整个图从左往右读,就是数据从源头读取、经过层层转换、最终输出结果的过程。点击某个框还能进一步看到对应的代码位置和指标摘要。
Stage列表的主要字段:
- Description:这个Stage执行的代码描述。
- Submitted / Duration:提交时间与总耗时。
- Tasks:成功/总数,比如99/100说明有1个任务失败过,最终靠重试才完成。
- Input:从数据源读取的数据量。
- Shuffle Read / Shuffle Write:从上一个Stage拉取的数据量 / 写给下一个Stage的数据量。这两个指标是判断数据倾斜和Shuffle开销的关键。
我强烈建议新手在查看Stages时,先不要管总耗时,而是看每个Stage的Duration柱状图。展开每个Stage后,你能看到该Stage下面所有Task的执行时长分布。如果你发现某个Stage里绝大多数Task几十秒就完成了,但有几个Task跑了十几分钟,这就是典型的数据倾斜信号。如果所有Task时长都差不多,但整体耗时很长,那大概率是资源不足或单Task计算量过大。
2.3 Executors和Storage页:资源与缓存都在这里
Executors页面展示的是当前应用下每个Executor的信息,相当于每个干活工人的考勤表和绩效表。关键字段有:
- Executor ID:编号。
- Address:主机名加端口。
- RDD Blocks:存储在该Executor上的RDD分区块数量。
- Storage Memory:存储用的内存 / 已用内存。
- Cores:分配的CPU核数。
- Tasks:已完成、失败、进行中、总数。
- Task Time:该Executor上所有Task累计执行时间。
- GC Time:垃圾回收时间。这个指标很容易被忽略,但GC时间占比过高通常意味着内存压力大,频繁Full GC会导致整个Stage变慢。
- Shuffle Read / Write:该Executor读入和写出的Shuffle数据量。
排查资源问题时,Executors页能帮你快速回答几个问题:Executor有没有被杀掉重启?内存够不够?GC是不是太频繁?如果多个Executor的Task完成数差异巨大,说明任务分配不均衡,大概率有数据倾斜。
Storage页相对冷门,但用对场景也很香。当你在代码里调用df.persist()、df.cache(),或者Spark SQL里用了CACHE TABLE,Storage页就会出现对应的持久化条目。页面会显示存储级别(比如Memory and Disk)、缓存分区数、缓存比例、内存占用、磁盘占用等。
有一种情况会让新手产生困惑:明明代码里调用了cache(),Storage页却什么都没有。原因通常有两个:一是没有触发Action,persist是惰性的,必须有一个Action真正执行后数据才会被缓存;二是存储级别设置了MEMORY_ONLY但内存不足,部分分区被丢弃了。Storage页能帮你验证数据到底有没有被真正缓存住,别光看代码里写了什么。
2.4 Environment和SQL等扩展页面
Environment页面会列出当前应用全套运行配置,包括Spark属性、Hadoop属性、系统属性、Classpath。这个页面最常用的场景是“确认某个参数到底生效没有”。举个例子,你以为自己在代码里设了spark.sql.shuffle.partitions=200,但如果提交脚本时被集群级别的配置覆盖了,你从UI里一眼就能看出来。很多诡异的问题,最后都出在“你以为你设了,其实你没设”上。
SQL页面只有程序执行过Spark SQL相关的操作才会出现内容。点开某个查询后,你会看到这个SQL的逻辑计划和物理计划,以及执行过程中产生的一个或多个Job。对于写Hive SQL、Spark SQL的人来说,这个页面是排查查询性能的第一现场。它能把一条SQL对应到具体的Stage和Job,帮你定位是哪个Join、哪个聚合拖慢了整个查询。
Streams页面则只对Structured Streaming任务有意义。里面展示的是每个批次(Batch)的输入速率、调度延迟、处理时长等信息。批处理开发基本不用碰它,但如果你接手了一个实时任务,这个页面能用最快速度告诉你“数据有没有在持续消费,处理速度跟不跟得上”。
3. 手把手实操:用WebUI定位一个慢作业
3.1 借一个真实的慢作业,先学会打开UI入口
理论说再多,不如走一遍实际排查过程。我假设一个场景:某物流公司有一条凌晨跑的数据任务,它会读取前一天订单明细、城市维表和类目维表,做汇总统计后写回结果表。最近三天这个任务都在凌晨超时,业务方早上起来看不到报表数据,非常恼火。A同学刚接手这条链路,被领导叫去排查。
先解决一个实际问题:怎么打开UI。
如果任务跑在本地或测试环境,比如用spark-shell或spark-submit --master local[*]提交,直接浏览器访问http://localhost:4040就能看到当前应用的WebUI。如果任务跑在YARN集群上,情况稍微麻烦点。打开YARN的ResourceManager页面,找到对应的Application,点进去以后,找到Tracking UI那一栏,通常会有一个指向ApplicationMaster页面的链接,点进去就是Spark WebUI。有些集群出于安全限制,会要求你先登录或配置代理,这个阶段可以先找运维开通访问权限,别自己乱改集群网络配置。
如果是Kubernetes环境部署的Spark,一般需要通过kubectl port-forward把driver的UI端口转发到本地才能访问。操作方法大致是:先用kubectl get pods找到driver对应的Pod名,然后执行类似kubectl port-forward pod名 4040:4040的命令,再访问本地的4040端口。这块和具体集群环境强相关,建议先确认你的集群类型再决定入口方式。
A同学接手后发现,他自己没有直接访问YARN ResourceManager的权限。这时候他做的事很对:先在运维配合下,通过YARN页面拿到了ApplicationMaster的访问地址,成功进入了Spark WebUI。
3.2 从Jobs页锁定嫌疑Job
A同学进入UI后,首先落在Jobs页面。他看到Job列表里有连续好几天夜间作业留下的运行记录。挑出耗时最长的一条Job,Duration显示跑了一个多小时。他点进这条Job,看到Description对应的是项目代码里负责“聚合统计写结果表”的那一行。
这里我插一句:如果你不清楚某个Job对应哪行代码,UI里的Description能帮上大忙。点开Job详情后,页面里会显示完整的DAG和Stage列表,而且每个Stage左下角会有代码位置提示。把UI里的描述和代码行号对应起来,是快速定位问题的基本功。很多新手习惯于直接在日志里搜ERROR,但慢作业通常没有异常日志,只有UI能告诉你“时间到底花在哪个环节”。
A同学现在确定了慢的Job是哪个,接下来要回答“这个Job里面,具体是哪个环节慢”。
3.3 进Stages看DAG,找出“卡脖子”的Stage
点进Job详情后,A同学看到这个Job被拆成了4个Stage。正常预期是4个Stage的耗时差异不会特别大,但实际数据显示Stage 3的Duration远远长于其他三个Stage。柱状图拉得很长,一眼就能看出问题在Stage 3。
他点开Stage 3,看到DAG可视化图。这个Stage做的事情大致是:对明细数据和城市维表做了一次Join,然后对结果做聚合。在DAG图上,Shuffle Read的数据量非常大,Shuffle Write也明显高于其他Stage。到这里,疑问已经收窄到两种可能:要么是Join把数据发散了,要么是聚合操作里某些Key的数据量极度不均匀。
A同学还注意到一个细节:Stage 3的Task总数是400,而前面几个Stage的Task总数只有50到100。Task数量骤增通常意味着发生了宽依赖,也就是Shuffle。大量Task本身不是坏事,但如果某一个Task处理的数据量远超其他Task,问题就变成了数据倾斜。
3.4 用Task分布判断是否数据倾斜,并落地优化
在Stage详情页里,A同学把页面往下拉到Task表格。这个表格会列出每一个Task所在的Executor、运行时长、GC时间、Shuffle Read / Write数据量、是否失败等信息。
他做了一件事:按Duration从大到小排序。结果很典型:排在第一名的一个Task运行了40多分钟,第二名30分钟,第三名开始骤降到两分钟,后面的Task大多在几十秒到一分钟之间。再看Shuffle Read列,Top 1那个Task读取了将近10GB数据,而大多数Task读取量只有几十MB到几百MB。这种分布几乎可以断定:某个Join Key对应的数据量严重失衡,导致某个分区承担了巨量数据处理工作。
定位到问题后,优化方案就有方向了。A同学在代码里检查发现,Join时的小表(城市维表)只有几千行,但Spark并没有自动把它广播出去。原因可能是表统计信息缺失或广播阈值设置不当。他做的第一步优化是给维表增加广播Hint,用类似/*+ BROADCAST(...) */的方式强制广播,避免全量Shuffle。改完之后,Stage 3的耗时从40多分钟降到了2分钟以内。
这里必须强调一下:UI本身不会告诉你“该怎么优化”,但它能精确告诉你“问题出在哪个阶段、是哪个Task异常”。你得基于UI提供的这些线索,结合自己对代码的理解去决定下一步动作。没有UI的话,你只能靠猜,效率差太多了。
如果A同学遇到的不是广播能解决的倾斜,而是业务Key本身分布不均匀,还可以考虑加盐两阶段聚合:第一轮给Key拼上随机后缀,把大Key拆散到不同分区,做局部聚合;第二轮去掉后缀再做全局聚合。这种写法会稍微增加一点CPU开销,但能把单Task的压力降下来,是处理数据倾斜的另一张王牌。实际用不用,得根据UI里Task数据的差异程度来权衡。
4. 常见问题排查:新手最容易踩的坑
4.1 打不开UI或端口被占怎么办
本地模式最常见的现象是:作业在跑,但浏览器访问localhost:4040直接拒绝连接。先确认作业真的在跑吗?如果你用spark-submit提交的是Java或Python任务,它运行完就退出了,UI自然不在了。其次确认有没有在代码里关闭过spark.ui.enabled,有些团队为了“减少开销”会把它设为false,这是排查UI打不开的头号原因。
4040端口被占用时,Spark会自动往后找一个可用端口,比如4041、4042。如果你启动多个应用,或者别的前端服务占了4040,端口就会顺延。这时候可以直接看应用日志,里面会打印一行类似Bound SparkUI to 0.0.0.0, and started at http://...的提示,那才是真正的访问地址。
如果你是提交到集群再访问UI,遇到打不开的情况,先不要怀疑防火墙。第一确认你这个应用还活着没有,YARN页面进去之后,如果Tracking UI有跳转链接,点开即可。如果Tracking UI点了没反应,多半是YARN的代理访问配置没开。这时需要找集群管理员开启相应配置,自己别直接改生产集群的配置。
4.2 页面显示不全、没有SQL页、没有数据
有同学跑了一段含有Spark SQL的代码,打开UI却发现没有SQL Tab,很慌。其实原因可能很简单:这次运行根本没有执行到SQL语句,或者SQL逻辑在代码的其他分支没有走到。SQL Tab是靠SQL执行事件驱动的,只有真正跑过SQL,才可能生成记录。
Storage页没有任何数据,多半是persist或cache之后没有触发Action。我前面说过,persist是惰性的,不触发Action数据不会真的缓存。另一种情况是缓存级别写的是MEMORY_ONLY,而内存本身不够,导致数据落不了内存被丢弃。这时候可以看看Storage页有没有显示“Fraction Cached”小于1,有的话说明缓存不完整,要么调大executor内存,要么换成MEMORY_AND_DISK级别。
还有人说Stages页的DAG图不显示或者只有文字没有图。常见原因是浏览器版本太老,或者页面加载时事件还没完全处理好。最简单的办法是强制刷新,或者换Chrome系浏览器重开一次。真干活的时候不要在这种问题上耗太久。
4.3 如何用HistoryServer做事后复盘
Spark应用一旦结束,WebUI地址就访问不到了。这时候要看历史记录,就得靠Spark HistoryServer。它的原理是:应用运行期间,把执行过程以事件日志的形式写到指定目录;HistoryServer启动后去读这些日志,渲染成和WebUI几乎一样的页面。默认端口一般是18080。
想让HistoryServer正常工作,提交应用前就要开启事件日志记录。相关配置有两项:spark.eventLog.enabled=true,以及spark.eventLog.dir指定存放目录(HDFS或本地路径)。HistoryServer启动时也要用spark.history.fs.logDirectory指向同一个目录,两边目录对不上就看不到记录。
很多团队把Task跑挂了之后,第一反应是翻YARN日志,这当然没错。但如果能养成“跑完看一眼HistoryServer”的习惯,你会发现大多数执行层面的问题根本不用翻日志,UI里全都有。尤其对于Sliding Window式的地毯式排查,HistoryServer能让你一次性对比多个历史作业的执行情况,从趋势里看出哪个参数是最近才恶化的。
5. 我的几条实操心得
这套排查方法,我自己后来反复用了很多次,有些细节值得单独拎出来讲。
第一个心得:先看Task分布,别直接看Job总耗时。Job总耗时只是一个结果,并不能告诉你瓶颈在哪个环节。但Task表格里按Duration排个序,几秒钟就能看出“平均很短但有长尾”的倾斜特征。这个判断速度比你盯着DAG图苦想快得多。
第二个心得:GC Time一定要扫一眼。很多人排查慢作业只看CPU和数据量,忽略了垃圾回收。如果发现Task的GC Time占总执行时间的30%以上,说明内存压力非常大,应用很可能在频繁Full GC。这时候你调整并行度、增加分区都不一定有效,优先考虑加大Executor内存或者优化对象存储方式,效果会更明显。
第三个心得:把UI截图存下来,尤其是失败作业的。排查完一个问题后,随手把Jobs、Stages、Executors三个页面的关键信息截个图放工单里,会比空口描述“任务很慢”有说服力得多。而且过两周再遇到类似问题,翻旧截图对比一下能节省很多时间。
第四个心得:养成跨应用对比的习惯。同一个任务,昨天跑40分钟,今天跑2小时,不要只盯今天这一个应用。打开HistoryServer,把连续跑了几天的同类作业放在一起看,很容易发现是数据量涨了、某个参数被改了,还是集群资源被别人挤占了。UI不只用来解决问题,更值得用来找趋势,这个价值往往被新手忽略。
这次就说这么多。Spark WebUI的内容说深可以很深,但对绝大多数新手而言,先把Jobs、Stages、Executors三个页面玩熟,再配合Storage和SQL页做专项排查,日常需求已经覆盖得七七八八了。下次再遇到作业卡住,希望你先别急着四处求助,打开UI,按这套顺序走一遍,很多答案其实就摆在页面上。