MapReduce 数据倾斜以及解决思路
1.小背景
- 在mapreduce的分布式解决框架中,数据处理主要分为2个步骤,map阶段和reduce阶段
- map阶段主要是数据转换,也就是按照预期把输入的数据进行转换,变为中间数据
- 中间数据再分发给reduce阶段作为输入数据源进行处理。
- 在这个过程中,由于数据本身按照设计的key规则划分时就不均匀,再加上使用默认的分区规则进行对key进行计算时,也有产生数据倾斜的可能(key.hashcode() % numberOfReduceTasks)
2.大背景
- 为了解决传统计算机处理数据受限于单台计算机性能上限的问题,新的数据处理思路,分布式计算被提出来。
- 单台计算机处理,提升处理效率主要就是从硬件和软件上提升。硬件提升如单CPU多核心、多CPU多核心、内存容量增加例如部分服务器主板可以加内存到521GB甚至1TB、硬盘改为高速硬盘甚至固态硬盘,硬盘Raid技术等等。
- 分布式计算则换一种思路,打破单台计算机处理性能上限的问题。程序=数据+逻辑(算法/代码),如果能够让数据做切分,让代码也做切分,让后把切分后的数据和代码同时让多台计算机处理,这样就可以做到以往一台计算机需要处理十小时的任务,使用100台计算机可能几分钟就出结果的效果。
- 但分布式计算有一个大前提,需要针对分布式计算的特点,对程序要处理的数据和程序执行代码做处理,方便让程序和数据分发到计算机集群中进行并行执行。
3.MapReduce简介
- Hadoop的mapreduce处理框架,结合hadoop自带的hdfs,就可以做到数据分布式存储,程序分布式计算的目标。
- mapreduce是一种编程框架,也可以看作是一种编程思路。就是把问题分成2个大的步骤,第一步,map阶段,就是把输入数据进行转换。转换后的数据称之为中间数据。第二步,reduce阶段,就是把map阶段产生的中间数据进行聚合处理,最后得出结果。
- 这里面有几个关键点,hdfs可以把数据进行分布式存储,例如一个200MB文件,默认就会分为0–128MB,128MB–200MB 2个数据块存储到hdfs文件系统中。每个数据块会有3个备份来保证数据安全性。这样就隐形带来一个好处,数据被切块了。
- map task处理任务,如果能够让集群中计算机分别处理一个文件的不同数据块,这样就可做到集群分布式并行计算。hdfs已经做到让数据分布式存储。剩下的就是怎样让程序分别处理不同的数据块。
- 其实很简单,就是安排一个协调者,指定某台计算机就只处理分布式文件系统中的某一块数据即可。
- map task阶段结束后,也会产生中间结果数据,这个数据没错,也是会存到分布式文件系统中的。这时候执行reduce task的计算机,也一样,处理对应数据块的数据即可。最后进行结果汇总就是最后的结果。
4.数据倾斜
- 简单示意图如下:

