1. 一次批处理任务从10小时到45分钟的实战复盘
先讲一个真实的场景。几个月前,我接手了一套日志分析平台,每天要处理的数据量在TB级别,涵盖用户行为日志、业务埋点、服务端上报数据,总记录数大概在几十亿条。这套系统用的是Hadoop生态,调度链路是Hive SQL + Spark,跑的是第二天凌晨的T+1批处理任务。
我刚接手的时候,核心任务跑完一次需要10个小时左右,调度窗口是从凌晨1点开始,正常情况下早上7点能出数据,遇到数据量波动或者集群资源被挤占,经常会拖到中午甚至下午才跑完,直接影响业务方看数据。整个处理链路涉及十几个任务,从数据清洗、维度标准化、分流,到核心指标计算、多维度聚合并入结果表,最重的几个任务单次Shuffle的数据量超过几百GB。
我做的第一件事不是调参数,而是把整条链路梳理了一遍,一个个任务单独跑一遍,记录每个任务的运行时间、资源消耗、Shuffle数据量和Executor的GC时间。这一步非常关键,因为优化没有数据支撑就是在瞎猜。
结果很快就暴露了问题:某个上游任务的Map端输出比预期大了将近4倍,原因是代码里有几个字段做了大量的字符串拼接,导致序列化后的数据膨胀;另一个核心聚合任务,十几个Executor在跑,但实际有效并行度很低,大部分时间都耗在了一个倾斜的Key上;还有一个任务频繁触发Full GC,Spark UI里能看到Executor的GC耗时占比超过15%。
经过三轮优化——第一轮修数据倾斜,第二轮改存储格式和压缩算法,第三轮调并行度和内存配置——最终核心链路的总执行时间稳定在45分钟以内,比优化前提升了92%左右,集群资源占用还降了大概30%。
这篇文章就把这套优化的思路、工具、踩坑过程拆开讲清楚。整个过程并不复杂,但需要有一套系统的方法论,而不是遇到性能问题就无脑加大并行度或者堆内存。文中涉及的工具和版本是Spark 3.2.1、Hive 3.1.3、YARN 2.9.2,数据存储是HDFS加Hive表。你如果用的版本稍有差异,原理完全一致,参数位置可能略有不同,不影响整体思路。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 大规模数据处理的架构选型:为什么我选了Spark + Hive这套组合
2.1 选型考虑的核心变量
面对大规模数据处理,很多人第一反应是“用Spark就行了”“上Flink吧”,但实际上选型要回答的问题远不止框架本身。
我这次选型的核心约束有三条:第一,团队对Java和Scala比较熟,Python也会一些,但对深度底层调优的能力有限;第二,数据源极其多样化,包括MySQL业务库的直接抽取、日志文件、第三方API回传数据;第三,离线场景占大头,但有一个实时看板的需求,对延迟要求是分钟级。
实际上我最终采用的是Hive管理元数据和表结构,Spark做核心计算引擎,YARN做资源调度的组合,流式部分单独用Flink处理少量实时链路。这套组合放在今天不算新潮,但在稳定性和团队上手的性价比上非常能打。
为什么不用纯Hive跑?Hive底层还是MapReduce,单个任务的启动开销大,中间结果大量落盘,TB级数据规模的跑批效率确实不够看。为什么不用纯Spark替代Hive?因为Hive的元数据管理(表结构、分区、权限)经过这么多年沉淀非常成熟,Spark SQL直接复用Hive Metastore的表定义,开发效率极高,团队里的分析师写SQL跑数不需要关心底层引擎是什么。
2.2 计算引擎选型的进一步拆解
从计算模型的角度来看:
| 框架 | 计算模型 | 适用场景 | 延迟量级 | 选型结论 |
|---|---|---|---|---|
| MapReduce | 批处理 | 超大离线任务 | 分钟~小时 | 不选择,性能瓶颈明显 |
| Spark | 微批/内存迭代 | 离线批处理、交互式查询 | 秒~分钟 | 主选 |
| Flink | 流式/事件驱动 | 实时流处理、毫秒级延迟 | 毫秒~秒 | 仅用于实时链路 |
对一个以T+1离线跑批为核心、数据规模在TB级、技术栈以Java/Scala为主的团队来说,Spark的RDD/DataFrame API覆盖了80%的场景,而且Spark SQL天然兼容Hive语法,迁移成本极低。
2.3 中间结果存储选型:Parquet是不二之选
很多人在大数据处理中忽略的一个关键环节是中间结果表的存储格式。你从源表抽出来的数据,清洗后要不要落地?落地用什么格式?这直接决定了后续任务的计算效率。
我这次统一选择了Parquet存储格式 + Snappy压缩。原因很简单:Parquet是列式存储,查询时只需要读取涉及的列,IO压力大幅降低;同时Parquet天然支持向量化读取,Spark 3.x对Parquet有专门的优化,读数据时能跳过和谓词下推都做得很好。
对比一下各格式的实测效果,我拿一份大约200GB的日志表做过基准测试:
| 存储格式 | 压缩比 | 读取时间 | 查询时IO量 | 使用场景 |
|---|---|---|---|---|
| TextFile + 不压缩 | 1x | 最长 | 全量 | 原始日志落盘 |
| ORC + Zlib | 0.25x | 较短 | 较少 | 适合Hive重度场景 |
| Parquet + Snappy | 0.3x | 最短 | 最少 | 适合Spark计算场景 |
Zlib压缩比更高,但解压时的CPU开销也更大。在Spark场景下,Snappy的解压速度极快,整体运行时间反而更优。ORC在Hive中的表现也很好,但Spark对Parquet的原生支持更成熟,向量化读取和谓词下推的优化更充分。
这里有一个容易忽略的点:即使同一个任务里的临时表,也要用Parquet格式落地,不要直接对源数据反复读取。我把任务链中的临时表全改成Parquet之后,整个链路的执行时间直接缩短了约20%,没有任何代码逻辑的改动。
3. 任务调度与数据分区设计:规模化处理的第一步
3.1 调度链路:从“大而全单任务”拆成“小而美多阶段”
大规模数据处理的第一个核心,不是搭建多牛的引擎,而是设计好执行的骨架。
这次我接手时,整条链路是四个大任务,每个任务内部用SQL搞定了几乎所有逻辑。问题在于:某个上游任务如果失败了,整个链路全挂,重跑成本极高;而且每个任务内部计算逻辑太复杂,Spark SQL的执行计划优化空间受限,数据倾斜更难定位。
我重构后做了一件很朴素的事:把链路上的每个任务按业务步骤拆成独立阶段。每个阶段只做一件事——清洗、标准化、关联、聚合——并落到对应的Parquet中间表。这样做有几个好处:失败后只需从失败阶段重跑,不用全链路重来;每个阶段都可以单独调优参数;数据血缘更清晰,排查问题时能直接定位到具体环节。
3.2 分区策略:按时间 + 按业务维度分
对于天天增长的日志类数据,分区设计直接对应任务的执行效率。
我采用的是两级分区:按天分区 dt + 按业务线分区 biz_line。这样做的好处是,当某个业务线的数据异常需要回溯重算时,不需要扫描全量数据,直接指定 dt 和 biz_line 就能快速定位到目标数据。数据量特别大的任务分区维度甚至可以加第三级,比如按小时分区。
这里要提醒一个坑:分区不是越细越好。过多分区会导致HDFS上大量小文件,NameNode压力巨大,Spark读取时任务数暴涨,调度开销大于计算收益。我在实践中发现,单分区数据量在1GB到2GB之间是比较合理的范围。如果写入数据太少,可以通过合并小文件或调整分区粒度来平衡。
3.3 数据重分区和分桶:减少Shuffle的隐性技巧
一个很常见但容易被忽略的操作是:在任务中合适的位置主动做一次Repartition或Coalesce。
举个例子:某个上游任务清洗后输出200个分区文件,但下游任务只需要按用户维度聚合,如果直接跑聚合,就会触发200个Map任务到下游的Shuffle。如果在写入中间表时,按下游任务的聚合键做一次Bucket化(分桶),就能让数据在物理上按用户ID分桶,下游聚合时可以直接桶对齐,省去一次全量Shuffle。
典型的做法:
sql复制-- 写入临时表时按用户ID分桶
INSERT OVERWRITE TABLE dwd_user_log_bucket
PARTITION (dt = '2024-06-01')
SELECT user_id, event_name, event_time
FROM dwd_user_log
WHERE dt = '2024-06-01'
DISTRIBUTE BY user_id;
这个 DISTRIBUTE BY 的语义和Spark里的 repartition 是一致的,它会让数据在写入时按照 user_id 的哈希值分布到不同的桶里。下游再按 user_id 做 JOIN 或 GROUP BY 时,Spark 检测到双方都是分桶表,就能跳过Shuffle,直接本地Join。
这个优化在很多任务里能省下30%到50%的执行时间,但前提是分桶字段要和后续聚合/关联字段保持一致。分桶键一旦和下游的计算键对不上,这个优化就不生效,甚至因为多了一次写入和哈希,反而更慢。
3.4 依赖管理:调度系统里不要省事
任务拆分好之后,依赖关系如果不用DAG(有向无环图)来管理,靠人的记忆去维护是肯定会出事的。
我用的调度工具是Apache Airflow,每天任务分三层:ODS层(原始数据同步)、DWD层(清洗明细)、DWS/ADS层(聚合应用)。每层之间通过明确的依赖关系串联,上层任务必须等下层任务成功才能触发。
这里有一个经验:不要依赖任务内部的“等待重试”逻辑去容忍上游延迟,那是给运维挖坑。应该在调度层面配置好重试次数和重试间隔,并把超时时间设置成比任务正常耗时多20%左右,一旦超时立即告警,方便快速介入处理。重试时间太短会导致上一轮的临时文件和锁还没释放,下一轮就跑起来,引发数据错乱。
4. 性能优化的五个实战切入点:并行度、内存、小文件、网络与数据倾斜
4.1 并行度设置:用公式替代拍脑袋
很多人调Spark性能,上来就把 spark.sql.shuffle.partitions 改成2000、5000,任务还是慢,甚至更慢。原因在于并行度必须和集群可用资源匹配。
我这里的经验公式是:
[
\text{Shuffle分区数} = \frac{\text{Shuffle阶段数据总量}}{\text{目标分区大小}(约128\text{MB}~256\text{MB})}
]
假设一次Shuffle的总数据量是300GB,目标分区大小是256MB,分区数大约是1200个。如果集群给这个任务的可用Executor总核数是100,那并行的核心数就是100,1200个分区就意味着每个核大概要处理12个分区,这个比例是合理的。
过高的分区数会导致每个任务的启动和调度开销淹没实际计算,过低则会导致单个任务处理数据量过大,出现资源不均和GC压力。我通常的做法是在Spark SQL里先估出Shuffle数据量,再反推分区数,最后用监控确认Stage耗时是否均匀。
这里给一个实际可用的参数起点(按500GB Shuffle数据量、100核Executor估算):
bash复制spark.sql.shuffle.partitions=600
spark.default.parallelism=600
spark.sql.adaptive.enabled=true
spark.sql.adaptive.coalescePartitions.enabled=true
spark.sql.adaptive.coalescePartitions.minPartitionNum=50
spark.sql.adaptive.advisoryPartitionSizeInBytes=256MB
从Spark 3.0开始,Adaptive Query Execution(AQE)是必须开的功能,它能根据Shuffle阶段的实际数据量动态调整分区数,合并过小分区或拆解过大分区。开启后,很多手动调分区数的操作都可以省掉,Spark会自己找平衡点。
4.2 内存配置:命中和GC是两个关键
Executor内存配置是另一个高频优化点。这里要给一个非常反直觉的结论:把Executor内存无限加大不一定是好事,反而可能触发更长的Full GC停顿。
Spark Executor内存分三块:Reserved Memory、User Memory(存储/执行共享的Spark Memory)和Overhead。默认配置下,spark.memory.fraction=0.6,即Executor堆内60%的内存留给执行和存储,剩余40%留给用户代码和数据结构。
我的经验做法是:
- 每个Executor分配4到8GB内存,超过8GB收益递减明显,因为JVM的GC对大堆管理并不友好;
- 设置
spark.executor.memoryOverhead为Executor内存的10%到20%,给Shuffle的堆外内存留足空间,否则容易出现OffHeapError; - 在Spark UI里盯两个指标:Shuffle Spill (Memory) 和 JVM GC Time。如果Spill频繁说明执行内存不够,如果GC Time占比超过10%,说明堆内数据结构过大或对象分配过密。
有一个我踩过的坑:为了追求缓存效果,我把多张大表都放在了内存里用 cache() 缓存,结果不仅没有加速,反而导致执行内存被大量挤占,后续Shuffle疯狂Spill到磁盘。后来改成 只缓存小维表,大表直接读文件不加缓存,GC时间立刻降了下来,任务整体快了30%。
4.3 小文件问题:处理吞吐量的隐形杀手
大规模数据处理稳定之后,最容易拖慢任务的其实是小文件。
每天几亿条日志按小时写入Hive表,如果没有合理控制文件大小,几个小时就会产生几万个小文件,单个几KB到几十KB不等。Spark读取时,每个小文件就会启一个Task,几万个小文件就是几万次调度,还没开始算数据,调度开销已经顶满。
解决方案是在任务写入时加一层文件合并逻辑。我在DWD层清理完数据后,每个分区的输出文件数量控制在100到200个左右,每个文件大约在128MB到256MB之间。具体方法是写入前用 REPARTITION 控制最终分区数,或者在写入后用合并工具把小于某个阈值(比如32MB)的小文件合并成大文件。
另外一个技巧是:在主要任务链路的末端,统一做一次 INSERT OVERWRITE ... SELECT ... 的数据重排,既清洗了数据,又把文件控制在目标大小范围内。这个操作看起来多跑了一次,但能避免后续所有读取任务因小文件多而拖慢速度,总体收益很大。
4.4 网络IO和数据本地性:一个往往被忽视的维度
Spark任务在计算时,如果数据在本地节点上,就能走本地读;如果数据在其他节点上,就必须通过网络拉取。网络带宽在大规模数据处理中往往是最容易成为瓶颈的资源之一。
Spark默认 spark.locality.wait=3s,意思是如果本地没有空闲资源,最多等3秒,然后就会去别的节点找可用Executor。对于高并发业务,集群繁忙时这种“等待本地”的机制会浪费不少时间。我的调法是:如果任务是纯计算型的,可以适当缩短等待时间,避免因为等待本地数据而阻塞;如果任务是IO密集型的,则应该尽量让计算靠近数据。
实际运维中,最有效的本地性手段是关掉YARN的动态资源分配,或者至少限制动态分配的最大Executor数。否则在高并发执行时,Spark可能会频繁启停Executor,导致每个Executor上没缓存任何数据,本地性优势完全丧失,所有Shuffle数据都走网络传输。
4.5 数据倾斜:最大的性能杀手,没有之一
数据倾斜是绝大多数Spark任务性能瓶颈的根源,也是最花费排查精力的问题。所谓倾斜,就是在Shuffle阶段某个Key的数据量远大于其他Key,导致分配到该Key的Task处理时间远超其他Task,整体任务被这个“木桶最短板”拖住。
举一个实际例子:在某个按渠道统计用户留存的任务里,其中一个渠道的流量占了全渠道的70%以上,其他渠道各占几个百分点。任务在Reduce阶段,那个大渠道所在的Reducer要处理比其他Reducer大50倍的数据量,耗时就拖了一倍多。
具体的排查过程我在下一章详细拆。这里先给出几个常用解决思路:
- 加盐 + 二次聚合。把倾斜的Key加随机前缀,让数据分散到多个Reducer先做局部聚合,再去掉前缀做全局聚合。适合
GROUP BY场景。 - 广播小表。如果大表倾斜的Key对应小表数据量不大,可以把小表用
BroadcastHashJoin广播到所有Executor,避免Shuffle。 - 拆分倾斜Key和非倾斜Key。倾斜Key的数据单独拿出来走一条路径,非倾斜Key走正常路径,最后Union结果。
5. 数据倾斜排查实录:一场完整的性能问题诊断链路
5.1 现象:任务卡在特定Stage,大量Task秒完
有一次,某个核心聚合任务运行时间从正常的30分钟飙升到3个小时。从YARN的ResourceManager页面看,任务并没有失败,只是大量Executor处于空闲状态。进入Spark UI查看,问题很明显:Stage 3有1200个Task,其中约1150个Task几十秒就跑完了,但剩余50个Task卡了将近2个小时,且数据Shuffle Read的量差异极大——最大Task读取了约15GB数据,而中位数Task读取只有约230MB。
这就是非常典型的数据倾斜特征:不是任务失败,而是个别Task的执行时间远高于中位数,Shuffle读数呈现长尾分布。
5.2 定位:从执行计划到具体Key的逐步排查
第一步,在Spark UI里定位Stage 3的逻辑。点击该Stage看SQL执行计划,发现是在一次 GROUP BY user_id 的算子里,按user_id分桶产生了倾斜。
第二步,确认倾斜Key。一种快速方法是从源表中抽样统计 user_id 对应的行数分布:
sql复制SELECT user_id, COUNT(*) AS cnt
FROM dwd_user_log
WHERE dt = '2024-06-01'
GROUP BY user_id
ORDER BY cnt DESC
LIMIT 10;
运行结果让我很意外:排名第一的user_id有近6000万条记录,排名第二只有3万条。某个异常ID占当日总日志量的近30%。
第三步,排查这个Key的来源。查了业务系统日志,发现这个user_id是一个内部测试账号,因为一个埋点bug,每次页面刷新都会上报大量重复日志。这是数据质量问题,不是框架问题,所以最优解法是从源头清洗。
5.3 修复:数据清洗 + 加盐二次聚合双管齐下
对于这种由脏数据引起的倾斜,光靠临时加盐解决不了根本问题,因为脏数据还在源源不断进来。我做了两件事:
第一,在ODS层同步数据时就过滤掉了明显异常的埋点日志,包括测试账号、重复上报、字段缺失的记录。这直接去掉了最大的倾斜源。
第二,对于无法完全避免的数据倾斜(比如热点用户的正常行为日志),在DWD层聚合时采用加盐二次聚合方案。具体做法是:给 user_id 加上一个0到N-1的随机后缀,先做一轮局部聚合,再去掉后缀做全局聚合:
sql复制-- 第一层聚合:加盐分散
SELECT CONCAT(user_id, '_', FLOOR(RAND() * 100)) AS salted_user_id,
event_name,
COUNT(*) AS cnt
FROM dwd_user_log
WHERE dt = '2024-06-01'
GROUP BY CONCAT(user_id, '_', FLOOR(RAND() * 100)), event_name;
-- 第二层聚合:去掉盐值
SELECT user_id,
event_name,
SUM(cnt) AS cnt
FROM (
SELECT SPLIT(salted_user_id, '_')[0] AS user_id, event_name, cnt
FROM ...
)
GROUP BY user_id, event_name;
加了第一层盐之后,原本6000万条的热点记录被分散到了100个分组里,每个分组只有60万条,和普通用户的数据量级一致,Shuffle从极端倾斜变成了均匀分布。
修复后,任务总耗时从3小时降到22分钟。
5.4 排查方法论:不要一上来就优化,先看现象
这次排查给我的最大教训是:性能问题排查必须按“现象 → 定位 → 验证 → 修复 → 回归”的顺序走,不要跳步。
现象观察要具体:是哪个Stage慢?是Map端慢还是Reduce端慢?是GC问题还是数据倾斜?是网络问题还是CPU问题?先花时间看清楚,比盲目调参数重要一百倍。
定位要精确:用Spark UI的SQL Tab看执行计划,用Stage页看每个Task的耗时和Shuffle数据量分布,用YARN日志看Executor的资源使用情况。倾斜的特征非常明显:长尾分布 + 大量Task秒完 + 个别Task卡死。
验证要有依据:任何调优都要有前后对比数据。我每次调完都会记录指标——执行时间、Shuffle量、GC占比、资源使用率——这样才能判断优化是否真的有效,也能防止调优引入新的问题。
6. 极致性能优化的进阶操作:AQE、动态合并与缓存策略
6.1 把Adaptive Query Execution放到最高优先级
Spark 3.x时代,开了AQE等于免费拿了一部分性能优化。AQE会在每个Shuffle阶段结束后,根据实际产生的分区数据量和运行情况,动态调整后续阶段的执行计划。
我将AQE的几个关键参数设为:
bash复制spark.sql.adaptive.enabled=true
spark.sql.adaptive.coalescePartitions.enabled=true
spark.sql.adaptive.coalescePartitions.minPartitionNum=20
spark.sql.adaptive.advisoryPartitionSizeInBytes=256MB
spark.sql.adaptive.skewJoin.enabled=true
spark.sql.adaptive.skewJoin.skewedPartitionFactor=5
spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes=256MB
其中 skewJoin.enabled 是一个容易被人忽略但非常实用的功能。它会在Join过程中自动检测数据倾斜的Partition,并把倾斜的Partition拆分成多个子分区,缓解长尾Task。我在开启这个参数后,很多原本需要手动加盐处理的Join场景,都不需要写特殊代码了,直接生效。
但注意:AQE只能应对Join操作中的倾斜,对于 GROUP BY 聚合产生的倾斜,AQE目前没有内建处理,还是需要人肉加盐或者看业务逻辑是否可以把热点Key拆出去。
6.2 动态合并分区:让小任务自动变“胖”一点
配合AQE还有一个参数值得专门提一下:spark.sql.adaptive.advisoryPartitionSizeInBytes。这个参数的作用是告诉Spark,Shuffle分区多大算“合适”。
我实测下来的体感是:当分区大小控制在128MB到256MB时,任务的调度开销和单Task处理耗时能达到比较好的平衡。分区太小,Task数过多,调度和序列化开销大;分区太大,单Task数据量过大,GC压力大且容易碰到数据倾斜。
如果发现任务中Shuffle后的分区数非常多(比如几万个分区),但每个分区只有几十KB数据,就是典型的“碎片化”。此时可以把 advisoryPartitionSizeInBytes 调到256MB以上,或者提高 minPartitionNum,让AQE自动把过小的分区合并成大分区。这个调整能显著减少Executor的空转时间。
6.3 缓存策略:不是所有数据都值得Cache
大数据处理中,缓存是一个非常实用但容易被滥用的工具。我见过很多团队把几十GB的大表 cache() 在内存里,结果资源不够频繁驱逐,浪费了大量内存,任务反而变慢。
我的缓存原则很简单:
- 小维表(几MB到几百MB)值得缓存,维度表被反复Join时,缓存可以大幅减少重复扫描;
- 中间大表不要缓存,直接落Parquet文件,下次需要读取时就靠Spark的谓词下推去处理;
- 如果内存不够,用
MEMORY_AND_DISK级别,这样不会因为缓存而影响执行内存,只是磁盘IO可能多一些。
具体写法:
python复制# 用小维表构建广播变量
dim_df = spark.table("dim_channel").collect()
broadcast_dim = spark.sparkContext.broadcast(dict(dim_df))
然后在下游处理时,直接引用 broadcast_dim 这个Python字典,不需要反复查表,这个优化在维度关联类的任务里效果非常明显。
6.4 资源竞争:队列配额和动态分配的坑
还有一个优化容易被人忽略,就是集群资源的竞争和配额。在共享集群里,即使你的代码写得再好,如果资源配额被别人抢走,任务依然会慢。
我的做法是:把核心链路任务和实验/探索型任务放在不同的YARN队列里,给核心链路分配固定的资源配额,确保跑批任务不被其他任务挤占。同时关掉动态资源分配(spark.dynamicAllocation.enabled=false),避免任务运行过程中Executor被反复启停,影响缓存和本地性。
如果你所在的团队用的是K8s调度,逻辑也类似:给核心任务设置独立的Namespace和资源配额,避免资源争抢。
7. 压箱底的几条经验判断
7.1 优化顺序:先改架构,再调参数
有一种很常见的错误是:一上来就调Spark参数,把 executor-memory 调大、shuffle.partitions 调高,然后跑一遍发现没效果,再去排查代码逻辑。
我的建议是先做三件事:梳理数据集的文件大小和分区合理性、查看是否有明显的Shuffle浪费、确认是否可以直接用AQE解决倾斜问题。这三件事做完,已经能解决大部分性能问题,参数调优只是微调。
7.2 监控体系是性能优化的底层保障
没有监控,就没有调优的依据。我在这次优化前,对整个集群搭建了一套基础的监控:Spark UI的日志汇总、YARN资源监控、HDFS文件大小和数量统计、每个任务的核心指标记录。
有了这些监控数据,我才能回答“任务为什么慢”这个问题,而不是靠猜。如果你的团队还没有这套基础,建议先把监控补上,哪怕是最简单的脚本定时拉取Spark和YARN的指标,也好过没有。
7.3 性能优化是持续迭代的过程
最后想说一点:大规模数据处理的性能优化不是一个一次性的项目,而是一个持续迭代的过程。数据量在增长,业务逻辑在变化,Spark版本在升级,今天优化的结论可能一个月后就不适用了。保持对监控数据的敏感度,定期复盘任务执行情况,把优化经验沉淀成文档和平台能力,这才是长期受益的做法。
我现在的习惯是每周看一次核心任务的执行汇总,遇到异常波动及时处理,而不是等业务方投诉才去查。这套“监控 + 复盘 + 优化”的循环,是我个人觉得在大数据领域最值得投入时间的地方。
