Hive 数据倾斜是分布式计算中一个非常普遍且棘手的问题,它指的是在处理大规模数据集时,某些计算节点(通常是 Reduce 任务)需要处理的数据量远大于其他节点,导致这些节点成为瓶颈,任务执行时间显著延长,甚至失败(如 OOM)。理解其现象、原因和解决方案对于优化 Hive 性能至关重要。
一、数据倾斜的现象
- 任务执行时间过长且不均衡:
- 绝大多数 Map 任务很快完成,但少数几个 Reduce 任务(通常只有 1 个或几个)长时间运行(卡在 99%)。
- 整个 Job 的完成时间由这几个最慢的 Reduce 任务决定。
- 监控指标异常:
- Hadoop/Hive 监控界面(如 YARN ResourceManager UI, Tez/Spark UI)显示个别 Container/Executor 的 CPU、内存、网络或磁盘 I/O 使用率异常高,远高于其他节点。
- 个别 Reduce 任务的输入记录数(Input Records)和处理的数据量(Input Size)远高于其他 Reduce 任务(可能是几十倍、几百倍甚至更多)。
- 日志报错:
- 经常能看到 Reduce 任务失败(Failures),错误信息常与资源不足相关,如
Container killed by YARN for exceeding memory limits(OOM - Out Of Memory)。 - 在日志中可能观察到处理特定 Key 时花费了极长时间。
- 经常能看到 Reduce 任务失败(Failures),错误信息常与资源不足相关,如
- 资源利用不均:
- 集群中大部分节点空闲或低负载,而承载倾斜任务的节点负载极高。
二、数据倾斜的主要原因
核心原因在于:数据在需要进行分布式聚合或连接的 Key 上分布极度不均匀。具体场景包括:
- Join 倾斜:
- 大表 Join 大表: 其中一个表(或两个表)的 Join Key 存在热点值(某些 Key 对应的记录数极其庞大)。例如,用户行为日志表 Join 用户维度表时,如果存在大量未登录用户(
user_id = NULL或user_id = 0),这些 NULL/0 值会被分发到同一个 Reduce 任务处理。 - Map Join 不适用: 当参与 Join 的表都很大,无法使用 Map Join(小表广播)时,只能走 Reduce Join(Common Join),此时 Key 分布不均的问题就会暴露。
- 大表 Join 大表: 其中一个表(或两个表)的 Join Key 存在热点值(某些 Key 对应的记录数极其庞大)。例如,用户行为日志表 Join 用户维度表时,如果存在大量未登录用户(
- Group By 倾斜:
- 进行
GROUP BY操作的字段存在高度集中的值。例如,按“国家”分组统计,但 90% 的数据都属于“中国”;按“商品状态”分组,绝大多数商品状态是“已下架”。
- 进行
- Count Distinct 倾斜:
- 使用
COUNT(DISTINCT column)时,如果该列存在大量重复值,或者某些值对应的记录数巨大,处理这些值的 Reduce 任务负载会很高。即使使用approx_count_distinct,在极端倾斜下也可能有问题。
- 使用
- 窗口函数倾斜:
- 使用
ROW_NUMBER(),RANK(),DENSE_RANK(),SUM() OVER (PARTITION BY ...)等窗口函数时,如果PARTITION BY的字段分布极不均匀,会导致某些分区数据量巨大。
- 使用
- 数据本身特性:
- 业务数据天然存在倾斜,如热门商品/用户的访问日志、默认值/错误值大量积累、枚举类型字段中某个枚举值占比过高。
- Key 设计不合理:
- 选择的 Join Key 或 Group By Key 本身不具有较好的离散性(如布尔值字段、状态字段只有少数几种值)。
- NULL 值或空值集中:
- 大量记录的 Key 为 NULL 或空字符串,在 Hive 中这些值通常会被视为同一个 Key 发送到同一个 Reduce 任务。
三、数据倾斜的解决方案
解决思路的核心是:打散热点 Key,将原本由一个 Reduce 任务处理的海量数据分散到多个 Reduce 任务上处理。以下是一些常用且有效的策略:
通用策略 & 参数调优
hive.map.aggr和hive.groupby.skewindata(针对 Group By):set hive.map.aggr = true;:在 Map 端进行部分聚合,减少传输到 Reduce 的数据量。推荐始终开启。set hive.groupby.skewindata = true;:专门应对 Group By 倾斜。 它会启动两个 MR Job:- 第一个 Job:Map 输出随机分发到 Reduce 进行部分聚合(此时 Key 可能被打散)。
- 第二个 Job:基于第一个 Job 的部分聚合结果,再按真正的 Group By Key 分发到 Reduce 进行最终聚合。这有效分散了热点 Key 的负载。
- 增加 Reduce 数量:
set mapred.reduce.tasks = N;/set mapreduce.job.reduces = N;(Hadoop 2.x+): 直接设置 Reduce 任务数。仅对 Key 本身有一定分散度的情况有效,如果存在单个超级热点 Key,增加 Reduce 数对此 Key 无效(它仍只在一个 Reduce 里处理)。通常结合其他打散方法使用。set hive.exec.reducers.bytes.per.reducer = ;:设置每个 Reduce 任务处理的数据量(默认约 256MB 或 1GB,取决于版本)。减小此值可以间接增加 Reduce 数量。同样对超级热点 Key 效果有限。
- 开启 Skew Join 优化 (Hive 0.10.0+ 推荐):
set hive.optimize.skewjoin = true;: 开启 Skew Join 优化。set hive.skewjoin.key = ;: 设置判断 Key 倾斜的阈值(默认 100000)。如果一个 Key 出现的次数超过该值,则认为是倾斜 Key。- 原理: Hive 会识别出倾斜的 Key。对于这些热点 Key,它不会在常规的 Reduce Join 中处理,而是将它们单独存储。然后启动一个额外的 MR Job(或使用 Map Join),将大表中对应热点 Key 的数据与小表中对应热点 Key 的数据进行 Map Join(广播小表片段)。非倾斜 Key 的数据仍在原 Reduce Join 流程中处理。最后合并结果。这是处理 Join 倾斜最常用且有效的手段之一。
针对特定场景的高级解决方案
-
拆分处理 - 热点 Key 分离 (Join 和 Group By 都适用):
- 步骤:
- 识别热点 Key(通过采样
SELECT key, count(1) FROM table GROUP BY key ORDER BY count(1) DESC LIMIT 10;)。 - 将原查询拆分成两部分:
- 处理热点 Key 部分: 单独写一个查询,只处理
WHERE key IN (热点Key列表)的数据。对于 Join,可以配合 Map Join(如果小表片段不大);对于 Group By,热点 Key 被拆出后,其数据量可能已可接受。 - 处理非热点 Key 部分: 写另一个查询,处理
WHERE key NOT IN (热点Key列表) OR key IS NULL的数据。此时数据分布相对均匀,可以正常使用 Reduce Join 或 Group By。
- 处理热点 Key 部分: 单独写一个查询,只处理
- 将两个查询的结果进行
UNION ALL。
- 识别热点 Key(通过采样
- 优点: 思路清晰,效果显著。
- 缺点: SQL 变复杂,需要预先知道热点 Key。
- 步骤:
-
给热点 Key 加随机前缀/后缀 (打散 - 主要针对 Join):
- 场景: 大表 A Join 大表 B,表 A 的 Join Key 有热点值 K。
- 步骤:
- 对表 A (有热点 Key 的表) 的 Join Key 添加随机前缀(如 0~N-1 的随机数,N 为打散因子,如 10):
SELECT ..., CONCAT(key, '_', CAST(floor(RAND() * 10) AS STRING)) AS salted_key FROM A; - 对表 B (需要扩容的小表) 进行笛卡尔积扩容。将表 B 每条数据复制 N 份(N 同上),并为每条复制记录添加不同的后缀(0~N-1):
SELECT ..., CONCAT(key, '_', CAST(salt AS STRING)) AS salted_key FROM B LATERAL VIEW explode(array(0, 1, 2, ..., N-1)) salts AS salt; -- 使用 UDTF 或 笛卡尔 JOIN 一个包含 N 个数字的小表 - 将处理后的临时表 A_salted 和 临时表 B_expanded 进行 Join,条件是
A_salted.salted_key = B_expanded.salted_key。这样,原来对应热点 Key K 的数据,现在被打散成了K_0,K_1, …,K_(N-1)这 N 个 Salted Key,分散到最多 N 个 Reduce 任务上处理。 - 在最终结果中,可能需要去除添加的前缀/后缀(如果 Key 需要原样输出)。
- 对表 A (有热点 Key 的表) 的 Join Key 添加随机前缀(如 0~N-1 的随机数,N 为打散因子,如 10):
- 优点: 能有效打散单个超级热点 Key。
- 缺点: SQL 非常复杂;扩容小表增加了网络和计算开销;只适合一个表有热点 Key 的情况。
-
使用 Map Join 替代 Reduce Join (针对 Join):
set hive.auto.convert.join = true;(默认开启)set hive.mapjoin.smalltable.filesize = ;(默认约 25MB): 调整此参数,使得小表可以被广播到所有 Map 任务的内存中。- 原理: 完全避免 Shuffle 和 Reduce 阶段,将小表加载到每个 Map 任务的内存中,与大表的片段进行 Join。这是解决 Join 问题最高效的方式,但前提是小表足够小能放进内存。 对于大表 Join 大表无效。
-
优化
COUNT(DISTINCT):- 改为两层
GROUP BY:-- 低效且易倾斜: SELECT COUNT(DISTINCT user_id) FROM click_log; -- 改为: SELECT COUNT(1) FROM ( SELECT user_id FROM click_log GROUP BY user_id -- 先按 user_id 分组去重 ) tmp;- 内层
GROUP BY user_id可能仍有倾斜(如果 user_id 分布不均),但 Hive 的hive.groupby.skewindata=true或 Spark 的优化通常能更好地处理 Group By 倾斜。
- 内层
- 使用
approx_count_distinct: 如果业务能接受一定误差(通常误差率在 1% 以内),使用 Hive 的approx_count_distinct(基于 HyperLogLog) 或 Spark 的approx_count_distinct可以极大减少计算量和避免倾斜。
- 改为两层
-
过滤或单独处理异常值:
- 如果热点 Key 是无效数据(如 NULL、0、-1 等),且业务允许,直接在 Where 条件中过滤掉这些数据
WHERE key IS NOT NULL AND key != 0。 - 如果不允许过滤,参考“拆分处理”方法,将异常值单独处理(例如,对 NULL 值赋予随机数打散
COALESCE(key, CAST(RAND() * 1000 AS INT)))。
- 如果热点 Key 是无效数据(如 NULL、0、-1 等),且业务允许,直接在 Where 条件中过滤掉这些数据
-
调整数据模型/ETL 过程 (治本):
- 避免使用易倾斜的 Key: 在表设计时,尽量选择离散度高的字段作为 Join Key 或 Group By Key。
- 预先聚合: 在数据仓库的 ETL 层,对易发生倾斜的维度(如国家、状态)进行预聚合,生成中间汇总表,后续查询直接使用汇总表避免复杂计算。
- 数据均匀化: 在源头数据生成或 ETL 过程中,尝试对可能导致倾斜的值进行打散(如给特定用户 ID 加随机后缀,但这会影响明细查询)。
总结
解决 Hive 数据倾斜没有银弹,需要根据具体的 SQL、数据分布和倾斜原因来选择最合适的组合方案:
- 首先尝试配置优化: 开启
hive.map.aggr,hive.groupby.skewindata(针对 Group By),开启hive.optimize.skewjoin(针对 Join)。调整hive.exec.reducers.bytes.per.reducer或直接设置 Reduce 数。 - 针对 Join 倾斜:
- 首选 **Skew Join 优化 (
hive.optimize.skewjoin=true) **。 - 如果小表不大,强制使用 Map Join。
- 对于极端热点 Key,考虑 热点 Key 分离 或 加盐打散。
- 首选 **Skew Join 优化 (
- 针对 Group By 倾斜: 确保
hive.groupby.skewindata=true开启。 - 针对
COUNT(DISTINCT)倾斜: 改写为两层GROUP BY或使用approx_count_distinct。 - 分析数据: 始终先通过采样查询识别热点 Key 和倾斜程度。
- 考虑长远: 在数据建模和 ETL 设计时尽量避免引入倾斜。
处理数据倾斜需要结合监控工具(YARN UI, Tez/Spark UI, Hive/Spark日志)进行诊断,并通过实验不同方案来找到最优解。
4081



被折叠的 条评论
为什么被折叠?



