本质:脏数据 → 数据倾斜 → OOM

业务背景:用户行为日志每日离线跑批,Hive 执行 group by user_id 统计用户行为次数。每日跑批,某天作业突然失败,Reduce 任务 OOM,绝大多数 task 很快跑完,仅 1 个 reduce 反复重试后 OOM 失败。报错日志:

java.lang.OutOfMemoryError: Java heap space
Container killed by YARN for exceeding memory limits.
AttemptID:attempt_xxxx_r_000012,多次重试失败,退出码143

区分:

  1. 单纯调大内存只是临时掩盖;
  2. 本次根因:上游埋点采集 bug 产生脏数据,大量记录 user_id 字段输出同一个异常脏 key(字符串 -99999),全部 shuffle 到同一个 reduce,把 reduce 堆内存撑爆。不是代码逻辑错误,是上游脏数据造成热点 key 倾斜,引发 reduce OOM。

一、完整排查步骤

步骤 1:YARN UI 观察现象,确认是 Reduce 侧问题

  1. 打开 ResourceManager 页面,找到失败 Job;看 Job 概览:Map 100% 全部完成,Reduce 卡在 99%,只有一个 reduce task 反复失败,其余几十个 reduce 几秒全部跑完。
  2. 打开 Counters → Map-Reduce Framework → Reduce input records
    • 正常 reduce:每个输入记录 2~5 万条。
    • 失败的 reduce12:输入记录 1200 万条,相差几百倍。👉 确认发生数据倾斜,问题出在这个 reduce。

Counter 只能看到数量,看不到具体 key

步骤 2:定位异常 Reduce 对应的 Container,下载 task 日志

  1. Job 页面找到失败的 reduce taskID r_000012,点开 Attempts,拿到 ContainerID。
  2. 跳转该 NodeManager 节点,下载 reduce task 日志(syslog/stderr)
  3. 日志特征:大量频繁 Full GC,GC 时间占比极高;大量重复打印同一个 key 值:user_id=-99999

此时定位热点脏 key:‑99999

步骤 3:回查原始 Hive 表,验证脏数据

执行 SQL 统计 key 分布,确认脏 key 的量级:

select user_id,count(*) as cnt
from dwd_user_log
group by user_id
order by cnt desc
limit 5;

输出:

表格

user_idcnt
-999991180 万
135xxxx2.3 万
136xxxx2.1 万

确认:上游埋点服务 bug,部分设备上报时用户 ID 解析失败,统一填默认值‑99999,产生千万级脏 key,shuffle 全部落到同一个 reduce,reduce 迭代器把海量 value 加载进内存,直接堆溢出 OOM。

注意:不是业务正常数据,属于脏数据

步骤 4:临时应急方案(先让任务跑过,保障调度)

方案 1:过滤脏 key(业务允许),SQL 增加 where 条件

select user_id,count(1)
from dwd_user_log
where user_id != '-99999' --过滤脏数据
group by user_id;

临时应急,任务直接跑通。

方案 2:如果业务不能直接过滤,对脏 key 做加盐打散

select
  if(user_id='-99999',concat(user_id,'_',floor(rand()*10)),user_id) as user_id_salt,
  count(1)
from dwd_user_log
group by if(user_id='-99999',concat(user_id,'_',floor(rand()*10)),user_id)

把脏 key 打散到 10 个 reduce,避免单 reduce 过载;后续再做聚合合并结果。

方案 3:开启 hive.groupby.skewindata=true,拆两轮 MR 做自动倾斜处理(消耗更多资源)。

❗不要优先只调大 reduce 内存:mapreduce.reduce.memory.mb。就算调到 8G,脏数据量继续涨,早晚还会 OOM,治标不治本。

步骤 5:根治源头(防止次日再次发生)

  1. 通知埋点开发修复采集 bug,不再生成‑99999异常 user_id。
  2. 在数仓 DWD 层增加数据清洗逻辑,过滤 / 标记该脏 key;增加监控告警:监控 user_id 分布,某一个 key 行数超过阈值就告警。

二、关键点

  1. 为什么同一个 key 千万条记录会把 reduce OOM?

MR reduce 函数接收 (key, Iterable<value>),shuffle 拉取过来该 key 全部的 value;虽然 Iterable 是迭代读取,但 shuffle 合并阶段大量数据会压入内存缓冲区;当同一个 key 千万条,内存缓冲区撑爆,就报 Java heap space OOM。

  1. 调大 reduce 内存能不能彻底解决?

不能。脏数据量持续增长,再大内存也会被打满;调内存只能临时续命,根本要处理脏数据 / 热点 key

  1. 怎么区分:是业务正常热点 key,还是脏数据热点 key?

查询原始表,看该 key 业务含义;如果是异常默认值、null、异常编码,就是脏数据;如果是真实业务对象(爆款商品、头部大用户)属于业务热点。

  1. Counter 和日志分工再回顾

Counter 看每个 reduce 输入记录数,发现倾斜现象;下载对应 Container 的 task 日志拿到真实热点 key。

三、精简版

线上遇到 Hive on MR 的 reduce OOM。现象是 map 全部跑完,大部分 reduce 快速结束,只有一个 reduce 反复重试 OOM。

首先去 YARN UI 看 job 的 Counter 中 Reduce‑input‑records,发现单个 reduce 输入千万条,远大于其他 reduce,确认倾斜。

找到失败 reduce 对应的 Container,下载 task 日志,发现大量重复 key‑99999

回查源表统计 key 分布,确认是上游埋点 bug 产生脏数据,千万条记录 user_id 等于‑99999,shuffle 全部落到同一个 reduce,造成 OOM。

应急在 SQL 过滤脏 key 先保障调度跑通;

同时通知上游修复埋点,DWD 层增加清洗逻辑和分布监控,从源头解决。

没有单纯调大 reduce 内存,因为调内存只是临时掩盖问题。

四、延伸:Spark 版本同类现象

Spark 中对应现象:Spark UI 某个 Stage,少数 task 的 Shuffle Read 几百 MB‑GB 级别,Executor 报 OOM;

排查思路:看 task 的 Shuffle Read → 找到慢 task 对应 Executor 日志 → SQL 统计 key 分布定位脏 key。

容易踩坑点

  1. 看到 OOM 就直接加大 YARN 容器内存,不去查数据分布,任务后续还会失败。
  2. 分不清:容器被 YARN kill,有两种情况:JVM 堆 OOM;物理内存超限(堆外 + 进程占用),要区分日志信息阿里云帮助...。
  3. 脏数据不只是‑99999,还有大量 null、空字符串、异常编码,都会造成热点倾斜 OOM。

更多推荐