MapReduce详细工作流程一:
  • 如图
MapReduce详细工作流程二:
  • 如图
Shuffle机制
  • Map方法之后,Reduce方法之前的数据处理过程称之为Shuffle。如下图所示:
  • hadoop 处理数据过程 hadoop数据处理流程图_Text

  • 图解:
  1. MapTask搜集map()方法的kv对,放入内存缓冲区中
  2. 从内存不断溢写到本地磁盘文件,可能会溢出多个文件
  3. 多个溢出文件会被合并成大的溢出文件
  4. 在溢写过程和合并过程中,都要调用Partitioner进行分区和针对key进行排序
  5. ReduceTask根据自己的分区号取各个MapTask机器上取相应的的结果来分区数据
  6. ReduceTask会取到不同MapTask机器上同一分区的结果文件,再进行合并(归并排序)
  7. 合并成大文件后,shuffle阶段结束,后面进入ReduceTask的逻辑运算过程(从文件中取出一个一 个的键值对Group,调用用户自定义的reduce()方法)
  • 注意
    Shuffle中的缓冲区大小会影响到MapReduce程序的执行效率,原则上说,缓冲区越大,磁盘io的次数越少,执行速度就越快。(缓冲区的大小可以通过参数调整,参数:io.sort.mb默认100M)
Partition分区:
  1. 问题引出:
    要求将统计结果按照条件输出到不同文件;例如将统计结果按手机号码所属省份输出到不同的文件中(分区)
  1. 默认的Partitioner分区:
public class HashPartitioner<K,V> extends Partitioner<K,V>{
    public int getPartitoion(K key,V value,int numReduceTask){
	return (key.hashCode() & Integer.Integer.MAX_VALUE) % numReduceTasks;
	//用户无法控制哪个key存储到哪个分区
    }
}
  1. 自定义Partitioner步骤:
    ①自定义类继承Partitioner,重写getPartition()方法
public class CustomPartitioner extends Partitioner<Text,FlowBean>{
	@override
	public int getPartitoion(K key,V value,int numReduceTask){
		//控制分区的逻辑代码
		
		return partition;
	}
}
  1. ②在job驱动中,设置自定义Partitioner:
    Job.setPartitionerClass(CustomPartitioner.class) ③自定义partition后,要根据自定义的Partitioner的逻辑设置相应数量的ReduceTask:
    Job.setNumReduceTask(5);
  2. 分区总结:
    ①如果ReduceTask的数量 > getPartition的结果数:则会多产生几个输出文件part-r-000xx
    ②若 1 < ReduceTask的数量 < getPartition的结果数:有一部分数据无处安放,会报错
    ③若 ReduceTask = 1:则所有分区的结果都交给这一个ReduceTask,最终也只有一个输出文件part-r-00000
    ③分区号必须从零开始,逐一累加
  3. 案例分析:
    假设自定义分区数为5,则:
    job.setNumReduceTask(1);//正常运行,产生一个文件job.setNumReduceTask(2);//报错job.setNumReduceTask(6);//多产生一个空文件
  • 分区案例:
  1. 需求:将统计结果按照手机归属地不同省份输出到不同文件中(分区)
    ①输入数据格式:

    ②期望输出数据:
  2. 在原来序列化案例(求流量总和)基础上增加一个分区类:
public class ProvincePartitioner extends Partitioner<Text, FlowBean> {

   @Override
   public int getPartition(Text key, FlowBean value, int numPartitions) {
       //1 获取电话号码的前三位
      String preNum = key.toString().substring(0, 3);     
      int partition = 4;   
    
      //2 判断是哪个省
      if("136".equals(preNum)) {
          partition = 0;
      }else if ("137".equals(preNum)) {
          partition = 1;
      }else if ("138".equals(preNum)) {
          partition = 2;
      }else if ("139".equals(preNum)) {
          partition = 3;
      } 
      return partition;
   }
}
  1. 在驱动函数中增加自定义数据分区设置和ReduceTask设置:
public class FlowCountDriver {