- 如图所示,数据倾斜主要就是在中间数据阶段,由于对数据分区处理不均匀,导致有些文件中数据量会非常大,有些文件数据量会少很多。这样一来,后续进行reduce task的计算机节点的任务量就会有很大差异。旱涝不同,雨露不均。
5.解决思路
- 加内存和提升CPU性能,数据倾斜导致最明显一个问题就是个别或者部分计算机节点数据量处理过大,导致整体处理速度变慢。提升集群中计算机硬件水平,可以缓解这个问题,就是比较费钱。。。
- 提升并行度,可以看到,我们如果让中间数据文件数量增多,这样数据就会分不到更多文件,对于单个文件数据量过大问题就能得到较好缓解。hadoop中的中间文件数量默认和reduce task数量一致,所以增加reduce task数量可以缓解数据倾斜问题。不过增加并行度其实也受限于集群中计算机节点数量,除非愿意增加计算机物理节点数,这又回到费钱的方式上
- 既然数据倾斜是由中间数据分区导致的,那就干脆不执行reduce阶段,直接把数据输出到文件中,这样就不需要进行中间的数据分区操作。实际业务场景就是,启动一个mapreduce,但只是利用map阶段针对数据做清洗和转换。
- 既然中间文件数量受限于reduce task任务数,如果设置为1,文件就一个,也不存在数据倾斜问题,数据都写入到一个文件中去了。但这样不符合分布式计算的准则,会极大降低处理效率。
- 既然分区是由针对key的处理而产生的,我让key变得均匀散布,也就是把key打散。常见操作就是加一个随机数前缀或者后缀,这样一来,以往相同的key,由于新增了随机数前后缀,变得不一样,中间数据分区时,自然而然就会分到不同文件中去,数据倾斜就被很大缓解了。不过带来另外一个问题,需要对key打散后reduce聚合后数据再做一次mapreduce操作来得到最后数据。这时候可能还会有数据倾斜,但由于已经聚合过一次,数据量会降低很多,数据倾斜也会由较大缓解。
- 自定义shuffle分区算法,也就是换一种算法。既然默认的分区算法有问题,那我换一个算法来对数据分区即可。生活中类比来看就是,如果某方面大家都一样,那我就换一个标准和角度来重新划分,一定可以把物品相对均匀的分成相同多份。
6.代码演示
6.1输入数据源:
a b a b a c hu jn jgh op a a a a b c c c c a a a a a a a c c c c c c c c c c c a a a a
a b a b a c hu jn jgh op a a a a b c c c c a a a a a a a c c c c c c c c c c c a a a a
a b a b a c hu jn jgh op a a a a b c c c c a a a a a a a c c c c c c c c c c c a a a a
a b a b a c hu jn jgh op a a a a b c c c c a a a a a a a c c c c c c c c c c c a a a a
a b a b a c hu jn jgh op a a a a b c c c c a a a a a a a c c c c c c c c c c c a a a a
a b a b a c hu jn jgh op a a a a b c c c c a a a a a a a c c c c c c c c c c c a a a a
a b a b a c hu jn jgh op a a a a b c c c c a a a a a a a c c c c c c c c c c c a a a a
a b a b a c hu jn jgh op a a a a b c c c c a a a a a a a c c c c c c c c c c c a a a a
a b a b a c hu jn jgh op a a a a b c c c c a a a a a a a c c c c c c c c c c c a a a a
a b a b a c hu jn jgh op a a a a b c c c c a a a a a a a c c c c c c c c c c c a a a a
a b a b a c hu jn jgh op a a a a b c c c c a a a a a a a c c c c c c c c c c c a a a a
a b a b a c hu jn jgh op a a a a b c c c c a a a a a a a c c c c c c c c c c c a a a a
a b a b a c hu jn jgh op a a a a b c c c c a a a a a a a c c c c c c c c c c c a a a a
a b a b a c hu jn jgh op a a a a b c c c c a a a a a a a c c c c c c c c c c c a a a a
a b a b a c hu jn jgh op a a a a b c c c c a a a a a a a c c c c c c c c c c c a a a a
a b a b a c hu jn jgh op a a a a b c c c c a a a a a a a c c c c c c c c c c c a a a a
a b a b a c hu jn jgh op a a a a b c c c c a a a a a a a c c c c c c c c c c c a a a a
a b a b a c hu jn jgh op a a a a b c c c c a a a a a a a c c c c c c c c c c c a a a a
a b a b a c hu jn jgh op a a a a b c c c c a a a a a a a c c c c c c c c c c c a a a a
a b a b a c hu jn jgh op a a a a b c c c c a a a a a a a c c c c c c c c c c c a a a a
a b a b a c hu jn jgh op a a a a b c c c c a a a a a a a c c c c c c c c c c c a a a a
a b a b a c hu jn jgh op a a a a b c c c c a a a a a a a c c c c c c c c c c c a a a a
a b a b a c hu jn jgh op a a a a b c c c c a a a a a a a c c c c c c c c c c c a a a a
a b a b a c hu jn jgh op a a a a b c c c c a a a a a a a c c c c c c c c c c c a a a a
a b a b a c hu jn jgh op a a a a b c c c c a a a a a a a c c c c c c c c c c c a a a a
a b a b a c hu jn jgh op a a a a b c c c c a a a a a a a c c c c c c c c c c c a a a a
a b a b a c hu jn jgh op a a a a b c c c c a a a a a a a c c c c c c c c c c c a a a a
a b a b a c hu jn jgh op a a a a b c c c c a a a a a a a c c c c c c c c c c c a a a a
a b a b a c hu jn jgh op a a a a b c c c c a a a a a a a c c c c c c c c c c c a a a a
a b a b a c hu jn jgh op a a a a b c c c c a a a a a a a c c c c c c c c c c c a a a a
a b a b a c hu jn jgh op a a a a b c c c c a a a a a a a c c c c c c c c c c c a a a a
a b a b a c hu jn jgh op a a a a b c c c c a a a a a a a c c c c c c c c c c c a a a a
a b a b a c hu jn jgh op a a a a b c c c c a a a a a a a c c c c c c c c c c c a a a a
a b a b a c hu jn jgh op a a a a b c c c c a a a a a a a c c c c c c c c c c c a a a a
a b a b a c hu jn jgh op a a a a b c c c c a a a a a a a c c c c c c c c c c c a a a a
a b a b a c hu jn jgh op a a a a b c c c c a a a a a a a c c c c c c c c c c c a a a a
a b a b a c hu jn jgh op a a a a b c c c c a a a a a a a c c c c c c c c c c c a a a a
a b a b a c hu jn jgh op a a a a b c c c c a a a a a a a c c c c c c c c c c c a a a a
a b a b a c hu jn jgh op a a a a b c c c c a a a a a a a c c c c c c c c c c c a a a a
a b a b a c hu jn jgh op a a a a b c c c c a a a a a a a c c c c c c c c c c c a a a a
a b a b a c hu jn jgh op a a a a b c c c c a a a a a a a c c c c c c c c c c c a a a a
a b a b a c hu jn jgh op a a a a b c c c c a a a a a a a c c c c c c c c c c c a a a a
a b a b a c hu jn jgh op a a a a b c c c c a a a a a a a c c c c c c c c c c c a a a a
6.2不做key打散的mapreduce代码
- 代码
package com.doit.hadoop.skew;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
/**
* @author hulc
* @slogan: just do it
* @date 2020/8/20 22:48
*/
public class DataSkewDriver {
public static void main(String[] args) {
// 配置文件
Configuration conf = new Configuration();
conf.set("mapreduce.framework.name", "local");
// job 注意由于引入了配置文件,需要设置为local模式运行
try {
Job skew = Job.getInstance(conf, "skew");
// mapper 和reducer类
skew.setMapperClass(SkewMapper.class);
skew.setReducerClass(SkewReducer.class);
// map输出的key value
skew.setMapOutputKeyClass(Text.class);
skew.setMapOutputValueClass(IntWritable.class);
// reduce 输出的key 和value
skew.setOutputKeyClass(Text.class);
skew.setOutputValueClass(IntWritable.class);
// 输入和输出数据源 E:\DOITLearning\8.Hadoop\mrdata\flow\input\dataskew.txt
FileInputFormat.setInputPaths(skew, new Path("E:\\DOITLearning\\8.Hadoop\\mrdata\\flow\\input\\dataskew.txt"));
FileOutputFormat.setOutputPath(skew, new Path("E:\\DOITLearning\\8.Hadoop\\mrdata\\flow\\skew_output1"));
// reduce 任务数
skew.setNumReduceTasks(2);
// 启动任务
boolean b = skew.waitForCompletion(true);
if(b) {
System.out.println("success");
} else {
System.out.println("failed");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
class SkewMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
IntWritable mapValue = new IntWritable(1);
Text mapKey = new Text();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
// 简单的统计单词个数
String line = value.toString();
String[] split = line.split("\\s+");
for (String s : split) {
mapKey.set(s);
context.write(mapKey, mapValue);
}
}
}
class SkewReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
IntWritable reduceValue = new IntWritable();
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
// 聚合统计单词个数
int count = 0;
for (IntWritable value : values) {
count ++;
}
reduceValue.set(count);
context.write(key, reduceValue);
}
}
- 输出结果
从结果看,数据倾斜其实比较严重,单词a和c数量过多,这样依赖,处理这些数据的reduce task的节点任务就比另外一个要重一些。主要看单词数据量对比,是7 -8倍的处理数据量的差距

