
干我们这行的跟Spark打交道绕不开一个东西——浏览器里那个8080端口起的Web界面。很多人把它当成监控页瞄一眼Job跑没跑完就关了。但说实话我会花很多时间盯着Spark UI看因为大部分性能问题、内存问题、倾斜问题答案全写在这个界面里只是很多人不会读。这篇文章我打算把Spark UI从入口到每个页签再到背后的控制参数逐层剥开讲透。包括每个界面上的指标到底是什么意思、有什么用、怎么根据界面表现反推代码和配置的问题。不绕弯子全部实战视角。1. 为什么要花时间理解Spark UI先聊点实际的。Spark任务跑挂了、跑慢了第一件事不是看日志是打开UI。为什么因为UI是Spark运行时状态的汇总视图它把你分布式跑在几十台机器上的信息统一聚合成一张张可读的页面。日志你还要去每台机器翻UI打开就是全局视角。UI的核心价值有三块。第一任务进度可视化。哪个Stage在跑、跑了多久、多少任务成功多少失败一眼就看到。第二资源使用情况可视化。每个Executor用了多少内存、多少磁盘、GC耗时多少全都有。第三执行计划可视化。SQL页签里可以看到Spark SQL从解析到物理执行的完整过程这对于排查数据倾斜、广播变量失效这类问题价值极大。另外还有一个很多人忽略的点Spark UI反映了Spark内部事件机制。界面上的每一个指标背后都有对应的SparkListener事件在驱动。理解UI的同时其实也在理解Spark任务的整个生命周期。做性能调优如果连UI都读不顺基本等于盲人摸象。1.1 Spark UI不只是个看板把Spark UI理解成“看板”其实有点窄了。它不只是展示状态的页面更是一个诊断工具和调优入口。举个例子你在Jobs页面看到某个Job耗时特别长点进去发现是某个Stage占了90%的时间再点进Stage详情能看到某个Task的Shuffle Read异常大——这一条链路走下来问题定位就已经完成了一大半。再比如你发现GC时间很长点进Executors页面看每个Executor的GC耗时和堆内存使用率就能判断是内存分配不足还是代码里产生了大量对象。这些判断没有UI数据支撑只能靠猜。靠猜调优通常越调越糟。1.2 界面参数背后的来源与入口Spark UI不是独立存在的它的数据来源和展示行为都受一组配置参数控制。最常见的入口参数就是spark.ui.port默认8080。多任务同时跑在同一台机器上时端口冲突很常见这时候你可以显式指定不同端口或者设置spark.ui.port0让系统随机分配空闲端口。UI相关的参数远不止端口。比如spark.ui.retainedJobs控制Jobs页面保留多少个Job的记录默认1000。spark.ui.retainedStages控制Stages页面保留多少条Stage记录默认1000。spark.ui.retainedTasks控制每个Stage保留多少条Task记录默认100000。这些参数在任务量大、UI刷新频繁时很关键。还有一组跟Event Log相关的参数虽然不直接显示在UI界面上但决定UI能不能在任务结束后还能查。spark.eventLog.enabled设为true后Spark会把事件写入指定目录之后可以通过spark.history.ui.port访问History Server回放任务过程。2. 核心页签逐个拆解Spark UI的页面结构相对固定主要有Jobs、Stages、Storage、Environment、Executors、SQL这几个页签不同版本可能略有差异但核心内容一致。我逐个说清楚每个页签能干什么、怎么用、怎么看。2.1 Jobs页签从全局到局部的入口Jobs页面是Spark UI打开后默认展示的页面它按Job维度列出所有作业。每个Job对应一个Action操作比如count()、collect()、saveAsTextFile()都会触发一个Job。页面上的表格会展示Job ID、Description、Submitted时间、Duration、Stages Summary、Events Timeline等信息。看这个页面时我一般会快速扫一眼Duration和Stages Summary。如果某个Job的Duration比其他Job高出几个量级那基本就是问题了。这时候点Job ID进去可以看到这个Job包含的所有Stage以及每个Stage的状态。如果某个Stage一直处于Running状态说明卡住了要么在等资源要么有任务在反复重试。有个小技巧Description列默认显示的是collect at XXX.scala:123这种格式很含糊。建议在代码里给Action加上有意义的名称比如df.count()改成df.count().as(count_user_table)这种思路或者用SparkContext.setJobGroup来分组命名。这样UI上展示的信息会更清晰定位问题时省很多时间。2.2 Stages页签性能诊断的主战场Stages页面是整个Spark UI信息量最大、最值得深挖的页面。每个Job拆分成多个StageStage之间通过Shuffle或者Broadcast连接。点进任何一个Stage上方的Summary Metrics表格会列出所有Task的指标汇总包括Duration、GC Time、Shuffle Read Block Size、Shuffle Write Size等。看这个表格时我通常先看三个指标Duration的Min和Max、Shuffle Write Size、Shuffle Read Size。如果Duration的Max远大于Median说明有任务拖后腿大概率是数据倾斜。理论上Map端任务处理的数据量应该相对均匀如果某个Task的Shuffle Write Size是其他Task的好几倍那这个Task分配到了过大的数据分区就是倾斜。页面下方是每个Task的详细列表可以按Duration排序快速找到最慢的几个Task。再往下是Event Timeline图展示了Executor的添加移除、Task的调度执行时间线。这图对判断资源分配时机很有用比如Executor是任务开始后很晚才加的说明动态资源分配在起作用。2.3 Storage页签缓存与持久化状态一览Storage页面展示的是RDD、DataFrame在缓存中的状态。这个页面相对简单列出每个缓存对象的RDD Name、Storage Level、Cached Partitions、Fraction Cached、Size in Memory、Size on Disk、Memory Size等。看这个页面的核心目的就两个第一确认缓存是否真正生效。有时候代码里写了.cache()但实际没触发Action缓存根本没建立这里就看不到对应条目。第二确认缓存是否被合理利用。如果某个表只需要用一次那就没必要缓存缓存反而会浪费内存、增加GC压力。有个坑我得特意提一下Storage页面的Size in Memory显示的是估算值并不会非常精确因为Spark的存储内存是动态调整的。如果发现缓存对象的Fraction Cached经常低于100%说明内存压力大部分分区被溢写到磁盘了这时候就要考虑调大spark.memory.fraction或者减少缓存数据量。2.4 Environment页签排障时先来这里Environment页面会把Spark任务运行时的所有环境信息和配置参数展示出来包括Java版本、Java Home、Scala版本、系统变量、Classpath、Spark Properties等。这页面平时很少人会翻但排障时价值极高。举个真实案例。有一次任务在集群上跑所有参数都对但就是连接不上外部数据库报错信息说找不到驱动类。我第一反应是Classpath问题于是打开Environment页面搜索Classpath条目确认驱动jar包是否被正确提交。类似这种“代码没问题但环境有问题”的场景Environment页面就是辅助定位的工具。另外当你怀疑某些参数没生效时也来这里确认。比如设置了spark.executor.memory真正确认Executor实际用了多少内存还是要看Environment里的Spark Properties和后面的Executors页面。如果两者不一致大概率是参数没传对或者被更高优先级的配置覆盖了。2.5 Executors页签内存和计算资源监控Executors页面列出了每个Executor的运行状态包括Executor ID、Address、RDD Blocks、Memory Used、Disk Used、Active Tasks、Failed Tasks、Complete Tasks、Total Tasks、Task Time、GC Time等。这个页面是内存和Task执行情况的核心监控入口也是我做资源评估时看得最多的页面。Memory Used列展示的是Storage Memory的使用量注意不是Executor进程的总内存使用量。这里Boot用来判断RDD缓存占用了多少存储内存但不反映执行内存Execution Memory的使用情况。要判断执行内存是否充足需要结合GC Time和Task执行速度来推断。GC Time这个指标值得给到足够关注。如果总GC时间占比很高比如超过总任务时间的10%以上就说明内存中对象过多、垃圾回收频繁代码里可能有大量不必要的对象创建。常见优化思路包括减少shuffle的中间对象、使用mapPartitions代替map、避免在循环里创建大对象等。2.6 SQL页签查看物理执行计划SQL页签是Spark 2.0以后新增的重要页面展示Spark SQL的执行计划和执行指标。每条SQL对应一个Query点进去可以看到物理执行计划树每个算子的执行时间、处理行数、shuffle数据量。这个页签对排查SQL性能问题非常有用。举例来说如果你发现某条SQL执行很慢可以在SQL页签里看到哪个算子耗时最长。正常情况下Shuffle操作和Join操作相对耗时但如果一个简单的Filter操作耗时异常可能是谓词下推没生效或者是UDF阻碍了优化。看SQL页签时有个技巧对比每个算子的“number of output rows”。如果某个算子的输出行数远大于输入行数说明产生了数据膨胀常见原因包括Join的笛卡尔积、explode函数使用不当、或者Broadcast Join失效导致Shuffle Join。顺着这个逻辑追下去往往能定位到问题根因。3. 与界面相关的配置参数解析前面讲的是界面上的内容怎么读现在讲UI本身怎么配置。Spark UI有一整组spark.ui.*前缀的参数控制UI的端口、保留数据量、是否启用、刷新行为、访问控制等。这些参数虽然不起眼但配置不当会直接导致信息丢失或者无法访问。3.1 端口与访问控制配置UI相关的核心参数是spark.ui.port默认值8080。生产环境中一台机器上可能同时跑多个Spark应用如果都使用默认端口后启动的应用会因为端口占用而无法启动UI。解决办法有两个一是为每个应用指定不同的端口二是设置为0让Spark自动选择空闲端口。# 方式一手动指定端口 spark.ui.port18080 # 方式二随机端口通过日志查看实际端口 spark.ui.port0UI访问控制是另一个容易被忽视的点。默认情况下Spark UI不设任何认证任何能访问到该IP和端口的人都能看到任务详情和日志这在生产环境里有信息泄露风险。可以通过spark.ui.filters指定自定义过滤器类或者用spark.acls.enable配合spark.admin.acls做粗粒度的访问控制。我建议至少设置spark.acls.enabletrue并配置好管理员用户列表。3.2 事件历史记录配置在集群模式下任务跑完后UI就在浏览器里消失了之后想再看就很麻烦。解决办法是启用事件日志并部署History Server。相关的核心参数如下# 启用事件日志 spark.eventLog.enabledtrue # 日志存储目录HDFS或本地文件系统都支持 spark.eventLog.dirhdfs://namenode:8020/spark-history # 日志文件滚动大小 spark.eventLog.buffer.kb128k # History Server端口 spark.history.ui.port18080启用事件日志后每个Spark应用的生命周期事件都会被序列化写入指定的目录History Server启动后自动读取这些文件并渲染成UI。这意味着任务跑完几个月后再打开UI还能看到当时的完整运行情况。对于复盘线上问题和做性能分析这个能力非常重要。3.3 保留时间与数据清理参数Spark UI在应用运行过程中会在内存里维护Job、Stage、Task的元数据展示在对应页面上的就是这些内存数据。默认的保留数量有限参数控制如下# Jobs页面最多显示最近1000个Job spark.ui.retainedJobs1000 # Stages页面最多显示最近1000个Stage spark.ui.retainedStages1000 # 单个Stage页面最多显示100000条Task记录 spark.ui.retainedTasks100000 # 所有Stage页面合起来最多保留100000条Task记录 spark.ui.retainedDeadExecutors100这些参数在任务数量极大时才会触及上限。举例来说如果一个Streaming应用每天处理几十万个微批次Stage条目会爆炸式增长。这种情况下如果不调大retainedStages较早的Stage信息会被覆盖掉排查问题时可能找不到想要的数据。但调大这些参数也有代价UI占用的JVM内存会增加。所以参数设置要和实际需求匹配不要盲目调大。3.4 与UI相关的其他实用参数除了上面这些还有几个参数值得留意。spark.ui.showConsoleProgress默认false设为true后在提交应用的终端里会显示Stage的进度条。虽然不影响UI但对本地调试非常有用。spark.ui.killEnabled默认true允许在UI上直接kill Stage或者Job。生产环境建议设为false防止误操作导致任务中断。spark.ui.xXss控制UI页面的跨站脚本过滤策略一般保持默认即可。spark.sql.ui.retainedExecutions控制SQL页签里最多显示多少条SQL执行记录默认1000和Spark SQL任务数有关。参数设置有一个基本原则能用默认值的就不动除非实际场景触发了问题。UI参数尤其如此大多数情况下默认配置足够实用。4. 用UI做性能排查的实操方法理解了界面和参数最终目标是为了排查问题。这一环节我不谈理论直接讲我实际用UI排查过的几类典型问题每一步是怎么操作的以及当时的判断依据是什么。4.1 数据倾斜问题怎么看数据倾斜是我遇到频率最高的Spark性能问题。外在表现是一个Stage内绝大多数任务几秒跑完但有一两个任务跑了十几分钟甚至更久。排查路径很简单。打开问题Job对应的Stage页面看Summary Metrics里的Duration列。如果Max远大于Median比如Median只有5秒Max却到了900秒基本可以判断有倾斜。再看Shuffle Write Size列如果某个Task的写入量是其他Task的几倍那么就是这个Task对应的分区数据量过大。定位到倾斜任务后解决办法要看具体场景。如果倾斜发生在Join阶段可以考虑给小的DataFrame加盐salting把热点数据分散到多个分区。如果倾斜发生在GroupBy阶段可以先做局部聚合再做全局聚合也就是所谓的两阶段聚合。如果是读取数据源时就倾斜可以考虑修改表的分区策略。UI在解决倾斜问题上能起到的作用就是“快速定位”。具体调优方案还需要结合代码和数据特征来定。4.2 内存压力问题怎么看Executor内存压力大外在表现是频繁GC、任务执行缓慢、甚至OOM导致Executor被移除。UI排查路径如下打开Executors页面看GC Time列。如果某个Executor的GC时间明显偏长或者所有Executor的GC时间占总运行时间的比例超过10%就需要警惕。接下来看Storage页面确认缓存的RDD是否占用了过多存储内存。Spark的堆内内存由spark.memory.fraction控制默认0.6其中Storage和执行内存共享这部分。如果RDD缓存占用过高执行内存相应减少就会频繁GC甚至报OOM。解决思路有几条检查代码里是否有不必要的cache()或persist()该去掉就去掉如果必须缓存可以考虑降低存储级别比如从MEMORY_ONLY改为MEMORY_AND_DISK让Spark在内存不够时溢写到磁盘还可以调整spark.memory.fraction增大执行内存的占比但要注意不要因为调参而掩盖了代码本身的问题。4.3 慢任务定位与Executor异常还有一种场景是某个任务卡了很久或者任务反复重试。在Stages页面里如果看到某个Task一直在Running或者经常出现FAILED后重新提交就需要点进Task列表看详细情况。Task反复失败常见原因包括Executor意外退出、运行时异常、或者某个Task处理的数据有问题。这时点击失败的Task能看到失败原因和对应的Stack Trace。注意看错误信息里是否有OutOfMemoryError或者Container killed字样这会直接影响后续处理方向。如果Executor本身被移除了Executors页面里会显示带DEAD状态的Executor。spark.ui.retainedDeadExecutors参数就是控制保留多少条这种记录。查看死Executor的历史记录往往能发现OOM或者NodeManager失联等底层问题。4.4 SQL执行计划怎么看SQL页签排查问题的场景也很常见。有一次我遇到一条SQL原本跑10分钟突然变成跑1小时用UI排查后发现Broadcast Join失效了变成了SortMergeJoin。原因可以分析一是小表数据量增长超出了spark.sql.autoBroadcastJoinThreshold的默认值10MB二是代码里显式提示导致优化器被迫放弃Broadcast。在SQL页签里展开对应的Query可以看到每个算子的耗时和输出行数。如果发现某个ExchangeShuffle节点输出行数异常多那就要思考是不是Join条件写错了或者关联键中有大量空值。对于排查这类问题实际操作中可以先给SQL加上EXPLAIN分析执行计划但要全局视角还是离不开UI里的可视化计划树。5. 常见问题与排查技巧实录写这部分内容的目的很直接把我的错误和踩坑经验写出来帮你少走弯路。因为UI相关的毛病网上讨论很少很多都是自己在实操中捶出来的教训。5.1 UI打不开或者端口冲突最常见的问题是UI打不开或者端口冲突。情况是这样的提交Spark任务后日志里提示可以访问某个URL但浏览器就是无法打开。根据我的经验排查思路按以下优先级走。第一确认进程是否还活着。如果Application已经结束UI自然就关了。这时需要启用Event Log和History Server才能继续看。第二确认端口是否被占用。多个Spark应用同时跑在同一个节点上且都用了默认的8080端口必然冲突。可以通过netstat -anp | grep 8080查看端口占用情况然后显式指定不同的spark.ui.port。第三确认网络或防火墙限制。UI监听所有网卡接口但如果是跨节点访问要确认对应端口是否放行。# 查看端口占用 netstat -anp | grep 8080 # 如果端口被占用换个端口提交任务 spark-submit \ --conf spark.ui.port18080 \ your-job.jar5.2 任务结束后UI数据丢失另一个困扰过得比较久的点是任务结束后UI数据丢失。任务跑完浏览器里UI还在只是过一段时间再刷新就变成了404。如果没启用Event Log这是正常现象UI数据只存在于内存中进程退出后就没了。要想任务结束后还能查看UI只能用History Server方案。配置前文已经提过核心点是spark.eventLog.enabledtrue和spark.eventLog.dir要设置对。有个容易踩的坑是History Server启动时指定的日志目录必须和Spark应用写入的目录完全一致不然读不到记录。# 启动History Server $SPARK_HOME/sbin/start-history-server.sh \ --properties-file conf/spark-defaults.conf配置文件里加上spark.history.fs.logDirectoryhdfs://namenode:8020/spark-history5.3 UI数据与日志不一致有一种情况是UI数据和日志不一致例如UI显示某个Task成功了但日志里明明有错误记录。这通常是因为UI的指标数据每5秒才刷新一次如果你在任务执行过程中刷新页面看到的状态可能不是实时的。这是Spark UI的默认行为spark.ui.port和spark.ui.updateInterval参数可以调整刷新频率但太频繁会增加Driver压力建议保持默认。另外UI里显示的Shuffle Read和Write量是估算值不是精确值。Spark通过SizeTracker来采样估算集合的大小因此会有误差。在做精细调优时需要结合其他监控手段比如在Driver和Executor日志里查看更精确的GC和内存数据不能完全依赖UI展示的数值做决策。5.4 UI本身响应很慢UI页面打开后加载很慢或者在滚动Task列表时卡顿这也是比较常见的情况。原因通常是Task记录数量太大UI在做前端渲染时开销高。我处理过的案例里某个大任务的Stage包含几十万个Task点开Stages页面后浏览器直接卡死。这种场景的解决办法不是去优化UI而是减少展示的数据量。调低spark.ui.retainedTasks让UI只保留最近的任务记录是最直接的手段。比如把100000调成10000UI压力能大大降低。当然这样设置后较早的Task记录查不到需要根据你的实际需求权衡。5.5 用账号密码保护UI环境我最后再提醒一个安全相关的点生产环境的Spark UI一定要做访问控制。默认情况下UI没有任何认证如果你的集群节点暴露在内网任何一个能连到你机器的同事都可以看到你的任务代码路径、日志、甚至杀死正在运行的任务。一个真实教训有一次测试环境的Spark UI放开了访问权限结果有其他组的同学在界面上误点了Kill按钮导致我们的任务全部中断。建议的最小化配置是spark.acls.enabletrue spark.admin.aclsadmin_user spark.modify.aclsadmin_user如果希望更细粒度地控制谁可以看UI、谁可以操作任务可以基于spark.ui.filters引入自己的HTTP过滤器和认证逻辑。逻辑不简单但生产环境里值这个成本。6. 深入场景多任务同时监控怎么处理在真实工程环境里不会只跑一个Spark应用更多情况是同一时间有多个任务在跑甚至同一批任务周期性地提交。这种情况下8080那个默认UI端口根本不够用你需要一套更系统的方案来同时管理和监控多个任务的UI。6.1 多端口策略手动分配还是随机分配多任务同时存在时可以手动给每个任务分配一个固定端口也可以让Spark自动分配。手动分配的优点是可预期哪个任务对应哪个端口看一眼就知道缺点是集群环境下的端口规划比较麻烦还要避免冲突。自动分配spark.ui.port0的优点是省心缺点是端口不固定每次都要从日志里现查实际端口。# 提交任务时动态获取实际UI地址 spark-submit \ --conf spark.ui.port0 \ your-job.jar # 日志中查找 Bound SparkUI to 0.0.0.0, and started at http://...如果你跑的是Streaming任务建议用固定的端口规划因为你需要长期稳定访问同一个UI做监控。如果是批处理任务用0随机分配更省事跑完就结束端口谁是谁不重要。6.2 History Server统一查看已成任务对于已经结束的任务无论当时用的什么端口最终都可以通过History Server统一查看。History Server启动后会从spark.history.fs.logDirectory里定时扫描新的事件日志自动渲染出完整的UI页面。我用这个方案做的事情有两类。一类是线上问题复盘。任务是凌晨跑的第二天业务方报了数据异常我打开History Server找到当时的UI记录确认任务当时是否有失败重试、是否有Stage异常。另一类是性能对比优化。修改代码或参数后重新跑一遍任务在History Server里对比两次任务的Stage耗时。6.3 监控作业里常见的告警配置思路UI本身不提供告警功能但你可以基于UI背后的数据源来搭建告警。常见做法是监控Event Log生成的文件或者解析spark.accumulators.sql.executionMetrics等指标再接入自定义的告警逻辑。生产环境中的应用如果频繁失败或超时手动盯UI是盯不过来的。建议配置条件如下某个Job执行时间超过阈值、某个Stage失败次数过多、Executor近几分钟内连续GC占比过高。具体怎么实现可以利用已有的监控链路比如让任务本身周期性打印UI相关指标到日志再通过日志采集系统做告警。需要注意的是告警策略要贴合业务过多告警会逐步麻木反而对真正的问题失去响应。7. 版本差异与生态工具Spark UI不是静止不变的每个Spark版本都会在界面细节、指标含义、页面布局上有所调整。了解这些差异能帮你避开“换版本之后UI不会看”的窘境。7.1 Spark版本之间的UI差异Spark 2.x的UI和Spark 3.x相比SQL页签更完善新增了Structured Streaming的监控页。Spark 3.0后还加入了自定义Resource Profile和相关展示代码生成阶段的耗时也能在SQL页面看到。如果你从2.3升级到3.3会有“界面更好用了”的直观感受。还有一些细微差异值得注意。比如在Spark 3.0中Stages页面新增了Flaky任务标记用于识别间歇性失败的任务。这个标记对定位“偶尔失败、重试就成功”的任务很有用。另外Executors页面里内存指标的计算方式不同版本间会有微调对比不同版本时不要直接拿数值硬比。7.2 第三方工具在UI上的扩展基于Spark UI的Extension机制社区有一些用于增强UI的工具。这些工具的Load思路是在spark.ui.customExecutors或者spark.sql.ui.retainedExecutions等配置点上做文章但目前多数定制能力有限。如果你需要更多自定义指标和图表一般建议在UI之外自建监控看板而不是强行扩展Spark UI。你可以在UI基线上做二次开发把需要的自定义Metrics从Listener里提取出来推送到自己的监控系统里形成看板。这也是我实际采用的方式UI看基础数据自己的看板看业务自定义指标。二者互补效率最高。8. 结尾经验和建议先说个人体会。Spark UI是一个被低估的工具。很多人觉得它只是展示一下任务进度但真正用熟了以后你会发现大部分Spark性能问题——数据倾斜、内存瓶颈、Executor异常、SQL退化——都能在UI里找到线索。每次做完调优再回看UI也能从数据上验证优化是否有效。这里我再分享几个自用的UI使用技巧第一拿到Spark任务第一件事是把spark.eventLog.enabledtrue和spark.eventLog.dir配置好。无论多小多临时的任务都值得开因为跑完复盘时UI能回放这个习惯让我解决了大量说不清楚的问题。第二排查性能问题时我给自己定了一个阅读顺序先看Jobs页面找耗时异常的Job再点进Stages找耗时异常的Stage再看Summary Metrics里的Duration和Shuffle列然后去Executors看GC和资源最后去SQL页签看执行计划。按这个顺序走基本不会漏。第三UI里看到的指标是“结果”不是“原因”。比如GC时间长是结果原因可能是代码里对象分配过多也可能是Executor内存太小还可能是缓存占用过高。不要只看UI就给结论应该用UI定位到具体模块之后再结合代码和日志综合分析。这个后续还可以延伸到Spark中其他可视化相关的内容——比如Structured Streaming UI里的进度信息、History Server的事件时间线、以及自建监控里对UI指标的自定义聚合。每条线都值得单独开一篇来聊这次先把Spark UI的基础彻底讲透剩下的路你按着这个思路去探索应该会顺很多。