☰
Spark UI实战:从Stage与Executor指标到数据倾斜与参数调优
2026/9/29 16:30:10 网站建设 项目流程

1. 从点错页面到看懂全局:Spark UI的层级关系先理清

很多人第一次点开Spark UI,习惯性先看最显眼的Jobs列表,发现作业失败或卡住,马上就慌。但UI这玩意儿,本质上是一套按执行粒度层层嵌套的档案系统,你跳过了外层直接看内层,等于看一本只有目录的书——信息都在,就是串不起来。

Spark UI的逻辑层级是固定的一套:Jobs(作业) → Stages(阶段) → Tasks(任务),外加Executors(执行器)和Storage(存储)两个辅助视角。一个Job由一次Action(比如count()、save())触发,Job里可能包含多个Stage,Stage之间以Shuffle为边界切开,每个Stage里又根据数据分区拆出很多Task。UI的主页默认按Job倒序排列,点击一条Job记录,你才能看到它包含的Stage列表,再点进某个Stage,才能看Task级别的明细。

这里有个新人最容易犯的错:看到一条Job整体变红就以为完蛋了,直接从头开始调参数。正确做法是先点进Job详情,看每个Stage的耗时占比。大多数实际场景里,一个Job的慢不是所有Stage都慢,而是某一个Stage拖了后腿——数据倾斜通常发生在Shuffle之后的聚合Stage,资源不足通常表现为所有Task运行时间超长。你只有先定位到是哪个Stage出了问题,后面调参数才有意义。

还有个小细节:UI顶部通常有SQL这个入口,但它的入站条件是有Spark SQL或DataFrame操作被触发过。你跑纯RDD操作时,这个标签页就是冷的,别在那里翻半天。按我自己的习惯,排查一个作业,先看三个地方就够了:Executors页确认资源是不是比申请时缩水了,Stage页确认是不是存在明显的Task耗时不均,SQL页确认物理执行计划有没有和预料中差别很大。这三步看完,80%的锅基本能定位。

2. Executor标签页的读数方法:把内存参数和界面数字对上号

Executors页面是所有性能排查的第一现场,因为它展示的是真实运行时的资源占用,不是你在提交参数里写的理想状态。页面上每行代表一个Executor,列的排序可以点表头调整,最关键的列是这三项:RDD Blocks、Storage Memory、Disk Used。

先说内存。你在提交脚本时写的spark.executor.memory,配置的是JVM堆内内存,但UI里显示的Storage Memory其实是堆内加堆外两部分加起来的结果。Spark在初始化时会从executor内存里划出一块专门的区域给RDD缓存和Shuffle聚合用,大小由spark.memory.fraction控制(默认0.6),这0.6又按spark.memory.storageFraction拆成执行内存和存储内存(默认0.5对0.5)。所以你看到某个Executor的Storage Memory一直很满,不要第一反应就觉得是缓存数据太多,先check一下是不是Shuffle期间的聚合数据也占用了这块区域——执行内存用完是会往存储内存区借的,这俩是动态抢占关系。

再一个容易被忽略的指标是GC Time。如果你看到某个Executor的GC时间飙升到几分钟,说明堆内压力极大——通常是spark.executor.memory给得不够,对象频繁创建和回收。这时候去翻这个Executor的日志,多半能看到规律的Full GC记录。很多人舍不得加内存,总觉得"代码优化一下就行",但实际上如果数据量级的Shuffle确实摆在那,该给的内存跑不掉。不过加内存也不是无脑加,经常见到spark.executor.memory=8g配着spark.executor.cores=8的配置,一个executor开了8个核,这其实是把内存分给了8个并发任务,单个任务能用的还是那一丁点。合理配比通常是cores=2、memory=4g~6g,保证单核能吃到2~3GB内存,这个比例在绝大多数集群上都不会太离谱。

还有Disk这一列以及与之相关的Shuffle Spill。SQL执行时如果内存缓冲不够,排序和聚合的数据会溢写磁盘,这在页面上的体现就是当前任务的Shuffle Spill (Memory)和Shuffle Spill (Disk)两列都有值。如果Disk的值远大于Memory,说明内存规划严重不足,执行内存在还没读完数据时就被打满了,系统只能把中间结果落盘,大量I/O自然拖慢作业。这时候除了调大executor内存,也可以考虑调整spark.sql.shuffle.partitions——如果这个参数设置得过大,每个分区的数据量太小反而管理开销大;设置得过小,单分区数据量过大又容易溢写。实际执行计划里,单个Reduce分区的数据量控制在128MB~256MB左右,是我个人实测比较稳的一个区间。

3. Stage页的“数据倾斜CT机”:三个表象定位一个病根

