【Hadoop】MapReduce数据倾斜问题解决方案
默认情况下Map任务的数量与InputSplit数量保持一致,Map阶段的执行效率也与InputSplit数量相关,当遇到大量的小文件时我们采用SequenceFile合并成一个大文件,以此来提高运行效率(【Hadoop】MapReduce小文件问题解决方案(SequenceFile,MapFile));默认情况下只有一个Reduce任务,那么解决了map阶段的小文件问题后,Reduce阶段的运行效率就是MapReduce运行效率的短板,我们当然可以通过增加Reduce任务的数量来提高数据处理的并行度,以此提高效率,但这种处理是“治标不治本”的。若遇到数据倾斜问题,仅增加Reduce任务的数量是于事无补的。
文章目录
1. 增加Reduce任务数量(分区数量)
MapReduce默认使用哈希方法进行分区,getPatition方法相关源码为:(key.hashcode()&Inyeger.MAX_VALUE)%numReduceTask
,其中numReduceTask
默认为1,而任何数向1取余都为0,因此默认只有一个分区,又因为一个分区对应一个Reduce任务,所以只有也一个Ruduce。若要提高并行度,增加Reduce任务数,只需要修改numReduceTask数值即可(在Main函数中添加源码job.setNumReduceTasks(N);
其中N为需要指定的Reduce数量)。
2. 增加Reduce任务数量+数据打散
还有一种思路,既然有大量的数据映射到同一个分区并最终进入同一个Reduce任务处理,那么我们就在这些数据后面增加一个便于切割的随机数,让他们均匀映射到不同的分区,最终让每个Reduce处理的数据量大致相同,随后将输出再进行一次MapReduce任务,把数据切割还原并会汇总。
对统计文本单词数量这个小实践项目的代码进行修改即可实现上述操作。
实验针对的文本数据改为写入901 0000个数字5,1~10的其余数字各10000次。
2.1 数据打散执行
- MyMapper类中的map函数重写
在每个5的后面加一个下划线并拼接十以内的随机数。
@Override
protected void map(LongWritable k1, Text v1, Mapper<LongWritable, Text, Text, LongWritable>.Context context)
throws IOException, InterruptedException {
//k1表示每一行的偏移量,v1表示该行内容
//首先把每一行的单词切割出来
String key = v1.toString();
if ("5".equals(key))
key += "_" + random.nextInt(10);
Text k2 = new Text(key);
LongWritable v2 = new LongWritable(1L);
context.write(k2, v2);
}
- 设置10个Reduce任务
在main函数中增加代码:
job.setNumReduceTasks(10);
然后打包执行即可。
2.2 执行结果比对
-
十个分区:
-
数据打散+十个分区:
在YARN的界面可以很明显看到仅使用十个分区,需要的运行时间为3mins, 3sec;而进行数据打散后再使用十个分区仅需要1mins, 24sec,运行速度提升了一倍。
2.3 运行结果处理整合
由于之前进行过数据打散,改变了原始数据,现在再编写一个MapReduce任务处理之前得到的结果。第一次MapReduce的结果如下:
[root@bigData01 ~]# hdfs dfs -ls /out0002
Found 11 items
-rw-r--r-- 1 root supergroup 0 2023-01-28 22:23 /out0002/_SUCCESS
-rw-r--r-- 1 root supergroup 19 2023-01-28 22:22 /out0002/part-r-00000
-rw-r--r-- 1 root supergroup 19 2023-01-28 22:22 /out0002/part-r-00001
-rw-r--r-- 1 root supergroup 19 2023-01-28 22:22 /out0002/part-r-00002
-rw-r--r-- 1 root supergroup 19 2023-01-28 22:22 /out0002/part-r-00003
-rw-r--r-- 1 root supergroup 11 2023-01-28 22:22 /out0002/part-r-00004
-rw-r--r-- 1 root supergroup 19 2023-01-28 22:23 /out0002/part-r-00005
-rw-r--r-- 1 root supergroup 19 2023-01-28 22:23 /out0002/part-r-00006
-rw-r--r-- 1 root supergroup 19 2023-01-28 22:23 /out0002/part-r-00007
-rw-r--r-- 1 root supergroup 28 2023-01-28 22:23 /out0002/part-r-00008
-rw-r--r-- 1 root supergroup 11 2023-01-28 22:23 /out0002/part-r-00009
[root@bigData01 ~]# hdfs dfs -cat /out0002/*
1 10000
5_3 901618
2 10000
5_4 900945
3 10000
5_5 902323
4 10000
5_6 900200
5_7 899259
5_8 900541
6 10000
5_9 900792
7 10000
5_0 901125
8 10000
10 10000
5_1 900987
9 10000
5_2 902210
现在要做的就是把5_n这样的数据转化为5,并这些文件整合为一个文件。
2.3.1 Mapper代码
public static class MyMapper extends Mapper<LongWritable, Text, Text, LongWritable> {
/**
* map函数接收<k1,v1>,产生<k2,v2>
*
* @param k1
* @param v1
* @param context
* @throws IOException
* @throws InterruptedException
*/
Random random = new Random();
@Override
protected void map(LongWritable k1, Text v1, Mapper<LongWritable, Text, Text, LongWritable>.Context context)
throws IOException, InterruptedException {
//k1表示每一行的偏移量,v1表示该行内容
//首先把每一行的单词切割出来
String key = v1.toString().split("\t")[0].split("_")[0];
String value = v1.toString().split("\t")[1];
System.out.println("v1:"+v1.toString()+",key:"+key+",value:"+value);
Text k2 = new Text(key);
LongWritable v2 = new LongWritable(Long.valueOf(value));
context.write(k2, v2);
}
}
2.3.2 Reducer代码
public static class MyReducer extends Reducer<Text, LongWritable, Text, LongWritable> {
/**
* 对v2s的数据进行累加,保存v2s的和
*
* @param k2
* @param v2s
* @param context
* @throws IOException
* @throws InterruptedException
*/
@Override
protected void reduce(Text k2, Iterable<LongWritable> v2s, Reducer<Text, LongWritable, Text,
LongWritable>.Context context) throws IOException, InterruptedException {
//sum计算v2s的和
long sum = 0L;
for (LongWritable v2 : v2s) {
sum += v2.get();
}
Text k3 = k2;
LongWritable v3 = new LongWritable(sum);
context.write(k3, v3);
}
}
2.3.3 main代码
public static void main(String[] args) {
try {
if (args.length != 2) {
System.exit(100);
}
//配置项
Configuration configuration = new Configuration();
//创建一个job
Job job = Job.getInstance(configuration);
//不设置的话,集群就找不到这个类
job.setJarByClass(WordCountSkew_2.class);
//指定输入路径
FileInputFormat.setInputPaths(job, new Path(args[0]));
//指定输出路径
FileOutputFormat.setOutputPath(job, new Path(args[1]));
//指定map相关的代码
job.setMapperClass(MyMapper.class);
//指定k2的类型
job.setMapOutputKeyClass(Text.class);
//指定v2的类型
job.setOutputValueClass(LongWritable.class);
//指定reduce相关的代码
job.setReducerClass(MyReducer.class);
//指定k3的类型
job.setOutputKeyClass(Text.class);
//指定v3的类型
job.setOutputValueClass(LongWritable.class);
//提交job
job.waitForCompletion(true);
} catch (Exception e) {
e.printStackTrace();
}
}
2.3.4 执行结果
再经过MapReduce整合后得到如下结果,统计出了每个数字的出现频率。
[root@bigData01 wordCountSkew]# hdfs dfs -ls /out0006
Found 2 items
-rw-r--r-- 1 root supergroup 0 2023-01-29 16:24 /out0006/_SUCCESS
-rw-r--r-- 1 root supergroup 83 2023-01-29 16:24 /out0006/part-r-00000
[root@bigData01 wordCountSkew]# hdfs dfs -cat /out0006/part-r-00000
1 10000
10 10000
2 10000
3 10000
4 10000
5 9010000
6 10000
7 10000
8 10000
9 10000