数据倾斜:主要就是在处理MR任务的时候,某个reduce的数据处理量比另外一些的reduce的数据量要大得多,其他reduce几乎不处理,这样的现象就是数据倾斜。
官方解释:数据倾斜指的是在数据处理过程中,由于某些键的分布极度不均匀,导致某些节点处理的数据量显著多于其他节点。这种情况会引发性能瓶颈,阻碍任务的并行执行,增加作业的整体执行时间。在Hadoop的MapReduce作业中,数据倾斜尤为明显,因为它会导致某些Reduce任务处理的数据量远大于其他任务,从而造成集群整体处理效率低下的问题。
这里比如有一个文本数据,里面内容全是:hadoop, hadoop, hadoop,hadoop ....,假设有800万条数据,这样更容易显示数据倾斜的效果,里面都是同样的单词,默认的hash取余分区的方法,明显不太适合,所以我们要自定义分区,重写分区方法。以及设置多个reduce,这里我设置为3,主要就是对数据倾斜的key进行一个增加后缀的方法,以及在Map阶段就增加后缀,实现过程是将每个hadoop都进行增加后缀,刚开始会全部默认存放到第一个分区里(0分区),然后写到分区后,自定义分区方法SkewPartitioner就会对里面的数据进行分析,如果后缀是1就分到1区里面,一共就0、1、2三个分区,以此来解决数据倾斜的问题。
注意:在Job端进行自定义分区器的设置:job,setPartitionerClass(SkewPartitioner.class)
具体代码如下:
package com.shujia.mr;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
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.Partitioner;
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;
public class Demo05SkewDataMR {
public static class MyMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, IntWritable>.Context context) throws IOException, InterruptedException {
String line = value.toString();
// 将每一行数据按照逗号/空格进行切分
for (String word : line.split("[,\\s]")) {
// 使用context.write将数据发送到下游
// 将每个单词变成 单词,1 形式
// 对数据倾斜的Key加上随机后缀
if ("hadoop".equals(word)) {
// 随机生成 0 1 2
int prefix = (int) (Math.random() * 3);
context.write(new Text(word + "_" + prefix), new IntWritable(1));
} else {
context.write(new Text(word), new IntWritable(1));
}
}
}
}
public static class MyReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Reducer<Text, IntWritable, Text, IntWritable>.Context context) throws IOException, InterruptedException {
// 统计每个单词的数量
int cnt = 0;
for (IntWritable value : values) {
cnt = cnt + value.get();
}
context.write(key, new IntWritable(cnt));
}
}
// Driver端:组装(调度)及配置任务
// 可以通过args接收参数
// 本任务接收两个参数:输入路径、输出路径
public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
Configuration conf = new Configuration();
// 创建Job
Job job = Job.getInstance(conf);
// 配置任务
job.setJobName("Demo05SkewDataMR");
job.setJarByClass(Demo05SkewDataMR.class);
// 设置自定义分区器
job.setPartitionerClass(SkewPartitioner.class);
// 手动设置Reduce的数量
// 最终输出到HDFS的文件数量等于Reduce的数量
job.setNumReduceTasks(3);
// 配置Map端
job.setMapperClass(MyMapper.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
// 配置Reduce端
job.setReducerClass(MyReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
// 验证args的长度
if (args.length != 2) {
System.out.println("请传入输入输出目录!");
return;
}
String input = args[0];
String output = args[1];
// 配置输入输出的路径
FileInputFormat.addInputPath(job, new Path(input));
Path ouputPath = new Path(output);
// 通过FileSystem来实现覆盖写入
FileSystem fs = FileSystem.get(conf);
if (fs.exists(ouputPath)) {
fs.delete(ouputPath, true);
}
// 该目录不能存在,会自动创建,如果已存在则会直接报错
FileOutputFormat.setOutputPath(job, ouputPath);
// 启动任务
// 等待任务的完成
job.waitForCompletion(true);
}
}
// 自定义分区:在Map阶段给key加上随机后缀,基于后缀返回不同的分区编号
class SkewPartitioner extends Partitioner<Text, IntWritable> {
@Override
public int getPartition(Text text, IntWritable intWritable, int numPartitions) {
String key = text.toString();
int partitions = 0;
// 只对数据倾斜的key做特殊处理
if ("hadoop".equals(key.split("_")[0])) {
switch (key) {
// case "hadoop_0":
// partitions = 0;
// break;
case "hadoop_1":
partitions = 1;
break;
case "hadoop_2":
partitions = 2;
break;
}
} else {
// 正常的key还是按照默认的Hash取余进行分区
partitions = (key.hashCode() & Integer.MAX_VALUE) % numPartitions;
}
return partitions;
}
}