3. 数据倾斜的根因分析:Key分布不均、数据膨胀、Join操作引发倾斜、窗口函数导致倾斜
说实话,做增量计算这些年,我见过太多团队一遇到任务卡死、OOM,第一反应就是加资源。加完发现,该慢还是慢,该挂还是挂。为什么?因为没找到根因。
数据倾斜,说白了就是「劳逸不均」。集群里大部分节点闲着,就那一两个节点累死。你想想看,这跟加班文化有什么区别?资源加再多,也架不住某个节点扛着全组的活。
这一章,我就把最常见的四种倾斜根因掰开揉碎讲清楚。每个原因我都踩过坑,有些坑现在想起来还肉疼。
核心认知:数据倾斜不是「数据量大」的问题,而是「数据分布不均匀」的问题。定位根因,比盲目优化重要一百倍。
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 倍,就要考虑是否能用其他方式替代窗口函数,或者对热点分区做拆分处理。
好了,四种根因都讲完了。你可能会问:知道了根因,然后呢?别急,后面几章我会针对每种根因给出具体的优化方案和代码实现。但在此之前,我建议你先在自己的任务日志里找找,看看你的倾斜属于哪一种。定位准确了,优化就是水到渠成的事。