MapReduce之Shuffle 三个阶段详解
1、 shuffle的基本概念
MapReduce确保每个reducer的输入都按key进行排序,系统执行排序的过程——将map输出作为输入传给reducer——称之为shuffle。可以将其理解为从map产生输出到reduce的消化输入的整个过程。
2、Shuffle 三个阶段分别
shuffle就是combine,partition,combine的组合
第一个是 map端的combine,是在map本地把同key的放在一起成列表 (Combiner 阶段)
第二个是 partition分割,把键值对按照key对应分配到reduce (Copy phase )
第三个是 reduce端的combine,把同key的再合并得到最后的reduce输入( Sort phase 应该为合并阶段 merge,因为排序是在map进行的)
shuffle阶段运行图:
说明:
1)Combiner 可做看local reducer
合并相同的key 对应的value (wordcount 例子)
通常与Reducer 逻辑一样
好处
减少Map Task 输出数据量(磁盘IO )
减少Reduce-Map 网络传输数据量( 网络IO)
如何正确使用
结果可叠加
Sum(YES!) ,Average (NO!)
2) Partitioner 决定了Map Task 输出的每条数据
交给哪个Reduce Task 处理
默认实现:hash(key) mod R (对应hashCode取模)
R 是Reduce Task 数目
允许用户自定义
很多情况需自定义Partitioner继承HashPartitioner类即可
3、 关于Combiner 与 Partitioner的实例:
首先准备测试数据:
实现代码:
import java.io.IOException; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.LongWritable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Counter; 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.input.TextInputFormat; import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat; import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat; import org.apache.hadoop.mapreduce.lib.partition.HashPartitioner; public class WordCountShuffle { public static void main(String[] args) throws Exception { Configuration conf = new Configuration(); Job job = new Job(conf, "WordCountShuffle"); job.setJarByClass(WordCountShuffle.class); // 打包在集群中运行必须设置主类的 class if (args == null || args.length == 0) { args = new String[2]; args[0] = "hdfs://hadoop-master.ganymede:9000/temp/input/hello"; args[1] = "hdfs://hadoop-master.ganymede:9000/temp/output"; } FileSystem fs = FileSystem.get(conf); fs.delete(new Path(args[1]), true); // 删除输出路径 FileInputFormat.setInputPaths(job, new Path(args[0])); job.setInputFormatClass(TextInputFormat.class); job.setMapperClass(WordCountShuffleMapper.class); job.setMapOutputKeyClass(Text.class); job.setMapOutputValueClass(LongWritable.class); job.setCombinerClass(MyCombiner.class); job.setPartitionerClass(HelloPartitioner.class); job.setNumReduceTasks(2); job.setReducerClass(WordCountShuffleReduce.class); job.setOutputKeyClass(Text.class); job.setOutputValueClass(LongWritable.class); FileOutputFormat.setOutputPath(job, new Path(args[1])); job.setOutputFormatClass(TextOutputFormat.class); boolean isSuccessed = job.waitForCompletion(true); System.exit(isSuccessed ? 0 : 1); } static class WordCountShuffleMapper extends Mapper<LongWritable, Text, Text, LongWritable> { @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String line = value.toString(); // 自定义计数器 Counter helloCounter = context.getCounter("Sensitive Words ", "hello"); if (line.contains("hello")) { // 记录敏感词出现在一行中 helloCounter.increment(1L); } String[] splited = line.split(" "); for (String word : splited) { System.out.println("Mapper 输出 <" + word + " , " + 1 + ">"); context.write(new Text(word), new LongWritable(1)); } }; } static class WordCountShuffleReduce extends Reducer<Text, LongWritable, Text, LongWritable> { @Override protected void reduce(Text key, Iterable<LongWritable> values, Context context) throws IOException, InterruptedException { // 显示次数表示Combiner函数被调用了多少, 表示k2有多少个分组 System.out.println("reduce 输入分组 <" + key + " , ...>"); long count = 0l; for (LongWritable v2 : values) { count += v2.get(); // 显示次数表示输入的键值对数量 System.out.println("reduce 输入键值对<" + key + " , " + v2.get() + ">"); } context.write(key, new LongWritable(count)); // 显示次数表示输入的键值对数量 System.out.println("reduce 输出键值对 <" + key + " , " + count + ">"); }; } /** * 自定义Comiber * @author Ganymede * */ static class MyCombiner extends Reducer<Text, LongWritable, Text, LongWritable> { @Override protected void reduce(Text key, Iterable<LongWritable> values, Context context) throws IOException, InterruptedException { // 显示次数表示Combiner函数被调用了多少, 表示k2有多少个分组 System.out.println("Combiner 输入分组 <" + key + " , ...>"); long count = 0l; for (LongWritable v2 : values) { count += v2.get(); // 显示次数表示输入的键值对数量 System.out.println("Combiner 输入键值对<" + key + " , " + v2.get() + ">"); } context.write(key, new LongWritable(count)); // 显示次数表示输入的键值对数量 System.out.println("Combiner 输出键值对 <" + key + " , " + count + ">"); }; } /** * 自定义Partitioner,对热点key进行分Partition来处理,分发到不同的reduce中 * @author Ganymede * */ static class HelloPartitioner extends HashPartitioner<Text, LongWritable> { @Override public int getPartition(Text key, LongWritable value, int numReduceTasks) { System.out.println("分区操作: " + key); return (key.toString().contains("hello")) ? 0 : 1; } } }
hadoop jar hadoop-Project.jar com.ganymede.hadoop.shuffle.WordCountShuffle /temp/input /temp/output1
4、作业运行分析
1 ) map端,可以看出map端即做了Sort 与 Combiner ,通过partition 分发数据到reduce端
说明: map输出文件位于运行map任务tasktracker的本地磁盘。
2 ) reduce端,对map端发来的数据进行 merge合并
根据自定义的partitions应该分为两个reduce来处理
a) 一个专门处理hello的key,对于大数据量中某些有热点key的mapreduce常用到
b) 另一个处理其他的key,非热点key
说明: 在reduce阶段,对已排序输出中每个键都要调用 reduce函数,此阶段的输出直接写到输出文件系统,如HDFS.