3. 数据倾斜的根因分析:Key分布不均、数据膨胀、Join操作引发倾斜、窗口函数导致倾斜

说实话,做增量计算这些年,我见过太多团队一遇到任务卡死、OOM,第一反应就是加资源。加完发现,该慢还是慢,该挂还是挂。为什么?因为没找到根因。

数据倾斜,说白了就是「劳逸不均」。集群里大部分节点闲着,就那一两个节点累死。你想想看,这跟加班文化有什么区别?资源加再多,也架不住某个节点扛着全组的活。

这一章,我就把最常见的四种倾斜根因掰开揉碎讲清楚。每个原因我都踩过坑,有些坑现在想起来还肉疼。

核心认知:数据倾斜不是「数据量大」的问题,而是「数据分布不均匀」的问题。定位根因,比盲目优化重要一百倍。

数据倾斜四大根因 数据倾斜 Key 分布不均 热点 Key 问题 数据膨胀 单条记录过大 Join 倾斜 大 Key 关联 窗口函数倾斜 PARTITION BY 问题 💡 排查思路 • 先看 Stage 耗时分布:如果某个 Stage 明显慢于其他,大概率有倾斜 • 再看 Task 数据量:同一个 Stage 内,某个 Task 处理的数据量是其他 Task 的 3 倍以上 • 最后看 Key 分布:用 GROUP BY + COUNT 统计每个 Key 的记录数,找出热点

3.1 Key 分布不均:最经典的倾斜场景

这是最基础、也最常见的倾斜原因。我刚开始做实时数仓那会儿,第一个线上事故就是栽在这个坑里。

什么叫 Key 分布不均?举个例子。你在做用户行为分析,按 user_id 分组统计。大部分用户一天操作几十次,但某个爬虫用户一天操作了几百万次。好了,这个用户所在的 Task 直接爆炸。

我的经验:判断 Key 是否均匀,我习惯先跑一个快速采样查询:SELECT key, COUNT(*) as cnt FROM table GROUP BY key ORDER BY cnt DESC LIMIT 10。如果 Top 1 的计数是 Top 10 的 10 倍以上,基本可以断定有倾斜。

常见的 Key 分布不均场景包括:

  • 空值/默认值聚集:比如 user_id 为空时,所有空值都落到同一个分区
  • 业务热点:大 V 用户、热门商品、头部主播——这些实体的数据量天然比其他实体大几个数量级
  • 时间窗口边界:按小时分区时,业务高峰时段的数据量可能是低谷时段的几十倍

3.2 数据膨胀:一条记录撑爆一个分区

这个原因比较隐蔽。不是 Key 分布有问题,而是单条记录本身太大。

我记得有一次排查一个 Flink 任务 OOM 的问题。看 Key 分布,挺均匀的。看数据量,每个分区也就几百 MB。但就是有一个 Task 频繁挂掉。

后来发现,某个用户的「画像标签」字段存了一个 JSON 数组,里面塞了十几万个标签。单条记录序列化之后接近 100MB。这个用户所在的 Task,每次处理这条记录都要分配大块内存,频繁触发 Full GC,最后直接挂掉。

数据膨胀类型 典型场景 影响
单条记录过大 嵌套 JSON、大文本、长数组 单个 Task 内存溢出
中间结果膨胀 笛卡尔积、未优化的 JOIN Shuffle 数据量暴增
聚合结果膨胀 GROUP BY 粒度过细 Reduce 阶段数据倾斜

⚠️ 避坑指南:我曾经在一个 Spark Streaming 任务里,因为没对 JSON 字段做 schema 裁剪,导致每条记录膨胀了 20 倍。后来加了一个 select ... except(nested_column) 把不需要的大字段去掉,问题直接解决。记住:能早过滤就早过滤,别把垃圾数据带到 Shuffle 阶段。

3.3 Join 操作引发倾斜:最头疼的倾斜类型

说实话,Join 导致的倾斜是我最不愿意碰的。为什么?因为它的排查链路最长,优化手段也最复杂。

Join 倾斜的核心逻辑很简单:两个表关联时,如果关联 Key 分布不均匀,那么大量数据会落到同一个 Reduce 节点上。

举个例子。你在做订单表和商品表的 Join,关联 Key 是 product_id。大部分商品一天卖几十单,但某个爆款商品一天卖了 100 万单。好了,这个 product_id 所在的 Reduce 节点要处理 100 万条订单数据,其他节点可能只处理几千条。

常见的 Join 倾斜场景:

  • 大表 Join 大表:两个表都有热点 Key,互相放大
  • 事实表 Join 维度表:维度表中某些维度值对应的事实表数据量巨大
  • 自关联:比如用户关系链、树形结构数据的自关联

关键判断指标:在 Spark UI 或 Flink Web Dashboard 中,如果某个 Join 操作的 Stage 耗时是其他 Stage 的 3 倍以上,而且该 Stage 的 Shuffle Read 数据量明显偏大,基本可以确定是 Join 倾斜。

3.4 窗口函数导致倾斜:最容易被忽视的坑

这个原因,我敢说 80% 的工程师第一次遇到时都懵了。窗口函数也会导致倾斜?

会的。而且一旦出现,排查起来特别费劲。

窗口函数倾斜的根因在于 PARTITION BY 子句。你想想看,窗口函数是在每个分区内独立计算的。如果某个分区的数据量特别大,那这个分区所在的 Task 就要处理远超其他 Task 的数据量。

我印象最深的一次,是在做用户行为序列分析时用了 ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY event_time)。大部分用户的行为记录在几十条到几百条之间,但有一个企业级用户,一天产生了上千万条埋点数据。结果这个用户所在的 Task 跑了 40 分钟还没跑完,其他 Task 几十秒就结束了。

窗口函数倾斜的典型场景:

  • PARTITION BY 字段分布不均:比如按 city_id 分区,一线城市的数据量是三四线城市的几百倍
  • ORDER BY + 大分区:分区内需要全局排序,数据量大的分区排序开销呈指数级增长
  • 多个窗口函数叠加:同一个分区上叠加多个窗口计算,放大单分区压力

我的建议:使用窗口函数前,先对 PARTITION BY 的字段做一次数据分布探查。如果发现某个分区的数据量超过平均值的 5 倍,就要考虑是否能用其他方式替代窗口函数,或者对热点分区做拆分处理。

好了,四种根因都讲完了。你可能会问:知道了根因,然后呢?别急,后面几章我会针对每种根因给出具体的优化方案和代码实现。但在此之前,我建议你先在自己的任务日志里找找,看看你的倾斜属于哪一种。定位准确了,优化就是水到渠成的事。