6.3 增加reduce task并行度的结果
- 代码
package com.doit.hadoop.skew;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
/**
* @author hulc
* @slogan: just do it
* @date 2020/8/20 22:48
*/
public class DataSkewDriver {
public static void main(String[] args) {
// 配置文件
Configuration conf = new Configuration();
conf.set("mapreduce.framework.name", "local");
// job 注意由于引入了配置文件,需要设置为local模式运行
try {
Job skew = Job.getInstance(conf, "skew");
// mapper 和reducer类
skew.setMapperClass(SkewMapper.class);
skew.setReducerClass(SkewReducer.class);
// map输出的key value
skew.setMapOutputKeyClass(Text.class);
skew.setMapOutputValueClass(IntWritable.class);
// reduce 输出的key 和value
skew.setOutputKeyClass(Text.class);
skew.setOutputValueClass(IntWritable.class);
// 输入和输出数据源 E:\DOITLearning\8.Hadoop\mrdata\flow\input\dataskew.txt
FileInputFormat.setInputPaths(skew, new Path("E:\\DOITLearning\\8.Hadoop\\mrdata\\flow\\input\\dataskew.txt"));
FileOutputFormat.setOutputPath(skew, new Path("E:\\DOITLearning\\8.Hadoop\\mrdata\\flow\\skew_output2"));
// reduce 任务数
skew.setNumReduceTasks(5);
// 启动任务
boolean b = skew.waitForCompletion(true);
if(b) {
System.out.println("success");
} else {
System.out.println("failed");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
class SkewMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
IntWritable mapValue = new IntWritable(1);
Text mapKey = new Text();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
// 简单的统计单词个数
String line = value.toString();
String[] split = line.split("\\s+");
for (String s : split) {
mapKey.set(s);
context.write(mapKey, mapValue);
}
}
}
class SkewReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
IntWritable reduceValue = new IntWritable();
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
// 聚合统计单词个数
int count = 0;
for (IntWritable value : values) {
count ++;
}
reduceValue.set(count);
context.write(key, reduceValue);
}
}
- 运行结果
注意这里有一个文件是空的,还记得之前说的,中间数据分区时,是根据reducetask任务数。key.hashcode() % 5,如果这时候这些key的hashcode模除5没有等于1的,中间文件对应1这个文件夹就是空的,自然而然,reduce task处理这个文件得出的结果也会是空的。

