Shuffle
MapReduce的Map阶段与Reduce阶段之间有一个Shuffle的过程,包括分区、排序等内容。数据从Map阶段出来后,会进入一个环形缓冲区(默认100M),环形缓冲区中会同时记录数据和索引,当使用了80%的时候,会进行反向写,已有的数据会进行溢写,写到文件中,在溢写之前,会进行排序,对数据的索引按照字典序进行快排。溢写文件的过程包括分区、排序、Combine、归并排序等过程,溢写完成之后,ReduceTask会主动拉取文件进行处理。
默认分区HashPartitioner
MpaReduce默认1个分区,分区规则是key的哈希值对ReduceTask的数量取余,用户没法控制哪个key存在哪个分区:
自定义Partitioner
继承Partitioner抽象类,实现getPartition方法:
自定义Partioner类的泛型跟MapTask的出参的key、value类型一致,getPartition方法返回的是整数(分区号从0开始),表示哪个分区。然后还需在job中设置Partitioner的class:
还要设置ReduceTask的个数,一般与分区数一致:
如果设置的ReduceTask的个数(1除外)小于分区数,则会报错,抛IO异常。但是ReduceTask的个数设为1是可以的,因为根据源码,分区数设为1,不会走自定义Partitioner:
如果设置的ReduceTask的个数大于分区数,也不会报错,但会产生空文件。
如果设置的ReduceTask的个数为0,则不会有ReduceTask执行,最终输出的文件仅仅是MapTask产生的输出文件。
MapReduce排序
排序是MapReduce中的一个重要的操作,MapTask和ReduceTask都会根据key进行排序,属于默认行为,不管逻辑上是否真的需要排序。默认是按照字典序排序。
自定义排序:实现WritableCompatable接口,实现其compareTo方法,根据业务逻辑需要自定义排序规则,一般是实现在自定义的可序列化的bean上。
Combiner
Combiner是MapReduce中Mapper和Reducer之外的一种组件,父类是Reducer,相当于在ReduceTask之前提前做了一些汇总工作,减轻ReduceTask的工作量,他与Reducer的区别在于Combiner是在MapTask节点运行,而ReduceTask是接受所有的MapTask的结果。应用Combiner时应确保不影响正确的业务逻辑,而且Combiner输出的key、value类型要与ReduceTask的输入的key、value类型对应。
例如不使用Combiner,会使得(a,1)、(a,1)、(a,1)这种数据想ReduceTask传三次,使用了Combiner,可以直接传(a,3)。
自定义Combiner
实现Reducer方法,实现reduce方法,然后再job中设置Combiner的class。如果Combiner的实现逻辑和自定义的Reducer逻辑一致,可以不用自定义Combiner实现类,直接在job中设置Combiner的类为自定义的Reducer类。
OutputFormat
OutputFormat是Mapreduce输出的基类,所有类型的MapReduce输出都要实现OutputFormat接口,默认实现类是TextOutputFormat,另外还有NullOutputForamt、DBOutoutFormmat实现类等。也可以自定义OutputFormat类,一般是继承FileOutputFormat类,实现wirte方法。
现在有个需求是讲输入文件中某个特殊网址筛选出来放入单独的文件中,其他网址放入另一个文件中,mapper类和reducer类已经写好,mapper类的输入输出的key、value类型分别为LongWritable、Text、Text、NullWritable类型,reducer类的输入输出的key、value类型分别为Text、NullWritable、Text、NullWritable类型,不再详细说明,关键是自定义OutputFormat如何实现。
先自定义一个类实现OutputFormat接口:
public class CustomOutputFormat extends FileOutputFormat<Text, NullWritable> {
@Override
public RecordWriter<Text, NullWritable> getRecordWriter(TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {
return new CustomRecordWriter(taskAttemptContext);
}
}
这里面只要先实现getRecordWriter方法,返回一个RecordWriter即可,而RecordWriter也需要自定义:
public class CustomRecordWriter extends RecordWriter<Text, NullWritable> {
FSDataOutputStream outputStream1;
FSDataOutputStream outputStream2;
public CustomRecordWriter(TaskAttemptContext job) {
try {
FileSystem fileSystem = FileSystem.get(job.getConfiguration());
outputStream1 = fileSystem.create(new Path("log1.log"));
outputStream2 = fileSystem.create(new Path("log2.log"));
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void write(Text text, NullWritable nullWritable) throws IOException, InterruptedException {
String string = text.toString();
if (string.contains("xxx")) {
outputStream1.writeBytes(string + "\n");
} else {
outputStream2.writeBytes(string + "\n");
}
}
@Override
public void close(TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {
IOUtils.closeStream(outputStream1);
IOUtils.closeStream(outputStream2);
}
}
这里面的构造方法需要传入一个上下文参数,即job,根据job中的配置获取文件系统,从而获取流,以便往指定文件写数据,另外就是write方法,就是具体的业务逻辑,这里是根据输入是否包含指定字符串,将内容写到不同的文件中去,最后是close方法,使用IOUtils工具类关闭流。实现完自定义OutputFormat类和自定一个RecordWriter类之后,别忘了去提交任务的时候设置自定义的OutputFormat类:
job.setOutputFormatClass(CustomOutputFormat.class);
MapTask和ReduceTask工作机制总结
MapTask工作机制:
任务提交阶段,还没进入MapTask:切片划分,提交任务到yarn(切片信息、jar包、任务信息),yarn计算MapTask个数
- read阶段:TextInputFormat(默认)读取数据(调用RecordReader的read方法)
- map阶段:进入mapper方法,业务逻辑
- collect阶段:进入环形缓冲区,默认100M,一侧存索引,一侧存数据,到达80%之后,进行反向溢写,这里的数据以分区的方式进行存储并排序
- 溢写阶段:环形缓冲区到达一定阈值之后就会进行溢写,写文件到磁盘,数据分区且区内有序
- merge阶段:溢写完之后还会对溢写文件进行归并排序
ReduceTask工作机制:
- copy阶段:主动拉取map阶段产生的文件数据
- sort阶段:对拉取来的数据进行归并排序
- reduce阶段:进入reduce方法,相同的key会进入同一个reduce方法
ReduceTask并行度
ReduceTask的个数可以手动决定,默认值为1,此时分区的概念将无意义,因为最终所有的数据都将输出到一个文件中,但若设为0,则输出文件仅为MapTask的输出。ReduceTask的个数由具体业务逻辑决定,若设置得不合适,有可能会产生数据倾斜。