    public static void main(String[] args) throws Exception {

        String s1 = "E:\\input\\input4";
        String s2 = "E:\\output\\output3";

        //获取配置信息,job对象
        Configuration con = new Configuration();
        Job job = Job.getInstance(con);

        //指定jar加载路径
        job.setJarByClass(FlowCountDriver.class);

        //设置map、reduce类
        job.setMapperClass(FlowCountMapper.class);
        job.setReducerClass(FlowCountReducer.class);

        //设置map输出
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(FlowBean.class);

        //设置最终的KV类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(FlowBean.class);

        //指定自定义数据分区
        job.setPartitionerClass(ProvincePartitioner.class);
        //指定相应的 reduceTask个数
        job.setNumReduceTasks(5);

        //指定job的输入原始路径和最终输出路径
        FileInputFormat.setInputPaths(job, new Path(s1));
        FileOutputFormat.setOutputPath(job, new Path(s2));

        //提交
        boolean b = job.waitForCompletion(true);

        System.exit(b ? 0 : 1);

    }

}
WritableComparable排序
  • 概述:
  • 排序是MapReduce框架最重要的操作之一。
  • MapTask和ReduceTask均会对key进行排序,这是Hadoop的默认行为,任何程序中的数据都会被排序,不管逻辑上是否需要
  • 默认排序是按照字典顺序排序,且实现该排序方法的是快速排序
  • 对于MapTask,它先将处理好的结果暂时放入环形缓冲区,当缓冲区的使用率达到一定的阈值时,再对缓冲区的数据进行一次快速排序,并将排好序的数据溢写到磁盘,当所有数据都处理完毕之后,它再对磁盘中的所有文件归并排序
  • 对于ReduceTask,它从每个MapTask远程拷贝相应的数据文件,若文件的大小超过一定阈值,则溢写到磁盘,否则存储在内存中;若磁盘中的文件数目达到一定阈值,则对文件进行归并排序生成一个更大的文件;若内存中的数据大小或数量超过一定阈值,则进行一次排序后溢写到磁盘,当所有文件拷贝完成,ReduceTask同一对内存和磁盘中所有数据进行一次归并排序
  1. 排序分类
    ①部分排序:
    MapReduce根据输入记录的键值对集排序,保证每个输出文件内部有序
    ②全排序:
    最终输出结果只有一个文件,且文件内部有序。实现方式是只设置一个ReduceTask,这种排序在处理大文件时效率极低,因为一台机器处理所有文件,完全丧失了MapReduce所提供的并行架构
    ③辅助排序(Grouping Comparable分组):
    在reduce端对key进行分组。应用于:在接收的key为bean对象时,想让一个或几个字段相同(全部字段比较不相同)的key进入到同一个reduce方法,可以采用分组排序。
    ④二次排序:
    在自定义排序过程中,如果compareTo中的判断条件为两个即为二次排序
  2. 自定义排序WritableComparable
    原理分析:当自定义bean对象当做key进行传输时,该类实现WritableComparator重写compareTo方法即可实现排序:
@Override
public int compareTo(FlowBean o) {
   
   int result;	 
   // 按照总流量大小,倒序排列
   if (sumFlow > bean.getSumFlow()) 
        result = -1;
   else if (sumFlow < bean.getSumFlow()) 
        result = 1;
   else 
        result = 0;    
        
    return result;
}
  • **WritableComparable排序案例实操之 全排序 **
  • 需求分析(根据总流量进行倒序排序)
    ①输入数据格式:
    ②输出数据格式:
    ③FlowBean实现WritableCompareable接口重写compareTo方法
    ④map类:
    context.write(bean,phoneNum) ⑤reduce类:
//循环输出,避免总流量相同的情况
for (Text text : values)
  context.write(text,key)
  • 代码实现

FlowBean类

import org.apache.hadoop.io.Writable;
import org.apache.hadoop.io.WritableComparable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;

public class FlowBean implements WritableComparable<FlowBean>, Writable {
    private int upFlow;
    private int downFlow;
    private int sumFlow;

    public FlowBean() {
    }

    public FlowBean(int upFlow, int downFlow) {
        this.upFlow = upFlow;
        this.downFlow = downFlow;
        this.sumFlow = upFlow + downFlow;
    }

    public int getUpFlow() {
        return upFlow;
    }

    public void setUpFlow(int upFlow) {
        this.upFlow = upFlow;
    }

