订阅更新

面试官问'你做过哪些性能优化'——讲不出数字就没有说服力

项目追问手册 第 4 题 约 10 分钟读完

本文目录
  1. 先说结论
  2. 性能优化的四层模型
  3. SQL层优化:最简单但效果最明显
  4. 引擎参数层优化:调对一个参数顶10行代码
  5. 架构层优化:从全量改增量
  6. 怎么组织'优化故事'给面试官讲
  7. 面试高频追问
  8. 实战踩坑
  9. 总结

先说结论

性能优化必须有Before/After数字:'从32分钟降到6分钟''Shuffle数据量从8GB减少到1.2GB''存储成本从每月$3000降到$1950'。没有数字的优化就是空谈。

优化要分层讲:从SQL层→引擎参数层→架构层→存储层,每一层都有不同的优化手段。面试官想听的是你能系统性地思考性能问题,而不是只会'加内存'。

讲优化的套路是'定位瓶颈→分析原因→选择方案→验证效果':不是上来就说优化方案,而是先说清楚你怎么发现瓶颈的。会定位问题比会优化更值钱。

先把这段讲顺;面试官追问时,再展开下面的原理和细节。

详细讲解

性能优化是大数据面试的必考题,但很多人讲起来就是'加了索引快了一点''调了参数好了一些'。面试官听完一脸问号——到底快了多少?从多少降到多少?优化前后的关键指标是什么?没有数字的性能优化,就像没有成绩的考试——你说你考得不错,但不告诉我多少分,谁信呢?今天把我在生产环境做过的优化经验系统性地分享一下。

性能优化的四层模型

优化层级常见手段效果量级难度
SQL层列裁剪、谓词下推、避免笛卡尔积、合理使用窗口函数30-50%★★
引擎参数层AQE、Broadcast阈值、Shuffle分区数、内存配比20-40%★★★
架构层分层设计、预聚合、增量替代全量、读写分离50-80%★★★★
存储层分区策略、文件格式、压缩算法、Z-Ordering30-60%★★★

面试时按这个顺序讲,从简单到复杂,展示你对性能优化的系统性理解。

SQL层优化:最简单但效果最明显

我在某知名出海硬件品牌做的第一件优化就是SQL层面的。当时接手的老代码到处是SELECT *,而且很多JOIN没有指定正确的Join条件。

-- ❌ 优化前:SELECT * + 不必要的大表JOIN
SELECT *
FROM orders o
JOIN products p ON o.product_id = p.product_id
JOIN users u ON o.user_id = u.user_id
WHERE o.dt = '2023-06-01';
 
-- ✅ 优化后:只选需要的列 + 先过滤再JOIN
SELECT o.order_id, o.amount, p.product_name, u.country
FROM (
  SELECT order_id, amount, product_id, user_id
  FROM orders
  WHERE dt = '2023-06-01'   -- 先过滤,减少JOIN数据量
) o
JOIN products p ON o.product_id = p.product_id
JOIN users u ON o.user_id = u.user_id;

效果:Shuffle数据量从8GB减少到4.4GB(减少45%),仅仅是把SELECT *改成列裁剪,加上谓词提前过滤。这个优化改动量小但效果明显,面试时讲出来很有说服力。

引擎参数层优化:调对一个参数顶10行代码

参数调优是面试时最常被问到的,但很多人只会说'调大内存'。其实最有价值的参数调优是这几个:

// ① Broadcast Join阈值:小表JOIN时避免Shuffle
spark.conf.set("spark.sql.autoBroadcastJoinThreshold", "200m")
// 默认10MB太小,我调到200MB
// 效果:多个JOIN从Shuffle变成Broadcast,作业时间减少40%
 
// ② 开启AQE自适应查询
spark.conf.set("spark.sql.adaptive.enabled", "true")
spark.conf.set("spark.sql.adaptive.skewJoin.enabled", "true")
spark.conf.set("spark.sql.adaptive.coalescePartitions.enabled", "true")
// 效果:自动处理数据倾斜 + 自动合并小分区
 
// ③ Shuffle分区数:根据数据量调整
spark.conf.set("spark.sql.shuffle.partitions", "400")
// 默认200,数据量大时分区过少导致OOM
// 但如果用了AQE,这个参数会被自动调整

我在某出海硬件品牌的实际效果:把autoBroadcastJoinThreshold从10MB调到200MB后,有3个核心作业的JOIN自动变成了Broadcast,每个作业的执行时间减少约40%。这一个参数改动,比改10行SQL效果还好。

架构层优化:从全量改增量

架构层优化是最难讲但也最有深度的。我遇到的一个经典场景是维度表的更新逻辑。

优化前,我们的维度表更新用的是'全量JOIN + UNION + OVERWRITE'的方式:每天把增量数据和全量数据做JOIN,找出新增和变更的记录,然后UNION合并后全量重写。代码量大、逻辑复杂、而且全量重写非常慢。

优化后,用Delta Lake的MERGE INTO替代了整个逻辑:

-- ❌ 优化前:全量JOIN + UNION + OVERWRITE(20+行SQL)
INSERT OVERWRITE TABLE dim_user
SELECT COALESCE(inc.user_id, full.user_id) AS user_id,
       COALESCE(inc.email, full.email) AS email,
       COALESCE(inc.updated_at, full.updated_at) AS updated_at