6.3 对key进行打散处理(随机数)
- 代码,这里的reducetask设置为2,和初始时的代码一样,注意并没有设置为5,单一变量,方便做对比实验。
package com.doit.hadoop.skew;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
import java.util.Random;
/**
* @author hulc
* @slogan: just do it
* @date 2020/8/20 22:48
*/
public class DataSkewDriver {
public static void main(String[] args) {
// 配置文件
Configuration conf = new Configuration();
conf.set("mapreduce.framework.name", "local");
// job 注意由于引入了配置文件,需要设置为local模式运行
try {
Job skew = Job.getInstance(conf, "skew");
// mapper 和reducer类
skew.setMapperClass(SkewMapper.class);
skew.setReducerClass(SkewReducer.class);
// map输出的key value
skew.setMapOutputKeyClass(Text.class);
skew.setMapOutputValueClass(IntWritable.class);
// reduce 输出的key 和value
skew.setOutputKeyClass(Text.class);
skew.setOutputValueClass(IntWritable.class);
// 输入和输出数据源 E:\DOITLearning\8.Hadoop\mrdata\flow\input\dataskew.txt
FileInputFormat.setInputPaths(skew, new Path("E:\\DOITLearning\\8.Hadoop\\mrdata\\flow\\input\\dataskew.txt"));
FileOutputFormat.setOutputPath(skew, new Path("E:\\DOITLearning\\8.Hadoop\\mrdata\\flow\\skew_output3"));
// reduce 任务数
skew.setNumReduceTasks(2);
// 启动任务
boolean b = skew.waitForCompletion(true);
if(b) {
System.out.println("success");
} else {
System.out.println("failed");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
class SkewMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
IntWritable mapValue = new IntWritable(1);
Text mapKey = new Text();
Random random = new Random();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
// 简单的统计单词个数
String line = value.toString();
String[] split = line.split("\\s+");
for (String s : split) {
// 打散key时,为了让数据更加均匀,一般都是取reducetask的倍数进行随机数生成并拼接。这样打散后数据分配到各个区中的概率会相对平均一些
mapKey.set(s+random.nextInt(4));
context.write(mapKey, mapValue);
}
}
}
class SkewReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
IntWritable reduceValue = new IntWritable();
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
// 聚合统计单词个数
int count = 0;
for (IntWritable value : values) {
count ++;
}
reduceValue.set(count);
context.write(key, reduceValue);
}
}
- 数据结果,注意这里来看,单词a作为key,加了随机数后缀后,确实被打散到了不同的文件中去,2个输出结果文件现在的数量看起来要均匀很多。

- 注意key打散之后,需要对打散后的数据做二次mapreduce聚合处理,但这个时候数据倾斜已经有了较大缓解了。
总而言之,数据倾斜的来源是为了方便分布式计算而对中间数据做切分,但切分代码并不是所有场景下都能相对均匀切分,这时候就会出现多分数据,但有些数据多,有些数据少。这样后续的计算几点处理数据时,运行负载就会有差异,所以需要想办法进行重新划分,其实就是负载均衡的一种体现
5218



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



