在大数据计算领域,最让人头疼的问题之一就是 数据倾斜。它常常是导致任务跑得慢、集群资源被白白浪费、甚至直接失败的元凶。别看名字挺玄乎,其实本质并不复杂。

1. 数据倾斜的本质原因

一句话:数据分布不均导致计算资源不均。

更具体点说:
如果某些 Key 的数据量远远大于其他 Key,那就会导致部分计算节点的任务负担过重,结果就是——有的 Task 很快跑完,有的 Task 却累得要死要活,整个任务只能等最慢的那几个收尾。这就是典型的 “木桶效应”,也被叫做 长尾效应

2. 怎么判断是不是数据倾斜?

数据倾斜的典型现象,往往都能在 Spark、Flink 的 Web UI 上直接看到:

任务执行时间异常变长:某些 Task 执行特别久,把整体进度拖慢。

Task 卡住在 99% 不动:总有几个 Task 死活跑不完。

频繁失败重试 / OOM:个别节点的内存频繁爆掉,反复 GC,甚至直接挂掉。

单节点资源飙升:某台机器 CPU 或内存占用远超其他节点。

在 ODPS 里,Fuxi 就会直接把耗时超过平均值 2 倍的实例判定为长尾实例。

一句话总结:大部分任务都跑得轻松,就有几个“拖后腿”的。

3. 怎么排查数据倾斜?

通常要分两步走:

(1)先看是不是数据分布问题

在 Spark UI / Flink Web UI / Doris Profile 里看 Task 的执行时间分布;

关注 Shuffle 阶段的统计:是否有某些 Task 的 shuffle read 特别大;

看日志里是否有频繁的 Task attempt 重试、OOM。

还可以直接在数据源里跑一条 SQL 来看 Key 的分布情况:

SELECT key, COUNT(*) 
FROM table
GROUP BY key
ORDER BY COUNT(*) DESC
LIMIT 10;

如果结果显示 某个 key 出现了一千万次,而其他只出现几次,那基本就实锤了。

(2)再看是不是资源/节点问题

有些情况是节点本身的问题,比如:

某台机器 CPU 长时间 90%+;

内存频繁爆掉或者 swap;

磁盘 IO 异常,shuffle 文件写得很慢;

网络带宽不足,传输速度被卡住。

这时候就要结合 Prometheus/Grafana 或 Node Exporter 看节点指标,排除掉系统层面的问题。

4. 数据倾斜的高发场景 & 解决思路

常见的导致倾斜的 SQL 操作有几类:Join、GroupBy、Count(Distinct)、ROW_NUMBER (TopN)、动态分区。不同场景有不同的优化方法。

以ODPS为例

Join 倾斜

大表 + 小表:用 MapJoin,小表直接放内存里,避免 Shuffle。

大表 + 中表:用 DistMapJoin

热点 Key

手动拆分热点值,把热点值单独处理;

开启 SkewJoin 参数set odps.sql.skewjoin=true;

使用 SkewJoin Hint,明确告诉执行引擎哪些是热点值;

或者干脆用 倍数表取模法 来均摊负载。

GroupBy 倾斜

设置参数:set odps.sql.groupby.skewindata=true;

加随机数打散:把热点 Key 拆成多个小组,后面再合并。

滚存表:提前预聚合,减少最终的 GroupBy 规模。

Count(Distinct)

参数优化:set odps.sql.groupby.skewindata=true;

两阶段聚合:先拼接随机数打散,再在上层聚合。

用 分区字段 + 业务字段 组合聚合,比如 (ds, shop_id)

ROW_NUMBER (TopN)

SQL 写法的两阶段聚合:拼接随机数,让热点 Key 分担到多个分区。

用 UDAF 实现:基于 最小堆队列,提高计算效率。

5. Doris 中的数据倾斜处理案例

在 Doris 里,数据倾斜也经常出现在 聚合查询 或 Join 查询 场景。常见做法:

分桶策略优化
建表时,选择合适的 DISTRIBUTED BY HASH(key) 字段。如果 user_id 明显是热点,可以改用 (user_id, dt) 等组合字段来更均匀分布数据。

CREATE TABLE orders (
    order_id BIGINT,
    user_id BIGINT,
    dt DATE,
    amount DECIMAL(10,2)
)
DISTRIBUTED BY HASH(user_id, dt) BUCKETS 32;

随机数打散再聚合
查询时给热点 key 增加随机前缀,打散后再二次聚合:

SELECT user_id, SUM(amount) 
FROM (
    SELECT user_id, FLOOR(RAND()*10) AS salt, amount
    FROM orders
) t
GROUP BY user_id, salt;

再在外层做一次聚合:

SELECT user_id, SUM(sum_amount)
FROM (
    SELECT user_id, salt, SUM(amount) as sum_amount
    FROM orders_salt
    GROUP BY user_id, salt
) tmp
GROUP BY user_id;

利用物化视图
对热点聚合场景,提前做物化视图,避免查询时产生严重的倾斜:

CREATE MATERIALIZED VIEW mv_user_sum AS
SELECT user_id, SUM(amount)
FROM orders
GROUP BY user_id;

6. 总结

数据倾斜问题,说白了就是 数据不均衡 + 资源调度受限。定位时要先分清是 业务数据分布问题,还是 节点资源问题
解决思路也很明确:

从 SQL 逻辑上入手(打散热点 key、两阶段聚合、MapJoin);

从执行参数入手(开启防倾斜参数、设置 SkewJoin);

从资源角度入手(排查机器负载、网络 IO、磁盘 IO)。

一句大白话总结:遇到数据倾斜,别慌,先找“胖子 Key”,再决定是拆分、合并,还是直接交给引擎参数去搞定。