你可以把Stage详情页想象成一张分布式系统的CT片子,Task列表里的每一行都是一个分区上的局部快照。倾斜的本质很简单:某个分区的数据量远大于均值,表现出来就是它的Task耗时比其它Task高出一个数量级,而且同一Stage里大量Task已经Success了,就剩几十个还在跑。这种图我这边一天能见好几回,不用任何监控平台,肉眼就能扫出来。

判定倾斜,先看Shuffle Read Size这一列。正常的任务之间,这个值的分布应该是相对均匀的;如果出现了某个Task的Shuffle Read Size是其它Task的5倍以上,倾斜就已经成立了。这时去点开那个异常Task,看它的日志里有没有spill相关的记录,如果有,问题基本进入了内存和磁盘的双重困境。

代表成功后,还有一类倾斜是从界面数字上看不出来的,但会在运行时间上暴露:比如Task Deserialization Time或Executor Computing Time异常,而Shuffle Read Size不大——这多半是计算逻辑本身有热点键,某一条key的维表关联或者循环处理本身耗费极高。这种问题靠改参数没用,得改代码,典型做法是把倾斜的key打散成随机前缀,再和维表做两次join。

页面下方的DAG Visualization也是一大信息来源,但大多数人只会在作业失败时去看一眼,平时根本不看。我建议你把DAG当成一个执行叙事线来看:每个方框代表一个RDD转换或算子,连线代表数据依赖关系。一旦出现Exchange节点,就说明这里发生了Shuffle;如果DAG里一个Stage内部出现了两次Exchange,那大概率是执行计划没有优化干净——比如多表join时Spark没有把条件推下去,生成了冗余的shuffle。这种时候去看看spark.sql.autoBroadcastJoinThreshold有没有生效:小表如果超过默认的10MB阈值,Spark就不会广播它,转而走SortMergeJoin,多一次shuffle,传到UI上的体现就是Stage数变多、耗时变长。把这个阈值适当调大(比如调到20MB),很多中小维表的Join作业能明显提速。

Stage页面还有两个不太起眼的数字:Scheduler Delay和Task Deserialization Time。如果Job运行中,Executors健康稳定,这两个值也正常,问题就不在资源申请和网络传输层面;但如果Scheduler Delay非常高,通常有两种可能:一是动态资源分配(spark.dynamicAllocation.enabled)在反复调整Executor数量,每次调整都要做存量Task的重新调度,中间会产生较长的空窗;二是你开了太多的spark.sql.shuffle.partitions,导致单个Stage创建了上万个极小的Task,调度器光是分发这些Task就耗掉了大把时间。

4. SQL执行计划与参数联动:不只看“对不对”,还要看“为什么会这样”

SQL标签页里最有价值的东西在Physical Plan那一栏。点进去你能看到Spark准备怎么执行你这套查询,但默认展示的是一棵长的要命的树,新手想着去读每个节点是浪费时间,更简单的切入口是找关键词:Exchange、Sort、Shuffle、Broadcast、Scan这几个词盯住就够。

比如说,你在SQL页看到最终的物理计划里,有一个大扫描节点的输入量是数TB级别,而这个扫描上面没有跟任何Filter或Projection下推,那说明你的分区过滤条件没有生效。UI上显示是执行引擎老老实实把全表数据读出来再筛,这就是典型的Partition Pruning失效,代码里大概率是写了对分区字段做了函数运算(比如to_date(col)再拿去查),Spark在无法对函数结果做静态判断时,只能退化成全表扫描。这个问题的解法不是调参数,是改SQL:先算出日期范围,再用>=和<=直接过滤。

紧接着看Exchange出现的次数和位置。正常的Reduce阶段在两个Stage之间只需要shuffle一次;如果是同一张表在同一个Stage里出现了多个Exchange,那多半是逻辑里的Join顺序没有选好。Spark默认用SortMergeJoin来处理两张大表join,但如果一张大表经过过滤后实际参与Join的数据量已经缩到很小,就不如用广播。这里有个关键参数:spark.sql.adaptive.enabled——我会建议在海量数据作业里直接打开。它能在运行时根据统计信息重新优化执行计划,如果前面那一轮过滤已经把一张表缩到很小,自适应优化器会自动把SortMergeJoin替换成BroadcastHashJoin,在UI里看到的结果就是Exchange节点变少了。不用手动改多少代码,效果立竿见影。

关于自适应执行还有一个专门对应的UI变化:如果开了AQE(Adaptive Query Execution),在SQL页的计划里你会看到一些动态调整的标注,例如预估的分区数不再等于spark.sql.shuffle.partitions的设置值,而可能被动态缩小。很多人会奇怪"我明明设置了200个分区,怎么页面上只有几十个",其实这是AQE在根据末端阶段的实际数据量自动合并了稀疏分区,默认由spark.sql.adaptive.coalescePartitions.enabled控制。这个功能我在生产环境一直是打开的,它能省掉大量小文件写出的问题,尤其是下游要接Hive分区表时,动不动几百个小文件能把文件系统MetaStore压垮。

