2. 数据倾斜原理与定位:成因、表现与排查方法

数据倾斜,说白了就是“忙的忙死,闲的闲死”。

你想想看,一个 Spark 任务分了 200 个分区,199 个跑完了,就剩一个在那吭哧吭哧算。或者 Hive 跑个 Join,Map 阶段挺快,Reduce 阶段卡住不动了。嗯,这就是典型的倾斜。

我刚开始做大数据那会儿,遇到这种问题第一反应就是加资源。加完发现,该慢还是慢,该 OOM 还是 OOM。后来才明白,不解决倾斜,加再多机器也是白搭。

2.1 数据倾斜的成因

倾斜的本质就一句话:数据在分区之间分布严重不均。

为什么会这样?我归纳了最常见的几个原因:

成因 典型场景 影响
Key 分布不均 空值、默认值、热点 Key(如某个城市 ID 占 80%) 单个分区数据量巨大
Join 键倾斜 大表 Join 小表,小表 Key 重复极高 Shuffle 阶段数据倾斜
分组聚合倾斜 Group By 某个高基数字段,但某个值占比极高 Reduce 阶段长尾
数据本身特征 日志中某个用户行为占比 90% 所有算子都可能倾斜
分区策略不当 Hash 分区导致大量数据落入同一分区 写入/读取阶段倾斜

核心认知:倾斜不是 Bug,是数据分布的自然现象。你没法改变数据,但可以改变计算方式。

2.2 常见表现:OOM 与任务长尾

倾斜的表现其实很直观。我把它分成两类:

2.2.1 OOM(内存溢出)

OOM 是最疼的那种。任务跑着跑着,Executor 突然挂了,日志里一堆 OutOfMemoryError。

为什么会 OOM?因为某个分区数据太多,单机内存装不下。比如你在做 groupByKey 或者 map-side join 时,某个 Key 对应的 Value 列表有几千万条,直接撑爆了。

我曾经踩过的坑:有一次做用户画像聚合,用 groupByKey 把用户所有行为塞到一个 Iterable 里。结果某个超级用户有 3000 万条记录,Executor 直接 OOM。后来改成 reduceByKey 加预聚合,问题就解决了。

2.2.2 任务长尾

长尾比 OOM 更隐蔽。任务没挂,但就是慢。你看 Spark UI 上,大部分 Task 几秒跑完,就几个 Task 跑了半小时还在转。

长尾的典型特征:

  • Stage 进度条卡在 99% 不动
  • 某个 Task 的 Shuffle Read 数据量是其他 Task 的 10 倍以上
  • GC 时间异常高(超过 Task 运行时间的 20%)

2.3 定位方法:Spark UI 与 Hive 日志

定位倾斜,说白了就是“找证据”。我个人的习惯是三步走:

  1. 看宏观:哪个 Stage 最慢?
  2. 看微观:哪个 Task 数据量异常?
  3. 看根因:是哪个 Key 导致的?

2.3.1 用 Spark UI 定位

Spark UI 是我用得最多的工具。你打开 http://driver:4040,重点看这几个地方:

UI 页面 看什么 倾斜信号
Stages 每个 Stage 的耗时、Shuffle Read/Write 量 某个 Stage 耗时是其他 Stage 的 5 倍以上
Stage Detail 每个 Task 的 Duration、Shuffle Read Size 少数 Task 的 Shuffle Read 是平均值的 10 倍
SQL Tab 物理执行计划、每个算子的数据量 Exchange 节点数据量差异巨大
Executors 每个 Executor 的 Shuffle Read、GC Time 某个 Executor 的 Shuffle Read 异常高

我的小技巧:在 Stage Detail 页面,按 Shuffle Read Size 降序排序。如果前 3 个 Task 的数据量占了总量的 80% 以上,基本可以断定倾斜了。

2.3.2 用 Hive 日志定位

Hive 的倾斜定位稍微麻烦点,因为日志不像 Spark UI 那么直观。我一般这么干:

  • 看 YARN 日志:找到 application_xxx,看每个 Container 的耗时。如果某个 Container 跑了 2 小时,其他 10 分钟,那就是它了。
  • 看 Hive 执行计划:用 EXPLAIN 看 Stage 划分,重点关注 Reduce 阶段的数据分布。
  • 看 Counter:Hive 的 Counter 里有个 RECORDS_OUT,如果某个 Reducer 的输出记录数是其他 Reducer 的 10 倍以上,倾斜无疑。
-- 查看 Hive 执行计划
EXPLAIN
SELECT city, COUNT(*)
FROM user_logs
GROUP BY city;

-- 重点关注 Reducer 数量和数据分布
-- 如果某个 Reducer 的 RECORDS_OUT 异常高,就是它

2.4 知识体系:数据倾斜全景图

下面这张图是我自己总结的。每次遇到倾斜问题,我都会对照着看一遍。

数据倾斜定位全景图 数据倾斜定位全景图 成因分析 Key 分布不均(空值/热点) Join 键倾斜 分组聚合倾斜 常见表现 OOM(内存溢出) 任务长尾(99% 卡住) GC 时间异常高 定位方法 Spark UI(Stage/Task) Hive 日志(YARN/Counter) 执行计划分析 定位思路:先看表现(OOM/长尾)→ 分析成因(Key/Join/聚合)→ 用对应工具定位 发现异常 定位 Stage 定位 Key 定位到 Key 之后,下一步就是选择优化策略(见第 3 章)

2.5 实战定位流程

最后,我分享一个我常用的定位流程。遇到倾斜问题,按这个顺序查,基本不会漏:

  1. 打开 Spark UI,看 Stages 页面,找到耗时最长的 Stage。
  2. 点进 Stage Detail,按 Shuffle Read Size 排序,看前几个 Task 的数据量。
  3. 如果数据量差异大,说明是 Shuffle 倾斜。点开 Task 看它的 Shuffle Read 来自哪些分区。
  4. 如果数据量差异不大,但耗时差异大,说明是计算倾斜(比如某个 Key 的计算逻辑特别重)。
  5. 定位到具体 Key:在代码里加 map 算子,输出每个 Key 的记录数,或者用 countByKey 采样。

避坑指南:我曾经有一次查了半天,发现是空值导致的倾斜。所有空值都 Hash 到了同一个分区。后来加了个 coalesce 把空值随机打散,问题就解决了。所以,先看空值,这是最常见的倾斜源。