订阅更新

你有遇到过 Spark 的数据倾斜吗?

面试问 002 约 5 分钟读完 4 次阅读

本文目录
  1. 面试怎么回答
  2. 第一步:先确认是不是数据倾斜
  3. 第二步:找到底是哪一类倾斜
  4. 一个典型生产场景
  5. 为什么“增加分区数”经常没效果?
  6. 我理解的 Spark 数据倾斜处理

面试怎么回答

我遇到过。我的处理思路不是直接加盐,而是先确认是不是倾斜。我会在 Spark UI 里看同一个 Stage 的 Task 耗时、Shuffle Read 和记录数分布。如果只有少数 Task 数据量和耗时明显偏高,再定位对应的 Join 或聚合 key。聚合热点通常用两阶段聚合或加盐;小表 Join 优先考虑 Broadcast;NULL 和默认值热点会先过滤或拆开处理。Spark SQL 也可以利用 AQE 拆分部分倾斜的 Join 分区。核心是先找到数据为什么集中,再选择方案,而不是只调大资源。

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

详细讲解

你是怎么发现,又是怎么解决的?

Spark 数据倾斜:别一上来就加盐

很多人一上来就回答:

  • 给 key 加盐
  • 调大 Shuffle 分区数
  • 增加 Executor

这些方法都可能有用。

但如果只回答这些,面试官很可能会继续追问:

你怎么确定它真的是数据倾斜?又怎么知道该用哪一种方案?

因为 Spark 任务跑得慢,不一定就是数据倾斜。

真正有价值的回答,不是背优化手段,而是讲清楚一条完整链路:

先识别倾斜,再定位倾斜 key,最后根据产生原因选择方案。


第一步:先确认是不是数据倾斜

数据倾斜最典型的现象,不是整个任务都慢。

而是同一个 Stage 里:

  • 大部分 Task 很快结束
  • 少数 Task 长时间跑不完
  • 个别 Task 的 Shuffle Read 或输入记录数远高于其他 Task
  • 严重时还会出现大量 Spill、GC,甚至 OOM

这时候应该先看 Spark UI,而不是马上改参数。

重点看三个地方:

一看 Task 耗时分布

如果所有 Task 都慢,更可能是资源不足、计算逻辑复杂或者外部存储慢。

如果只有少数 Task 特别慢,才需要重点怀疑数据倾斜。

二看数据量分布

比较不同 Task 的 Input Size、Shuffle Read、Records 数量。

数据倾斜的本质,是少数分区承担了远高于其他分区的数据量。

三看执行算子

继续确认问题发生在 Join、Group By、窗口计算,还是 explode 之后。

因为发生位置不同,后面的解决方式也不一样。

先证明是倾斜,再谈怎么优化。

Spark 数据倾斜识别流程


第二步:找到底是哪一类倾斜

确认倾斜之后,不应该直接加盐,而是继续查数据分布和执行计划。

实际工作中常见的原因主要有下面几类。

第一类:聚合 key 本身就是热点

例如按店铺统计订单,某个头部店铺的数据量远高于其他店铺。

所有相同 key 最终都要进入同一个分区,单纯增加分区数并不能拆开这个热点 key。

这种情况可以考虑:

  • 先给热点 key 加随机前缀,做第一次局部聚合
  • 去掉随机前缀,再做第二次汇总
  • 能在上游预聚合的,尽量减少进入 Shuffle 的数据量

第二类:大表 Join 出现热点 key

例如大量事实数据都关联到同一个用户、商品或者默认值。

这时要先判断小表能不能广播。

如果维表足够小,使用 Broadcast Join 可以直接避免大表一侧按 Join key Shuffle。

如果不能广播,可以把热点 key 单独拆出来处理,或者给大表热点数据加盐,并对小表对应记录进行扩容后再 Join。

Spark SQL 的 AQE 也能在运行时识别并拆分部分倾斜的 Sort Merge Join 分区,但它更像一道运行时保护,不能替代对热点数据和业务语义的治理。

第三类:NULL 或默认值被集中到一起

这是非常容易被忽略的一种。

比如大量无效记录的 user_id 都被处理成 NULL、空字符串或者 -1。

业务上它们可能没有关联意义,但计算时却会被当成同一个 key,全部打到同一个分区。

这种情况应该先判断:

  • 无效 key 是否可以直接过滤
  • 是否需要单独走一条处理链路
  • 默认值是否掩盖了上游数据质量问题

很多时候,清理无效 key 比加盐更直接。

Spark 数据倾斜原因与解决方案


一个典型生产场景

比如一张行为明细表要关联用户维表。

任务运行后,绝大多数 Task 已经完成,只有一个 Task 长时间没有结束。

继续查看 Spark UI,会发现这个 Task 的 Shuffle Read 明显高于其他 Task。

再对 Join key 做频次统计,发现历史数据中大量缺失的 user_id 被统一写成了 -1。

问题就清楚了:

-1 并不代表同一个真实用户,却让大量数据集中到了同一个分区。

如果这些数据不需要参与用户关联,可以在 Join 前过滤或单独处理;如果必须保留,也应该把这部分数据拆开走独立逻辑,而不是盲目增加 Executor。

这条排查链路是:

少数 Task 异常慢 → 查看 Shuffle 分布 → 统计 Join key → 找到默认值热点 → 按业务语义拆分处理。

user_id 默认值导致倾斜的排查链路


为什么“增加分区数”经常没效果?

因为增加分区数解决的是“分区整体太大”,不一定能解决“某一个 key 太大”。

假设同一个热点 key 有大量数据。

无论设置 200 个还是 2000 个分区,只要分区规则仍然基于这个 key,它最终还是会进入同一个分区。

同样,增加 Executor 只能增加可用资源。

如果最后只剩一个倾斜 Task 在跑,其他 Executor 仍然可能处于空闲状态。

所以优化数据倾斜最关键的一步不是调参数,而是回答:

为什么这些数据会集中到同一个分区?


我理解的 Spark 数据倾斜处理

Spark 数据倾斜不是一个固定参数问题,而是一个数据分布问题。

加盐、Broadcast Join、两阶段聚合、拆分热点数据、开启 AQE,都只是工具。

真正高级的处理方式,是先用 Spark UI 和数据统计找到热点,再根据聚合、Join、默认值或上游数据问题选择最小成本的方案。

不要一看到任务慢就加盐,也不要把所有优化都归结为增加资源。


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