一:MapTask工作流程
- 简介
- 详细流程如下
⑴Read阶段:
Map Task 通过用户编写的 RecordReader,从输入 InputSplit 中解析出一个个 key/value。
⑵Map 阶段:
该节点主要是将解析出的 key/value 交给用户编写 map()函数处理,并产生一系列新的 key/value。
⑶Collect 收集阶段:
在用户编写 map()函数中,当数据处理完成后,一般会调用OutputCollector.collect()输出结果。
在该函数内部,它会将生成的 key/value 分区(调用Partitioner),并写入一个环形内存缓冲区中。
⑷Spill 阶段:
即“溢写”,当环形缓冲区满后,MapReduce 会将数据写到本地磁盘上,生成一个临时文件。
需要注意的是,将数据写入本地磁盘之前,先要对数据进行一次本地排序,并在必要时对数据进
行合并、压缩等操作。
溢写阶段详情:
a:利用快速排序算法对缓存区内的数据进行排序,排序方式是,先按照分区编号partition
进行排序,然后按照 key 进行排序。这样,经过排序后,数据以分区为单位聚集在一起,
且同一分区内所有数据按照 key 有序。
b:按照分区编号由小到大依次将每个分区中的数据写入任务工作目录下的临时文件
output/spillN.out(N 表示当前溢写次数)中。如果用户设置了 Combiner,则写入文件之
前,对每个分区中的数据进行一次聚集操作。
c:将分区数据的元信息写到内存索引数据结构 SpillRecord 中,其中每个分区的元信息包括
在临时文件中的偏移量、压缩前数据大小和压缩后数据大小。如果当前内存索引大小超过
1MB,则将内存索引写到文件 output/spillN.out.index 中。
⑸Combine 阶段:
当所有数据处理完成后,MapTask 对所有临时文件进行一次合并以确保最终只会生成一个数据文件。
当所有数据处理完后,MapTask 会将所有临时文件合并成一个大文件,并保存到文件output/file.out 中,
同时生成相应的索引文件 output/file.out.index。
在进行文件合并过程中,MapTask 以分区为单位进行合并。对于某个分区,它将采用多轮递归合并的
方式。每轮合并 io.sort.factor(默认 100)个文件,并将产生的文件重新加入待合并列表中,对文件排序后,
重复以上过程,直到最终得到一个大文件。
让每个 MapTask 最终只生成一个数据文件,可避免同时打开大量文件和同时读取大量小文件产生的随机
读取带来的开销。
二:MapReduce 工作流程
- 流程示意图如下:
- 流程详解如下:
⑴maptask 收集我们的 map()方法输出的 kv 对,放到内存缓冲区中
⑵从内存缓冲区不断溢出本地磁盘文件,可能会溢出多个文件
⑶多个溢出文件会被合并成大的溢出文件
⑷在溢出过程中,及合并的过程中,都要调用 partitioner 进行分区和针对 key 进行排序
⑸reducetask 根据自己的分区号,去各个 maptask 机器上取相应的结果分区数据 。
⑹reducetask 会取到同一个分区的来自不同 maptask 的结果文件,reducetask 会将这些
文件再进行合并(归并排序)
⑺合并成大文件后,shuffle 的过程也就结束了,后面进入 reducetask 的逻辑运算过程
(从文件中取出一个一个的键值对 group,调用用户自定义的 reduce()方法)
注意:
Shuffle 中的缓冲区大小会影响到 mapreduce 程序的执行效率,原则上说,缓冲区越大,
磁盘 io 的次数越少,执行速度就越快。缓冲区的大小可以通过参数调整,参数:io.sort.mb 默认 100M。
三:Combiner 合并
- 简介
- 案例:我们在wordcount案例中使用Conbiner
⑴数据准备
⑵使用原本的wordcount代码看看日志
⑶自定义一个 combiner 继承 Reducer,重写 reduce 方法
⑷在 job 驱动类中设置:
⑸效果:
⑹其实我们可以发现Combiner的代码和Reducer的代码一样,只不过执行的位置不同:
所以我们直接在Driver中将Combiner指定reducer也可以:
四:辅助 排序和二次排序案例(GroupingComparator)
- 需求:
- 数据准备:
- 思路分析
⑴在Mapper中处理的事情:
a:获取每一行输入的数据
b:对数据进行切割,只需要订单号,金额
c:将切割好的数据分装到自定义的bean当中,并且对订单号以及金额进行排序,都是从大到小。
因为在后面定义分组的时候,只会传递一个key给reducer,这时候我们取最大金额的订单。
⑵自定义分区,从mapper端传递过来的数据,只要订单号一样我们就分到一个分区之中,最后
3中编号的订单分到3个分区文件中。
⑶自定义groupingcomparator分组,获取相同订单号的数据,并且取第一个订单号数据发送给reducer.
⑷最后reducer分别把不同订单号的第一条金额最大的数据写出。 - 代码实现如下:
⑴自定义bean,并且实现订单号以及金额排序
package com.kgf.mapreduce.order; import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; import org.apache.hadoop.io.WritableComparable; /*** * 序列化排序自定义的bean对象 * @author KGF * */ public class OrderBean implements WritableComparable<OrderBean>{ /**订单号**/ private int orderId; /**价格**/ private double price; public OrderBean(int orderId, double price) { super(); this.orderId = orderId; this.price = price; } public OrderBean() { super(); } /*** * 反序列化操作 */ @Override public void readFields(DataInput input) throws IOException { this.orderId = input.readInt(); this.price = input.readDouble(); } /*** * 序列化操作 */ @Override public void write(DataOutput output) throws IOException { output.writeInt(orderId); output.writeDouble(price); } public int getOrderId() { return orderId; } public void setOrderId(int orderId) { this.orderId = orderId; } public double getPrice() { return price; } public void setPrice(double price) { this.price = price; } @Override public String toString() { return orderId + "\t" + price; } @Override public int compareTo(OrderBean o) { int result = 0; if(this.orderId>o.getOrderId()) { result = 1; }else if(this.orderId<o.getOrderId()) { result = -1; }else { if(this.price>o.getPrice()) { result = -1; }else if(this.price<o.getPrice()) { result = 1; } } return result; } }
⑵自定义OrderMapper对数据进行切割
package com.kgf.mapreduce.order; import java.io.IOException; import org.apache.hadoop.io.LongWritable; import org.apache.hadoop.io.NullWritable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Mapper; /*** * 继承Mapper接口,对读取的文件数据进行切割 * @author KGF * */ public class OrderMapper extends Mapper<LongWritable, Text, OrderBean, NullWritable>{ OrderBean k = new OrderBean(); @Override protected void map(LongWritable key, Text value,Context context) throws IOException, InterruptedException { System.out.println("===========OrderMapper BEGIN============="); //获取一行数据 String line = value.toString(); //切割数据 String[] fields = line.split("\t"); System.out.println("[fields:]"+fields); //封装数据对象 k.setOrderId(Integer.valueOf(fields[0])); k.setPrice(Double.parseDouble(fields[2])); //输出 context.write(k, NullWritable.get()); System.out.println("===========OrderMapper END============="); } }
⑶自定义分区,将不同订单号的数据写入到不同分区
package com.kgf.mapreduce.order; import org.apache.hadoop.io.NullWritable; import org.apache.hadoop.mapreduce.Partitioner; /*** * 继承分区函数,分为多个区 * @author KGF * */ public class OrderPartitioner extends Partitioner<OrderBean,NullWritable>{ /** * 注意:这个numberPartitions就是Driver中job.setNumReduceTasks(3); */ @Override public int getPartition(OrderBean key, NullWritable value, int numberPartitions) { return (key.getOrderId() & Integer.MAX_VALUE) % numberPartitions; } }
⑷自定义分组,将相同订单号的第一条金额最大的数据写入到reducer中
package com.kgf.mapreduce.order; import org.apache.hadoop.io.WritableComparable; import org.apache.hadoop.io.WritableComparator; /*** * 自定义GroupingComparator进行分组,WritableComparator是一个类 这个类是用于mapreduce编程模型中的比较 排序 . * mapreduce中有两次排序 一次是 在环形缓冲区域之中进行分区 排序,还有一次是数据在reduce端获取文件之后进行分组 * 它是用来给Key分组的, * @author kgf * */ public class OrderGroupingComparator extends WritableComparator{ /*** * 无参构造子 必须调用父类的构造子 不然会报空指针 未初始化 buffer */ public OrderGroupingComparator() { super(OrderBean.class,true); } /** * 我们这里通过比较orderId分组,相同的一组 */ @SuppressWarnings("rawtypes") @Override public int compare(WritableComparable a, WritableComparable b) { System.out.println("======OrderGroupingComparator begin======="); OrderBean aBean = (OrderBean) a; OrderBean bBean = (OrderBean) b; System.out.println("[aBean:]"+aBean); System.out.println("[bBean:]"+bBean); int result = 0; if(bBean.getOrderId()>aBean.getOrderId()) { result = 1; }else if(bBean.getOrderId()<aBean.getOrderId()) { result = -1; }else { result = 0; } System.out.println("======OrderGroupingComparator end======="); return result; } }
⑸自定义reducer,写出不同订单号的金额最大的数据
package com.kgf.mapreduce.order; import java.io.IOException; import org.apache.hadoop.io.NullWritable; import org.apache.hadoop.mapreduce.Reducer; /*** * * @author KGF * */ public class OrderReducer extends Reducer<OrderBean, NullWritable, OrderBean, NullWritable>{ @Override protected void reduce(OrderBean key, Iterable<NullWritable> value,Context context) throws IOException, InterruptedException { System.out.println("=========OrderReducer=====begin========="); context.write(key, NullWritable.get()); System.out.println("=========OrderReducer=====END========="); } }
⑹OrderDriver启动服务类
package com.kgf.mapreduce.order; import java.io.IOException; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.NullWritable; import org.apache.hadoop.mapreduce.Job; import org.apache.hadoop.mapreduce.lib.input.FileInputFormat; import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat; public class OrderDriver { public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException { //1:获取job配置信息 Configuration conf = new Configuration(); Job job = Job.getInstance(conf); //2:设置jar包加载路径 job.setJarByClass(OrderDriver.class); //3:加载自定义的mapper和reducer job.setMapperClass(OrderMapper.class); job.setReducerClass(OrderReducer.class); //4:设置mapper的输出类型 job.setMapOutputKeyClass(OrderBean.class); job.setMapOutputValueClass(NullWritable.class); //5:设置reducer的输出类型 job.setOutputKeyClass(OrderBean.class); job.setOutputValueClass(NullWritable.class); //6:设置文件输入和输出路径 FileInputFormat.setInputPaths(job, new Path(args[0])); FileOutputFormat.setOutputPath(job,new Path(args[1])); //8:设置分区 job.setPartitionerClass(OrderPartitioner.class); job.setNumReduceTasks(3); //9:设置自定义分组 job.setGroupingComparatorClass(OrderGroupingComparator.class); //7:提交 boolean result = job.waitForCompletion(true); System.exit(result?0:-1); } }
- OVER