我先说明一个背景:做分布式计算框架优化这件事,很多人第一反应是调参数——把 executor 内存调大、把并行度调高、把 shuffle 分区数改一改。但真正跑过生产任务、扛过集群告警的人都知道,参数只是最后一步,前期的瓶颈定位和策略选择才是决定性的。这篇文章我会从底层原理讲起,结合我在真实集群上处理过的性能问题,给出一套从定位到落地再到验证的完整优化链路,重点覆盖并行度、数据倾斜、Shuffle、内存模型和代码层面容易被忽视的细节,希望能帮你少走几次"参数全调了一遍但任务还是慢"的弯路。
1. 为什么资源翻倍了,作业还是那么慢:优化的第一步是量化
先讲一个我印象很深的案例。有两套集群,配置几乎一样,跑同一个 ETL 任务,A 集群 20 分钟跑完,B 集群要 1 小时。我一开始怀疑是机器性能差异,但对比了 CPU、内存、磁盘 IO 之后发现硬件水平几乎一致,最后查了半天,问题出在 B 集群的文件分区数上——它的上游任务生成了上千个小文件,导致下游任务的 task 数量爆炸,每个 task 的调度开销和元数据读取时间远远超过了实际计算时间。你说这种问题靠调大 executor 内存能解决吗?不能,问题的根源根本不在资源,而在数据分布。
这就是分布式计算框架优化的第一个原则:在决定改任何参数之前,必须先把耗时分布和资源利用率量化出来。很多人拿到一个慢任务,第一件事就是打开配置文档,把能调的参数都调一遍,这样做的结果往往是把一个本来 20 分钟的任务调成了 25 分钟——因为你可能引入了更多的序列化开销、更频繁的 GC,或者让数据倾斜变得更严重。
怎么量化?我的做法是分三步。
1.1 先看时间消耗在哪个阶段
以 Spark 为例,每个作业(Job)会被拆成多个阶段(Stage),阶段之间通过 Shuffle 衔接。打开 Spark UI 的 Stages 页面,你能清楚看到每个阶段的耗时、输入数据量、Shuffle 读写量。如果某个 Stage 的耗时占了整个作业的 80% 以上,那优化重点就应该锁定在这个 Stage;如果所有 Stage 都慢,那大概率是资源不足或者集群本身出现了干扰;如果每个 Stage 的耗时都不长,但任务总数非常多、每个任务的运行时间只有几秒,那问题可能出在 task 调度频率太高或分区粒度过细。
Flink 也是类似的逻辑。你要去看每个算子(Operator)的繁忙度(busy)和背压(backpressure)情况。一个算子的繁忙度持续 100%,说明它已经成了瓶颈;如果频繁出现背压,说明上游算子往下游发送数据的速度超过了下游的处理速度,这种情况常见的处理手段是调整算子并行度、优化算子内部的 IO 操作,或者检查是否有大状态导致 checkpoint 过慢。
1.2 再看资源利用率是否均衡
集群资源是否吃满,不等于集群资源是否被有效利用。我曾经在一个任务里看到 CPU 使用率只有 30%,但节点 CPU 的 iowait 却高达 60%,说明任务长时间在等待磁盘 IO——这种情况即便你把 executor 数量从 10 加到 30,性能也不会提升,因为瓶颈在磁盘。类似地,如果 CPU 使用率很高但内存使用率也很高且频繁 Full GC,那问题在 JVM 堆内存配置或对象生成速度上。如果 CPU、内存、磁盘都不高,但任务还是很慢,那要怀疑是不是网络开销过大,尤其是 Shuffle 数据量过大,导致大量数据在节点间传输。
1.3 对比优化前后基线
做任何优化之前,先记录下本次作业的输入数据量、输出数据量、总耗时、各阶段耗时、GC 次数和时间。这个基线数据非常关键。我见过不少同事改完了参数,凭感觉说"好像变快了",但拿不出任何对比数据,结果上线后被数据一打脸,才发现是集群负载波动造成的错觉。建立基线,用数据说话,这是优化能持续复现的前提。
有了这套量化方法,我们再来看那些真正值得动手的优化点。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 并行度与分区策略:大多数调优问题的根源
分区是分布式计算框架里最基础也最容易被误解的概念。简单来说,分区决定了你的数据被切成多少块、有多少个任务可以并行处理。分区数太少,集群资源用不满,白白浪费;分区数太多,任务调度开销增大,每个 task 处理的数据量过小,IO 和序列化成本反而会压过计算收益。这里有一个基本均衡点:单个任务的处理时间应该在 100ms 到几分钟之间,并且让任务数尽量等于集群可用并行度的 2 到 3 倍。任务数太接近并发度,某个任务晚到就会拖慢整个 Stage;任务数过多,调度器忙不过来,资源就浪费在切换上。
2.1 并行度应该怎么算
很多文档告诉你"并行度等于 CPU 核数",但在分布式场景下,这个说法是非常粗糙的。真正的并行度取决于你当前这一步要处理的数据量和单条数据的计算复杂度。一个比较实用的估算方法是:
并行度 = 期望单个任务处理的数据量 / 预估单条数据大小
举个例子,假设一个 Stage 要处理 200GB 数据,你觉得单任务处理 256MB 到 512MB 比较合适,那并行度就是 400 到 800。为什么要取这个范围?因为数据在 Shuffle 时要经过序列化和网络传输,如果单任务数据量太大,shuffle 阶段和计算阶段的内存压力都会增大;如果太小,又会放大任务调度和序列化的固定开销。实际调优时,我会先按这个公式粗算一个值,再通过一两次测试微调,观察总耗时与资源利用率的变化曲线。
2.2 分区数翻倍不一定更好:小文件问题
分区数多了,还有一个容易被忽视的副作用——小文件问题。如果你的任务最后落到 HDFS 上,每个分区会生成一个输出文件。当分区数达到上千甚至上万,输出目录里就会出现大量几十 KB 的小文件。这些小文件会让下游任务的元数据读取时间大幅增加,也会给 NameNode 造成压力,最终导致"优化了当前任务,拖垮了整个数据链路"。
所以,我对分区策略的建议是:
- 读入阶段:尽量让每个分区对应一个合理的 HDFS 块大小(通常 128MB 或 256MB),不要让一个小文件占一个分区。
- 计算阶段:按照本文 2.1 的估算方法,让分区数略大于集群最大并行度的整数倍。
- 输出阶段:如果业务不需要那么多分区,用 coalesce 或 repartition 把分区数压下来,但要注意,coalesce 会尽量减少 Shuffle,适合在数据量已经收敛的阶段使用;repartition 会触发一次全量 Shuffle,适合在需要重新数据分布时使用。
2.3 动态资源分配:不是银弹,但值得开
很多集群默认关闭了动态资源分配,导致无论任务有多小,都按照最大资源配置来申请。比如你给一个作业配置了 100 个 executor,实际只需要 20 个就能跑完,剩下 80 个空转着,白白浪费集群资源。开启动态资源分配后,调度器会根据任务的实际并发需求动态调整 executor 数量,这对提升集群整体吞吐量帮助很大。
但动态资源分配也有自己的问题。它会导致 executor 频繁启动和销毁,每次启动都要拉取依赖、初始化 JVM,实际上有几十秒的额外开销。所以对于"数据量不大、任务时间短"的作业,我建议关掉动态资源分配,固定分配适量资源;对于长时间运行、数据量波动大的任务,再考虑开启。另一个需要注意的点是,动态资源分配开启后,如果你设置了过大的 executor 内存上限,executor 数量激进增加时可能直接把整个集群资源池打满,影响其他任务,所以一定要配置好每个 executor 的资源上限。
3. 数据倾斜:最经典的分布式性能杀手与根治方案
如果说分布式计算框架优化只能记住一个词,那一定是"数据倾斜"。所谓数据倾斜,是指数据分布极不均匀,某个分区或某个 key 的数据量远大于其他分区,导致少数几个任务承担了绝大部分计算量,其他任务早就干完等着,整个作业的耗时被这几个"钉子户"任务卡死。
数据倾斜的判断方法很简单:在 UI 上查看各 task 的处理数据量,如果出现明显的长尾——比如大部分 task 处理 1MB 数据用 5 秒,某个 task 处理 5GB 数据用 20 分钟——基本就是倾斜了。
3.1 倾斜的三种常见成因
- Key 分布不均:比如日志数据里某个 IP 的访问量占到了 50%,按 IP 聚合时必然倾斜。
- Join 操作中的热点 Key:大表 join 小表时,小表里的某个 key 关联了大量大表数据。
- 分区函数设计不善:HASH 分区时,如果使用自定义分区器,某些取模结果落到同一个分区,也可能造成倾斜。
3.2 对症下药:三种解法对比
下面我列出三种最常用的倾斜处理方案,各有适用场景。
| 方案 | 原理 | 优缺点 | 适用场景 |
|---|---|---|---|
| 加盐(salting)扩容 | 给热点 key 加随机后缀,将其打散到多个分区,再聚合成最终结果 | 简单直接,但需要两阶段聚合,对全局聚合结果需要二次合并 | group by、聚合类算子 |
| 广播小表 | 把数据量小的表广播到每个 executor,避免 Shuffle join | 减少 Shuffle 量,但要求小表足够小,且对广播后的内存占用有压力 | 大表 join 小表 |
| 自定义 Partitioner | 根据 key 的分布特征重新设计分区函数,让热点 key 分散到不同分区 | 可控性最强,但需要了解数据分布,且写代码量稍多 | 对数据分布有先验知识的场景 |
3.3 加盐方案的具体实现思路
以 group by 为例。假定你是按 user_id 聚合并求和金额,其中某个 user_id 是超级会员,数据量占比极高。第一步,在写入时给这个热点 key 拼接一个随机数后缀(比如 0 到 9),这样同一个 user_id 的数据就被切成了 10 份,落在不同分区,第一阶段的聚合就分布到了多个任务上。第二步,在下一阶段,去掉后缀,对同一个 user_id 的多个聚合结果再做一次最终聚合。这个方案的逻辑很简单,但要注意一点:随机后缀的范围不能太大,否则第二阶段合并的数据量会膨胀,一般 10 到 100 就足够了。
3.4 有时候倾斜解决不了,但可以绕过
还有一种思路,叫"消除倾斜依赖"。比如某个 SQL 里做了两张表的 join,如果其中一张表的 key 分布严重不均,你可以先只 join 热点 key 的数据(此时通过过滤 + 加盐解决),非热点 key 的数据走普通 join,最后把结果 union 起来。这样做代码会复杂一些,但避免了把所有数据都套入加盐方案带来的额外开销。
需要提醒的是,数据倾斜不是每次都能根治,很多时候你能做的是把倾斜比例降低,让最慢的任务从 20 分钟降到 3 分钟,但这个任务仍然比其他任务慢。此时不必追求完全均匀,只要整体作业耗时达到可接受的范围,就可以上线了。
4. Shuffle与IO层优化:被低估的瓶颈
很多性能问题,表面看是计算慢,实际上慢在 Shuffle。所谓 Shuffle,就是数据在节点之间重新分布的过程——比如 join、group by、reduceByKey 这些操作都会触发全量数据重新分区,数据要经过"写本地磁盘 -> 网络传输 -> 目标节点读入内存"三个环节。这个过程涉及大量的磁盘 IO 和网络 IO,是整个分布式计算链路中最容易成为瓶颈的环节之一。
4.1 尽量把 Shuffle 从"必经之路"变成"可选项"
优化的第一原则是能不 Shuffle 就不 Shuffle。在编写计算逻辑时,你要优先考虑使用不需要全量重新分区就能完成的算子。
以 Spark 为例,reduceByKey 和 groupByKey 都能实现分组聚合,但前者的性能远好于后者。reduceByKey 会在 map 端先做一次本地合并,把相同 key 的数据先聚合成一个中间结果,再进入 Shuffle 阶段;groupByKey 则会把所有原始数据都传过去,Shuffle 数据量大了不是一点半点。我之前见过一个任务,把 groupByKey 改成 reduceByKey 之后,Shuffle 数据量从 1.2TB 降到了 200GB,耗时从 40 分钟降到了 12 分钟。而且代码改动只有一行,这种收益几乎是白捡的,属于"必须做"的优化。
在 Flink 里,也有类似的取舍。如果下游算子只需要按 key 做局部聚合,可以使用 KeyedProcessFunction 配合本地聚合逻辑,而不一定要把数据显示式地发到下游再做全局聚合。另外,合理使用 uid() 可以稳定 operator 的并发状态,从而减少状态恢复和 checkpoint 带来的额外 IO。
4.2 压缩与序列化:让 Shuffle 数据"变小"
Shuffle 数据量越大,网络和磁盘压力越大。降低数据量的两个核心手段是压缩和序列化。
- 压缩:Spark 默认的 Shuffle 压缩确实做了,但压缩算法值得选择。实测来看,
LZ4在压缩速度和压缩比之间平衡最好,适合大多数场景;ZSTD压缩比更高、但压缩时会消耗更多 CPU;Snappy与 LZ4 类似,但不同版本略有差异,我一般优先 LZ4,追求极致压缩比时用 ZSTD。 - 序列化:Spark 默认使用 Java 序列化,但它的性能低效、序列化后的字节数也很膨胀。改用 Kryo 序列化之后,序列化后的数据体积通常能缩减到原来的 1/3 到 1/5,网络传输和内存占用都会明显降低。不过使用 Kryo 时要提前注册自定义类,否则它会采用全类名字符串写入,你没法省这个空间。
4.3 管理好小文件:Shuffle 之前的隐藏杀手
前面我提过小文件的问题是输出的场景,其实在 Shuffle 过程中,小文件同样会拖垮性能。当上游算子并发度很高时,每个 map task 会为下游每个 reduce task 产生一个中间文件,这些中间文件数量极其庞大,光文件元数据的管理和磁盘寻道就占用了大量时间。
应对方案通常是开启 Shuffle 文件的合并机制。在 Spark 中,开启并配置 spark.shuffle.service.enabled 可以复用外部 Shuffle 服务;在 Flink 中,则可以调整 taskmanager.network.memory.buffer-per-channel 等参数,减少网络缓冲区的碎片化。更深层的手段是优化上游并发度:不要让上游任务数远大于实际需要的输出分区数,避免中间文件数量爆炸。
4.4 Join 策略对 Shuffle 量的决定性影响
Join 操作在分布式计算中非常考验优化水平。最朴素的实现是 SortMergeJoin:两张表都按 key 排序,然后合并,这个过程必然产生大量 Shuffle。如果你的场景里有一张小表(比如几百 MB 以内),就完全可以用 BroadcastJoin 替代——小表被复制到每个 executor 上,大表直接在本地跑 lookup,Shuffle 量直接变成 0。这个优化在 Spark 里可以通过 spark.sql.autoBroadcastJoinThreshold 来配置,在 Flink 里也有类似的广播状态机制。
但要注意,广播小表不是越大越好。当广播表超过 2GB 时,广播的副本会对每个 executor 的内存造成明显压力,反而可能引发 OOM。所以实际使用中,要根据 executor 内存大小来调节广播阈值,别盲从默认值。
5. JVM与内存调优:GC配置与执行内存模型之间的平衡
分布式计算框架(特别是 Spark、Flink)都跑在 JVM 上,JVM 的堆内存管理、垃圾回收策略和计算框架自身的执行内存模型之间有非常紧密的耦合。很多任务不是算得慢,而是被 GC 拖慢了,这个现象在数据量增大之后尤其明显。
5.1 先把框架的执行内存模型搞清楚
以 Spark 为例,一个 executor 的堆内存被划分为三块:Reserved Memory(系统预留)、User Memory(用户代码存储) 和 Spark Memory(执行与存储共享)。Spark Memory 是一个可动态调整的区域,默认情况下执行内存(用于 Shuffle、Join 排序)和存储内存(用于缓存 RDD、广播变量)共用一块空间,谁需要谁就能抢占。
这个设计看起来灵活,但也埋了坑。如果两个同时运行的任务都抢占执行内存,其中某个任务的缓存数据被强制溢写磁盘,那么下一次再用到这块数据时就要重新读磁盘——性能直接掉一个档次。因此,如果你的任务以迭代计算或缓存为主,我建议手动调整 spark.memory.storageFraction,给存储内存留出稳定的比例;如果你的任务以 Shuffle 和 Join 为主,则保持执行内存充足,避免频繁的缓存淘汰。
5.2 GC 参数配置:不是越大越好,也不是越少越好
GC 是 JVM 内存优化的核心。最常见的错误是:一看到 GC 时间长,就把堆内存调大。堆内存变大之后,对象分配更宽松,GC 频率确实会降低,但每次 Full GC 的时间会更长。而且堆内存一旦超过 32GB,JVM 会启用压缩指针失效的路径,反而增加对象引用占用的空间,这在分布式节点上经常出现"内存变大反而变慢"的怪象。
我的实测经验是:
- 优先用 G1GC,它对大堆、多核场景的暂停时间控制得比 CMS 好。如果任务对延迟不敏感、追求吞吐量,也可以用 ParallelGC。
- 配置
-XX:MaxGCHPauseMillis来控制 GC 暂停目标,比如设为 200ms,让 G1 自动调整年轻代大小和回收策略。 - 打开
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC,把 GC 日志落到磁盘,方便优后复盘。 - 每次只改一个参数,观察 3 到 5 次任务后再决定下一步,不要一次调五个参数,出了问题都不知道是谁的锅。
5.3 对象生成与内存泄漏:一个常被忽略的问题
框架代码里频繁生成大对象,是 GC 频繁的直接原因。比如在 Spark 的 map 函数里,如果对每条记录都 new 一个大容器对象,那么这些对象会大量进入老年代,最终触发频繁的 Full GC。这种问题的解法有两个方向:一是尽量复用对象,二是使用统一的数据模型(比如 DataFrame/Dataset),让框架自己管理序列化和内存布局,而不是在 RDD 的 map 里手动拼 Java 对象。
Flink 场景下还要留意状态后端的选择。如果你的任务有大规模状态,建议用 RocksDB 状态后端,虽然数据存储在磁盘上,但不会因为状态过大拖垮堆内存;而内存型状态后端(比如 HashMap)的读写快,但状态过大会直接 OOM。实际选择要在速度和容量之间做平衡,没有绝对的好与坏。
5.4 内存溢出不是只有 OOM 这一种表现
很多人只关注 OutOfMemoryError,但在分布式环境中,更常见的现象是任务没有报错但性能断崖式下跌。比如执行内存溢出时,Spark 会把数据溢写到磁盘,日志里会频繁出现"spilling"字样。看到 spilling,你就该意识到:这不是磁盘坏,是内存不够用。此时优先优化内存分配比例、降低单 task 数据量,而不是盲目加内存。
6. 代码级优化与执行计划:比参数更值得打磨的细节
参数调优到了后期,边际收益会越来越低。真正能让性能再上一个台阶的,是代码层面的优化和对执行计划的深入理解。这一节我讲几个实战中收益很高的点。
6.1 算子的选择:每多一次复杂的算子,就可能多一次完整的计算和 IO
很多人写数据处理逻辑时,喜欢用 map + filter + flatMap + groupBy + sort 一连串算子堆叠。但每个算子都会产生独立的计算阶段(在某些框架下还会引入额外序列化和网络传输),所以你要关注算子链条的效率。
拿 Spark SQL 来说,尽量用 DataFrame/Dataset API,而不是 RDD。因为 DataFrame 有 Catalyst 优化器,能自动做谓词下推、列裁剪、常量折叠等优化。比如你只需要三列数据,但上游全表扫描了 100 列,写了 RDD 代码,框架没法自动帮你裁剪;写了 DataFrame SQL,优化器会直接只读取那三列,IO 和网络开销天差地别。
6.2 谓词下推和列裁剪:很多时候是"免费的午餐"
这两个优化点不需要改任何业务逻辑,只需要用对 API。
- 谓词下推:把
where条件尽量下推到数据源端,减少需要跨网络传输的数据量。在写 SQL 时,要确保过滤条件写在子查询内部,而不是在外面再包一层 where。写 Spark 代码时,filter之后再进行 join 和 groupBy,效果远好于 join 之后再 filter。 - 列裁剪:只 select 必要的列,不要写
select *。在大数据场景下,每多一列,Shuffle、序列化、存储的开销都是线性增加的,少一列可能就少几 GB 的数据传输。
6.3 数据库和外部系统的访问:连接复用是第一优先级
在分布式计算任务里,最常见的"隐形杀手"是每个 task 都去连接一次外部数据库。一个任务有上千个 task 并发运行,每个 task 都新建一次数据库连接,数据库瞬间被打满,连接超时加上重试,直接让任务卡死。解决办法是:
- 使用连接池(如 HikariCP),让多个 task 复用连接。
- 批量写入代替逐条写入,把结果攒在内存里,一次性提交。
- 如果是读取外部数据,考虑用分布式缓存把数据提前加载到内存,避免每次计算都穿透到数据库。
6.4 熟悉执行计划,让优化有据可依
真正有效的优化不是靠猜,而是靠看执行计划。无论是 Spark 的 explain 还是 Flink 的执行计划 JSON,都会把你算子的连接顺序、Shuffle 策略、分区方式、数据扫描范围展示得一清二楚。我在每次调优前都会先做这一步:查看执行计划,问自己三个问题——有没有不必要的全表扫描?join 顺序是否合理?Shuffle 是否可以被广播替代? 90% 的调优点都可以在回答这三个问题的过程中找出来。
6.5 一个值得留意的现代化执行引擎特性
现在的 Spark 3.x 默认开启了 Adaptive Query Execution(AQE),它可以在运行时动态调整分区数、自动处理数据倾斜、优化 join 策略。这是一个"买了就会用"的功能,但前提是你得了解它的工作机制。我之前遇到一个任务,跑了 2 小时,开启 AQE 之后自动把 reduce 分区数从默认 200 调整到了 50,耗时直接降到了 40 分钟。这类运行时自适应能力,正在成为分布式执行引擎的标准配置,值得你花时间研究。
7. 监控、验证与一次完整调优流程参考
优化的最后一道工序是验证和回归。不做验证就说是"优化",是不负责任的。我的标准流程是这样的。
7.1 搭建基础监控
在集群层面,要监控节点的 CPU、内存、磁盘 IO、网络 IO 和 GC 频率;在作业层面,要记录作业的开始时间、结束时间、各阶段耗时、Shuffle 读写量、任务数量和数据倾斜程度。工具方面,Grafana + Prometheus 基本是标配,Spark 和 Flink 也都有自己的 Metrics 系统,可以对接输出。
7.2 定位问题:从"慢"到"为什么慢"
拿到一个慢任务,我通常按这个顺序排查:
- 打开执行计划,确认是否有全表扫描、糟糕的 join 顺序、过多的 Shuffle——解决执行计划层面的问题。
- 打开 UI 的各 Stage / 算子指标,看数据倾斜和资源利用率。
- 看 GC 日志,判断是 GC 暂停导致还是执行内存不足导致 spilling。
- 看外部系统(数据库、文件系统)的负载,排除连接数打满、带宽受限的干扰。
7.3 实施调优:一次只改一个变量
真正的调优过程,是围绕一个核心瓶颈逐步迭代的,而不是把所有参数一次性全改掉。我强烈建议你建立一张"调优记录表",每改一个参数就记录一次任务耗时和资源使用率的变化。改完一个参数后,至少要验证 2 到 3 次任务,排除集群负载波动的影响,确认稳定后再改下一个。
7.4 一次完整的调优流程示例
假设我有一个 Spark 任务做 session 级别的聚合,初始耗时 58 分钟。我按上面的顺序操作:
- 查看执行计划,发现 join 触发了全量 Shuffle,而其中一张表只有 20MB。我把
autoBroadcastJoinThreshold调大到 50MB,广播替代 Shuffle,耗时降到 42 分钟。 - 看 UI,发现某个 Stage 有数据倾斜,热点 key 明显。我通过加盐方案做两阶段聚合,耗时降到 25 分钟。
- 查看 GC 日志,发现 Full GC 频繁,原因是 executor 堆内对象过多。我把序列化方式改为 Kryo,并调低
storageFraction,让执行内存更充足,耗时降到 18 分钟。 - 打开 AQE,让它自动优化 reduce 分区数并处理倾斜,耗时进一步降到 12 分钟。
整个过程每一步都经过测试验证,最终效果是 58 分钟降到 12 分钟,但注意,我的每一步改动都和瓶颈的因果关系对得上,不是在盲目试参数。
我在实际项目中还发现,性能优化永远不要只做一次。数据量、数据分布、集群规模都会随着时间变化,上一次的优化方案在新数据形态下可能失效,所以建议把性能测试纳入到数据模型或作业开发流程中,定期回跑。另外,调整完参数后记得关注下集群其他作业的表现,因为有些参数(比如广播阈值、executor 资源上限)是集群级别的,可能会对周围任务产生“意外影响”。
最后分享一个小技巧:不要忽视小数据量测试的价值。在正式调优大数据量任务前,先拿 1/100 的数据量跑一遍基准,用同样的调优思路验证执行计划是否合理。小数据量下执行计划清晰、调试成本低,很多问题能提前暴露,远比直接在大数据量下反复试错来得高效。
