4. 数据倾斜的通用解决思路:调整并行度、重新设计Key、过滤异常数据、使用随机前缀

数据倾斜这个问题,说白了就是「忙的忙死,闲的闲死」。

我在一线处理过不少倾斜案例,有些是数据本身的问题,有些是业务逻辑埋的坑。但不管原因是什么,解决思路其实就那几板斧。今天我把最常用的四种方法掰开揉碎讲给你听。

4.1 调整并行度:最简单的第一招

很多人一遇到倾斜,第一反应就是「加大并行度」。这招有用吗?有点用,但治标不治本。

为什么?你想想看,并行度再大,如果某个Key的数据量占了一半,那这个Task还是得处理一半的数据。其他Task再闲也没用。

不过,在某些场景下调整并行度确实能缓解问题。比如数据分布虽然不均匀,但还没到「一核有难,多核围观」的程度。这时候适当提高并行度,能让每个Task处理的数据量降下来。

我的经验: 我个人习惯先看数据分布。如果最大Key的数据量不超过总量的20%,调并行度通常有效。超过这个阈值,就得想别的办法了。
-- Spark中调整并行度
-- 设置shuffle分区数
SET spark.sql.shuffle.partitions = 200;

-- 或者针对特定操作调整
df.repartition(200, col("city")).write...  

4.2 重新设计Key:从根源上解决问题

这是我最喜欢用的方法。说白了,倾斜的根源就是Key设计不合理。

举个例子,我在项目中遇到过按「省份」分组统计用户活跃度。结果呢?广东、江苏的数据量是西藏、青海的几百倍。这能不倾斜吗?

解决方案其实很简单——把Key拆细一点。比如把「省份+城市」作为组合Key,或者按时间维度再切分。

核心思路: 让Key的粒度更细,数据分布更均匀。不要用高基数的字段做Key,也不要用太粗粒度的字段。
-- 不好的Key设计:按省份分组
SELECT province, COUNT(*) FROM user_logs GROUP BY province;

-- 好的Key设计:按省份+城市分组
SELECT province, city, COUNT(*) FROM user_logs GROUP BY province, city;

-- 或者按时间+省份
SELECT dt, province, COUNT(*) FROM user_logs GROUP BY dt, province;

4.3 过滤异常数据:把「捣乱分子」揪出来

嗯,这里要注意。很多时候倾斜是因为数据本身有问题。

我记得有一次做用户画像,发现某个Key的数据量特别大。一查,原来是某个爬虫程序把「null」当成了用户ID,结果几千万条数据都聚到了null这个Key上。

这种问题怎么处理?先过滤,再分析。

避坑指南: 我曾经因为没过滤空值,导致一个任务跑了6个小时。后来加了个简单的过滤条件,15分钟就跑完了。所以,先看看你的数据里有没有「脏数据」。
-- 过滤空值或异常值
SELECT 
  COALESCE(user_id, 'unknown') AS user_id,
  COUNT(*) 
FROM user_logs 
WHERE user_id IS NOT NULL  -- 过滤空值
  AND user_id != ''        -- 过滤空字符串
  AND user_id != 'null'    -- 过滤字符串"null"
GROUP BY user_id;

-- 或者直接排除异常Key
SELECT 
  user_id, 
  COUNT(*) 
FROM user_logs 
WHERE user_id NOT IN ('', 'null', '-1', '0')  -- 排除常见异常值
GROUP BY user_id;

4.4 使用随机前缀:最灵活的「杀手锏」

这个方法,说白了就是给Key加点「盐」。让原本聚集在同一个Key上的数据,分散到多个Key上去。

具体怎么做?给Key加一个随机数前缀。比如原本的Key是「北京」,现在变成「0_北京」、「1_北京」、「2_北京」……这样数据就分散到多个Task了。

但要注意,这只是中间步骤。最后还得把加了前缀的数据聚合回来。

我的习惯: 我一般分两步走。第一步,加随机前缀做局部聚合。第二步,去掉前缀做全局聚合。这样既分散了数据,又保证了结果的正确性。
-- 第一步:加随机前缀,做局部聚合
SELECT 
  CONCAT(CAST(FLOOR(RAND() * 10) AS STRING), '_', city) AS city_with_prefix,
  COUNT(*) AS partial_count
FROM user_logs
GROUP BY city_with_prefix;

-- 第二步:去掉前缀,做全局聚合
SELECT 
  SPLIT(city_with_prefix, '_')[1] AS city,
  SUM(partial_count) AS total_count
FROM (
  SELECT 
    CONCAT(CAST(FLOOR(RAND() * 10) AS STRING), '_', city) AS city_with_prefix,
    COUNT(*) AS partial_count
  FROM user_logs
  GROUP BY city_with_prefix
) tmp
GROUP BY city;

4.5 四种方法怎么选?一张表说清楚

你可能会问,这么多方法,到底用哪个?我整理了一张表,方便你对照选择。

方法 适用场景 优点 缺点 推荐指数
调整并行度 数据分布轻度不均 实现简单,改动小 治标不治本,效果有限 ★★☆☆☆
重新设计Key Key粒度太粗 从根源解决问题 需要理解业务逻辑 ★★★★★
过滤异常数据 存在脏数据或异常值 效果立竿见影 可能丢失有效数据 ★★★★☆
使用随机前缀 单个Key数据量极大 灵活,适用性强 需要两步聚合,稍复杂 ★★★★☆
总结一下: 我个人建议的排查顺序是——先过滤异常数据,再看Key设计是否合理,然后考虑加随机前缀,最后才调并行度。这个顺序是我踩过不少坑之后总结出来的,你试试就知道。

好了,这四种方法讲完了。下次遇到数据倾斜,别慌,按这个思路一步步排查,大部分问题都能解决。