FROM dim_user full
FULL JOIN user_increment inc ON full.user_id = inc.user_id;
 
-- ✅ 优化后:MERGE INTO(4行搞定)
MERGE INTO dim_user t
USING user_increment s ON t.user_id = s.user_id
WHEN MATCHED THEN UPDATE SET *
WHEN NOT MATCHED THEN INSERT *;
 
-- 效果对比:
-- 代码量:从20+行 → 4行,减少70%
-- 性能:MERGE INTO只重写变更的文件,不需要全量重写

面试时讲这个优化很好用:既展示了你对新技术(Delta Lake MERGE INTO)的了解,又展示了你的架构思维——用更好的工具替代蛮力方案。

怎么组织'优化故事'给面试官讲

面试时讲性能优化,推荐这个结构:

// 性能优化的叙述结构
 
// Step 1: 说清问题和影响
"我们有一个核心ETL作业,原来跑32分钟,
  下游的BI报表每天早上9点要用,但这个作业8:40才跑完,
  经常卡住整个链路。"
 
// Step 2: 说清排查过程
"我打开Spark UI分析,发现两个瓶颈:
  第一是一个大表JOIN小表走了Shuffle,花了15分钟;
  第二是SELECT *带了60多列但下游只用了8列。"
 
// Step 3: 说清优化手段和选择理由
"针对第一个问题,我调大Broadcast阈值让小表走广播;
  针对第二个问题,我把SELECT *改成只选8列。
  选这两个方案是因为改动最小但效果最大。"
 
// Step 4: 用数字说结果
"优化后执行时间从32分钟降到6分钟,
  Shuffle数据量减少45%,每天早上7点就跑完了。"

面试高频追问

追问一:你怎么定位性能瓶颈的?用什么工具?

这是验证你是不是真的做过优化。我的回答是:'主要用Spark UI和Databricks的查询历史。Spark UI里看每个Stage的耗时、Shuffle数据量、Task的数据倾斜情况。Databricks的查询历史可以看到每个作业的历史执行时间趋势,发现是突然变慢还是慢慢退化。我习惯从最慢的Stage开始看,一般80%的优化收益来自最慢的那个Stage。'

追问二:有没有优化反而变慢的情况?

这是考你的实战经验深度。确实遇到过:'我曾经对一个作业加了cache()缓存中间结果,以为可以加速后续的多次使用。结果那个中间结果有15GB,缓存后反而触发了大量GC,比不缓存还慢20%。后来改成checkpoint写磁盘反而更好,因为checkpoint可以截断RDD血缘链,减少内存压力。教训是:缓存不是万能的,大数据量的缓存要考虑内存承载能力。'

追问三:如果让你优化一个你没见过的作业,你的排查思路是什么?

这考的是你的方法论。我会说:'分五步——第一步看执行时间分布,哪个Stage最慢;第二步看Shuffle数据量,是不是数据量导致的;第三步看Task的分布,是不是数据倾斜;第四步看SQL执行计划,有没有笛卡尔积或不必要的全表扫描;第五步看资源使用,是不是内存不足导致频繁GC。一般前三步就能定位到80%的问题。'

实战踩坑

我在某知名出海硬件品牌做过一次'好心办坏事'的优化。当时有个同事的Spark作业用的是老的RDD写法,我帮他改成了DataFrame。改完之后确实快了——执行时间减少了50%。

但问题来了:这个作业用的是一个自定义的Partitioner来做特殊的数据分配逻辑,RDD版本可以直接指定Partitioner,DataFrame版本做不到。改成DataFrame后,虽然整体变快了,但数据分配逻辑变了,导致下游的一个报表数字对不上。

// ❌ 我犯的错:RDD改DataFrame时丢失了自定义Partitioner
// 原来的RDD写法
rdd.partitionBy(new CustomPartitioner(24))
   .mapValues(process)
   .saveAsTextFile(output)
 
// 我改成的DataFrame写法
df.repartition(24, col("key"))
  .write.mode("overwrite")
  .parquet(output)
 
// 问题:repartition用的是HashPartitioner
// 而CustomPartitioner的分区逻辑不是标准Hash
// 导致下游按分区读数据的逻辑出错
 
// 解决:保留RDD的自定义Partitioner逻辑
// 在DataFrame前加一步转RDD做分区

教训:性能优化不能只看速度,还要保证逻辑正确。优化后一定要做数据对账——对比优化前后的输出结果是否一致。我后来在所有优化任务后都加了自动对账步骤,比对核心指标(总数、求和、去重数)。

总结

性能优化讲四件事:从哪里发现瓶颈、瓶颈的原因是什么、选了什么方案(以及为什么不选其他方案)、优化前后的数字对比。按SQL层→参数层→架构层→存储层的顺序准备,每层至少准备一个带数字的案例。面试时先讲定位过程再讲优化方案,展示你的排查能力和系统性思维。

下一篇聊大数据面试的高频技术题:数据倾斜只会说'加盐'?面试官要的是完整的排查和解决思路

你做过印象最深的一次性能优化是什么?效果怎么样?评论区聊聊~

关注「Wood聊大数据」| 9年大数据老兵,聊技术、聊职场、聊成长

刷题进度保存在当前浏览器