    public int getDownFlow() {
        return downFlow;
    }

    public void setDownFlow(int downFlow) {
        this.downFlow = downFlow;
    }

    public int getSumFlow() {
        return sumFlow;
    }

    public void setSumFlow(int sumFlow) {
        this.sumFlow = sumFlow;
    }

    @Override
    public String toString() {
        return upFlow + "\t" + downFlow + "\t" + sumFlow;
    }
    
    @Override
    public int compareTo(FlowBean bean) {

        if (sumFlow > bean.getSumFlow())
            return -1;
        else if (sumFlow < bean.getSumFlow())
            return 1;
        else
            return 0;
    }

    @Override
    public void write(DataOutput dataOutput) throws IOException {
        dataOutput.writeInt(upFlow);
        dataOutput.writeInt(downFlow);
        dataOutput.writeInt(sumFlow);
    }

    @Override
    public void readFields(DataInput dataInput) throws IOException {
        this.upFlow = dataInput.readInt();
        this.downFlow = dataInput.readInt();
        this.sumFlow = dataInput.readInt();
    }
}

FlowCountSortMapepr类

import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;

public class FlowCountSortMapepr extends Mapper<LongWritable, Text, FlowBean, Text> {

    FlowBean k = new FlowBean();
    Text v = new Text();
    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {

        String line = value.toString();
        String[] flow = line.split("\t");

        String phoneNum = flow[1];
        int downFlow = Integer.parseInt(flow[flow.length - 2]);
        int upFlow = Integer.parseInt(flow[flow.length - 3]);

        k.setDownFlow(downFlow);
        k.setUpFlow(upFlow);
        k.setSumFlow(upFlow + downFlow);
        v.set(phoneNum);

        context.write(k, v);
    }
}

FlowCountSortReducer类

import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;

public class FlowCountSortReducer extends Reducer<FlowBean, Text, Text, FlowBean> {
    @Override
    protected void reduce(FlowBean v, Iterable<Text> values, Context context) throws IOException, InterruptedException {
        for (Text k : values)
            context.write(k, v);
    }
}

FlowCountSortDriver类

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

public class FlowCountSortDriver {
    public static void main(String[] args) throws Exception {
        String inputPath = "E:\\input\\input4";
        String outputPath = "E:\\output\\output1";

        //1 获取配置信息,job对象
        Configuration conf = new Configuration();
        Job job = Job.getInstance(conf);

        //2 指定 jar加载路径、map类、reduce类
        job.setJarByClass(FlowCountSortDriver.class);
        job.setMapperClass(FlowCountSortMapepr.class);
        job.setReducerClass(FlowCountSortReducer.class);

        //3 设置map的输入输出类型和最终的输入输出类型
        job.setMapOutputKeyClass(FlowBean.class);
        job.setMapOutputValueClass(Text.class);
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(FlowBean.class);

        //4 指定job的原始输入路径和最终输出路径
        FileInputFormat.setInputPaths(job, new Path(inputPath));
        FileOutputFormat.setOutputPath(job, new Path(outputPath));

        //5 提交任务
        boolean res = job.waitForCompletion(true);
        System.exit(res ? 0 : 1);
    }
}
  • WritableComparable排序案例实操之 区内排序
    需求:要求每个省份手机号输出的文件中按照总流量内部排序。
    输入格式:
    输出格式:

    案例实操:
    ①增加自定义分区类:
public class ProvincePartitioner extends Partitioner<FlowBean, Text> {
    @Override
    public int getPartition(FlowBean key, Text value, int numPartitions) {

        // 1 获取手机号码前三位
        String preNum = value.toString().substring(0, 3);
       
        int partition = 4;

        // 2 根据手机号归属地设置分区
        if ("136".equals(preNum)) {
            partition = 0;
        }else if ("137".equals(preNum)) {
            partition = 1;
        }else if ("138".equals(preNum)) {
            partition = 2;
        }else if ("139".equals(preNum)) {
            partition = 3;
        }
        return partition;
    }
}

②在驱动类中添加分区类:

// 加载自定义分区类
job.setPartitionerClass(ProvincePartitioner.class);
// 设置Reducetask个数
job.setNumReduceTasks(5);