当然遇到过。数据倾斜是大数据处理中最常见、最棘手的问题之一,可以说是每个大数据工程师的“必修课”。
一、什么是数据倾斜?
数据倾斜 本质上是一种数据分布严重不均的现象。在分布式计算中,理想情况下数据和工作负载应该均匀地分散在所有计算节点上,这样每个节点能在相近的时间内完成工作,从而实现高效的并行计算。
而当发生数据倾斜时,某一部分数据(通常对应某个或某几个Key)的数据量或计算复杂度远远超过了其他部分,导致少数几个计算节点承担了绝大部分的计算任务,而其他节点早早完成后却处于空闲等待状态。
这就像一条10车道的高速公路,所有车都挤在1条车道上,其他9条车道空空如也。整个系统的吞吐量和效率不再由所有节点决定,而是被这些“拖后腿”的少数节点所限制。
二、数据倾斜的表现是什么?
数据倾斜的表现非常典型,无论是在Hadoop MapReduce还是Spark任务中,通常都会出现以下一种或多种症状:
-
绝大多数Task执行极快,少数(1个或几个)Task执行极慢
- 在任务监控界面(如Spark UI、YARN ResourceManager)上,你会看到大部分Task在几分钟甚至几秒钟内就完成了,但总有那么一两个Task运行时间超长(几十分钟甚至小时),迟迟无法结束。整个作业的完成时间就取决于这几个最慢的Task。
-
Executor/Observer出现OOM(内存溢出)错误
- 由于某个Key对应的数据量异常巨大,处理这个Key的Task需要将海量数据加载到内存中,极易导致JVM堆内存溢出,任务失败。错误信息通常是
java.lang.OutOfMemoryError: Java heap space。
- 由于某个Key对应的数据量异常巨大,处理这个Key的Task需要将海量数据加载到内存中,极易导致JVM堆内存溢出,任务失败。错误信息通常是
-
某个Executor/Observer的输入数据量(Input Size / Records)远高于其他节点
- 在Spark UI的Stages详情页里,可以看到每个Task的输入数据量。正常情况应该大致均匀,而数据倾斜时,会出现个别Task的
Input Size / Records是其他Task的成百上千倍。
- 在Spark UI的Stages详情页里,可以看到每个Task的输入数据量。正常情况应该大致均匀,而数据倾斜时,会出现个别Task的
-
节点资源使用率不均
- 监控系统会显示,少数几个节点的CPU、内存、网络IO利用率持续飙高,而其他节点资源利用率很低,早早进入空闲状态。
-
任务失败后反复重试,但重试的Task依然失败
- 如果某个Task因为处理数据量过大而失败,调度器会将其重新调度到其他节点执行。但由于要处理的数据量本身没变,重试的Task很可能再次因OOM或超时而失败。
三、一个简单的例子
假设你有一个超大的用户行为日志表 user_logs,其中有一个字段是 user_id。你想统计每个用户的点击次数(一个经典的WordCount问题)。
正常情况:大多数用户的活跃度差不多,每个Reducer/Task处理几千个用户的行为记录,大家同时完成。
数据倾斜情况:存在几个“超级用户”或“爬虫/机器人”,例如:
user_id = ‘-’(由于数据清洗缺失,被赋为默认值)user_id = ‘null’(同上)user_id = ‘0’- 或者某个异常活跃的真实用户/机器人(如
user_id = ‘12345’)
这些特殊的Key可能对应着数亿甚至数十亿条记录。当进行 GROUP BY user_id 操作时,所有相同Key的数据都会被发送到同一个Reducer/Task进行处理。处理 user_id = ‘-’ 的Task将需要处理数亿条记录,而其他Task只处理几千条。最终,这个Task就成了拖垮整个作业的“罪魁祸首”。
四、常见解决方案(简要概述)
解决数据倾斜的核心思路是:“打散”倾斜的Key,将原本由一个节点处理的工作分散到多个节点上。
-
预处理:过滤和隔离
- 过滤空值:如果倾斜的Key是无意义的(如
null,-),可以直接在业务逻辑中过滤掉。 - 隔离倾斜Key:将倾斜的Key和非倾斜的Key分开处理,最后再合并结果。
- 过滤空值:如果倾斜的Key是无意义的(如
-
调整Shuffle参数(治标不治本)
- 如增加
spark.sql.shuffle.partitions(Spark)或mapreduce.job.reduces(MapReduce)的数量。有时能缓解问题,但如果某个Key的数据量本身巨大,增加分区数可能帮助不大。
- 如增加
-
两阶段聚合(局部聚合+全局聚合)
- 适用场景:聚合类操作(如
count(),sum()),但groupByKey()不直接适用(需要先转换成聚合操作)。 - 方法:
- 给每个Key加上一个随机前缀(如1到n之间的随机数),变成
(random_prefix_key, value)。 - 对加完前缀的Key进行第一次聚合(局部聚合)。
- 去掉随机前缀,对局部聚合的结果进行第二次聚合(全局聚合)。
- 给每个Key加上一个随机前缀(如1到n之间的随机数),变成
- 效果:将原本一个Key的巨大负载分散到n个不同的Key上,由n个Task先处理一部分,最后再汇总。
- 适用场景:聚合类操作(如
-
将Reduce端Join转换为Map端Join
- 适用场景:一个大表和一个小表进行JOIN操作时。
- 方法:使用
Broadcast Hash Join,直接将小表广播到所有Executor节点,避免了大Shuffle。在Spark中可以使用broadcast函数提示优化器。
-
倾斜Key的Join优化
- 适用场景:两个大表JOIN,且其中一个表有少数几个倾斜的Key。
- 方法:
- 将倾斜的Key从表A中提取出来,单独与表B的对应部分进行JOIN。这个JOIN过程可以使用上面提到的“加随机前缀”技巧来打散。
- 将不倾斜的Key部分正常进行JOIN。
- 将两部分结果合并。
-
增加JVM堆内存
- 这只是一个临时解决方案,通过增加
spark.executor.memory来防止OOM,但并不能解决计算速度慢的根本问题。
- 这只是一个临时解决方案,通过增加
总之,数据倾斜是一个需要结合具体业务和数据特点进行分析处理的问题,没有一劳永逸的解决方案,考验的是开发者的经验和问题排查能力。

982

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