另外SQL页面还有一个Metrics列,里面的scan time、output rows很重要。跑一遍相同SQL,如果前后两次scan time相差巨大,别急着怀疑机器变慢了,先检查是不是跑到了同一个资源池里、以及是否因为前一次作业的缓存未清理导致数据落在了内存。UI里看到的scan time反映的是真实I/O开销,把它和parquet列裁剪打开与否关联起来,能让你的排查维度更立体。

5. 参数调整如何闭环验证:先改一处,UI上追踪三十分钟

讲了不少UI观察和参数关联,再说说实际调优中最容易翻车的地方:改完参数没有在UI上闭环验证,急急忙忙就上线。

我自己定的一个工作习惯是:每次只改一个参数,不要一次把spark.executor.memory、spark.sql.shuffle.partitions、spark.executor.cores三样全动了。原因很简单:如果你同时改了三个,作业运行变快了,你根本分不清是哪个改动起了作用;如果你改了三个且作业变慢了,你同样拆解不开。一次只动一个,拿去跑,在UI上明确观察到对应指标数值发生变化,再决定是继续调整还是回滚。

一个常见的调整链路是:先在Executors页发现Shuffle Spill (Disk)过高,回来把spark.sql.shuffle.partitions从默认值200调整为400,再次运行,在Stage页面看到单个Reduce Task的Shuffle Read Size从之前动不动几百MB,下降到300MB以内。如果Job变快,继续观察GC Time和Spill是否同步下降,这一刻才叫闭环。如果调整后Job反而慢了,多半是分区数过大导致调度开销压过了收益,UI上看到的现象就成了Scheduler Delay抬升,这时就需要往回再微调一档。

还有一个我踩过不少次的坑:长时间占用Storage Memory不释放。起因通常是用cache()或persist()缓存了较大的RDD结果,但后续逻辑并没有复用到它,造成Storage页里显示的缓存数据一直占用着内存,挤占了执行内存的空间。这种问题你在UI上看能发现,Redis里叫淘汰,Spark里叫溢写。处理办法其实很简单——用unpersist()主动释放,或者在存储级别上选择MEMORY_AND_DISK_SER,让缓存数据可以序列化落盘而不是必须驻留内存。只要你在Storage页看到缓存块的存在时间远大于作业的实际有效使用周期,就说明资源哪儿漏了。

关于动态资源分配再多说一句:spark.dynamicAllocation.enabled开启后,配合外部Shuffle服务spark.shuffle.service.enabled同时启用,才能让executor在空闲时被安全释放。很多人在YARN模式下会忽略后者的配置,结果动态缩容的Executor把Shuffle中间数据带走了,后续任务读不到数据,UI上表现出来的是大量FetchFailed异常和Stage重试。这个报错的复现过程在Stages页里会反复出现,排查时看到FetchFailed先别急着加内存,检查一下是不是Shuffle服务本身没起。

6. 写在最后:UI不是给领导看的仪表盘,是你自己的排错罗盘

我见过不少团队的Spark监控面板搭得很漂亮,各种Grafana指标大屏,但作业一出问题,最后还得靠Spark UI一页一页翻。不是指标系统没用,而是UI提供的是任务级别的执行真相——哪个Stage卡住、哪类Task重试、哪个Executor被驱逐,这些细节层面做精细调优时,只有原生的UI能给你这么完整的信息链。

我的习惯是把常用的参数调整组合做成自己的一个小手册,比方说:

现象优先检查页面函数首选参数
Executor频繁GCExecutors页GC Time列spark.executor.memory、spark.executor.memoryOverheadFactor
数据倾斜Stage详情页Shuffle Read Size分布spark.sql.adaptive.skewJoin.enabled
Shuffle溢出磁盘Task日志里spill记录spark.sql.shuffle.partitions
调度延迟高Stage页Scheduler Delay列spark.sql.adaptive.coalescePartitions.enabled

在启动一套调优之前,先把这套表格看一遍,结合UI体现出的当前作业特征去选对应的参数,而不是照着网上流传的“最佳实践配置”直接抄。每个集群的网络带宽、磁盘IO能力、YARN队列资源各不相同,能解决我这边数据倾斜的参数在你那边不一定灵,关键是掌握从UI现象反推参数问题的思考方式。

最后分享一个小技巧:重跑同一作业时,别反复刷新Jobs列表看结果,而是盯住Stage页的进度条颜色变化。Stage状态从Pending到Scheduled再到Running,颜色由灰变黄再变绿,如果看到某个Stage长时间停在橙色或者局部Task反复变红,那比等Job整体失败信息更早暴露问题。越早发现越早处理,调优的时间成本能压缩到最小。

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

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

立即咨询