大数据(十):MapTask工作机制与Shuffle机制(partitioner输出分区、WritableComparable排序)

一、MapTask工作机制

  1. Read阶段:MapTask通过用户编写的RecordReader,从输入InputSplit中解析出一个个key/value

  2. Map阶段:该节点主要是将解析出的key/value交给用户编写map()函数处理,并产生一系列新的key/value。

  3. Collect收集阶段:在用户编写map()函数中,当数据处理完成后,一般会调用OutputCollection.collect()输出结果。在该函数内部,它会将生成的key/value分区(调用Partitioner),并写入一个环形内存缓冲区中。

  4. Spill阶段:即“溢写”,当环形缓冲区满后,MapReduce会将数据写入本地磁盘上,生成一个临时文件。需要注意的是,将数据写入本地磁盘之前,先要对数据进行一次本地排序,并在必要时对数据进行合并、压缩等操作。

    1. 溢写阶段详情:

      1. 利用快速排序算法对缓存区内的数据进行排序,排序方式是,先按照分区编号partition进行排序,然后按照key进行排序。这样,经过排序后,数据以分区为单位聚集在一起,且同一分区内所有数据按照key有序。

      2. 按照分区编号由小到大依次将每个分区中的数据写入任务工作目录下的临时文件output/spillN.out(N表示当前溢写次数)中。如果用户设置Combiner,则写入文件之前,对每个分区中的数据进行一次聚集操作。

      3. 将分区数据的元信息写到内存索引数据结构SpillRecord中,其中每个分区的元信息包括在临时文件中的偏移量、压缩前数据大小和压缩后数据大小。如果当前内存索引大小超过1MB,则将内存索引写到文件output/spillN.out.index中。

  5. Combine阶段:当所有数据处理完成后,MapTask对所有临时文件进行一次合并,以确保最终只会生成一个数据文件。

        当所有数据处理完后,MapTask会将所有临时文件合并成一个大文件,并保存到文件output/file.out中,同时生成相应的索引文件output/file.out.index。

        在进行文件合并过程中,MapTask以分区为单位进行合并。对于某个分区,它将采用多轮递归合并的方式。每轮合并io.sort.factor(默认100)个文件,并将产生的文件重新加入待合并列表中,对文件排序后,重复以上过程,直到最终得到一个大文件。

        让一个MapTask最终只生成一个数据文件,可避免同时打开大量文件和同时读取大量小文件产生的随机读取带来的开销。

 

二、Shuffle机制

        MapReduce确保每个reducer的输入都是按键排序的。系统执行排序的过程(即将map输出作为输入传给reducer)称为shuffle。

 

三、Partition分区

1、默认partition分区

public class HashPartitioner<K,V> extends Partition<K,V>{
    public int getPartition(K key,V value,int numReduceTasks){
        retuern (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
    }
}

2、自定义Partitioner步骤

  1. 自定义类继承Partitioner,重写getPartition()方法

  2. 在job驱动中,设置自定义Partitioner

    1. job.setPartitionerClass(CustomPartitioner.class);

  3. 自定义Partition后,要根据自定义Partitioner的逻辑设置相应数量的ReduceTask

    1. job.setNumReduceTasks(5)

3、注意

  1. 如果reduceTask的数量>getPartition的结果数,则会多产生几个空的输出文件part-r-000xx

  2. 如果1<reduceTask的数量<getPartition的结果数,则有一部分分区数据无处安放,会抛出异常

  3. 如果reduceTask的数量=1,则不管mapTask端输出多少个分区文件,最终结果都交给这一个reduceTask,最终也就只会产生一个结果文件part-r-00000

4、自定义Partitioner实例:将统计结果按照手机归属地不同省份输出到不同文件

       1、根据手机号的前三位判断省份,如:139******31是江苏的,再统计出手机使用的流量总和

       2、准备数据新建txt文件,每行数据格式:id 手机号 上行流量 下行流量 ip

       3、分析

    1. MapReduce中会将map输入的kv对,按照相同的key分组,然后分发给不同的reducetask。默认的分发规则为:根据key的hashcode%reducetask数来分发

    2. 如果要按照我们自己的需求进行分组,则需要改写数据分发(分组)组件Partitioner,自定义一个CustomPartitioner继承抽象类Partitioner

    3. 在job驱动中,设置自定义的partitioner

    4. 最终输出结果  手机号 上行流量 下行流量 总流量

       4、编写bean,flowbean将会作为map的value,而手机号会作为key

public class FlowBean implements Writable {
    /**
    * 上行流量
    */
    private long upFlow;

    /**
    * 下行流量
    */
    private long downFlow;
    /**
    * 总流量
    */
    private long sumFlow;

    /**
    * 必须要有空参构造,为了后续反射用
    */
    public FlowBean() {
        super();
    }

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

    public void set(long upFlow, long downFlow) {
        this.upFlow = upFlow;
        this.downFlow = downFlow;
        this.sumFlow = upFlow + downFlow;
    }

    /**
    * 序列化方法
    */
    @Override
    public void write(DataOutput out) throws IOException {
        out.writeLong(upFlow);
        out.writeLong(downFlow);
        out.writeLong(sumFlow);
    }

    /**
    * 反序列化方法
    * 注意:序列化顺序和反序列化顺序必须一致
    */
    @Override
    public void readFields(DataInput in) throws IOException {
        this.upFlow = in.readLong();
        this.downFlow = in.readLong();
        this.sumFlow = in.readLong();    

    @Override
    public String toString() {
       return upFlow + "\t" + downFlow + "\t" + sumFlow;

    }

    public long getUpFlow() {
        return upFlow;
    }

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

    public long getDownFlow() {
        return downFlow;
    }

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

    public long getSumFlow() {
        return sumFlow;
    }

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

       5、编写Mapper

public class FlowMapper extends Mapper<LongWritable, Text, Text, FlowBean> {
    FlowBean v = new FlowBean();
    Text k = new Text();

    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        // 1 获取一行
        String line = value.toString();

        // 2 切割
        String[] fields = line.split("\t");

        // 3 封装对象
        // 手机号
        String phoneNum = fields[1];

        // 上行流量
        long upFlow = Long.parseLong(fields[fields.length - 3]);
        // 下行流量
        long downFlow = Long.parseLong(fields[fields.length - 2]);

        v.set(upFlow, downFlow);
        k.set(phoneNum);

        // 4 写出数据
        context.write(k, v);
    }
}

       6、编写Reducer

public class FlowReducer extends Reducer<Text, FlowBean, Text, FlowBean> {
    @Override
    protected void reduce(Text key, Iterable<FlowBean> values, Context context) throws IOException, InterruptedException {
    long sumUpFlow = 0;
    long sumDownFlow = 0;

    // 1 累加求和
    for (FlowBean flowBean : values) {
        sumUpFlow += flowBean.getUpFlow();
        sumDownFlow += flowBean.getDownFlow();
    }

    FlowBean flowBean = new FlowBean(sumUpFlow, sumDownFlow);

    // 2 输出
    context.write(key, flowBean);
}

       7、编写partitioner

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;

        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;
    }
}

       8、编写Driver

public class FlowDriver {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
        // 1 获取job对象
        Configuration configuration = new Configuration();
        Job job = Job.getInstance(configuration);

        // 2 设置jar包路径
        job.setJarByClass(FlowDriver.class);

        // 3 管理mapper和reducer类
        job.setMapperClass(FlowMapper.class);
        job.setReducerClass(FlowReducer.class);

        // 4 设置mapper输出的kv类型
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(FlowBean.class);

        // 5 设置最终输出kv类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(FlowBean.class);

        // 设置分区
        job.setPartitionerClass(ProvincePartitioner.class);
        /*
        NumReduceTasks是生成文件的个数 最好等于分类的个数
        等于1则没有效果
        大于1小于分类个数则会报错
        大约分类个数则会出现空文件
        */
        job.setNumReduceTasks(5);
        // 6 设置输入输出路径
        FileInputFormat.setInputPaths(job, new Path(args[0]));
        FileOutputFormat.setOutputPath(job, new Path(args[1]));

        // 7 提交
        boolean result = job.waitForCompletion(true);
        System.exit(result ? 0 : 1);
    }
}

       9、配置pargram arguments,需要处理的文件所在文件夹和处理后输出的文件夹(这个文件夹不可存在)

       10、运行程序查看结果

 

四、WritableComparable排序

        排序是MapReduce框架中最重要的操作之一。MapTask和ReducerTask均会对数据(按照key)进行排序。该操作属于Hadoop的默认行为。任何应用程序中的数据均会被排序,而不管逻辑上是否需要。默认排序是按照字典顺序排序,且实现该排序的方法是快速排序。

        对于MapTask,它会将处理的结果暂时存放到一个缓冲区,当缓冲区使用率达到一定阈值后,在对缓冲区中的数据进行一次排序,并将这些有序数据写到磁盘上,而当数据处理完毕后,它会对磁盘上所有文件进行一次,合并以将这些文件合并成一个大的有序文件。

        对于ReduceTask,它从每一个MapTask上远程拷贝相应的数据文件,如果文件大小超过阈值,则放在磁盘上,否则放到内存中。如果磁盘上文件数目达到阈值,则进行一次合并以生成一个更大文件;如果内存中文件大小或者数目超过一定阈值,则进行一次合并后将数据写到磁盘上,当所有数据拷贝完毕后,ReduceTask统一对内存和磁盘上的所有数据进行一次合并。

1、排序的分类:

  1. 部分排序:MapReduce根据输入记录的键对数据集排序。保证输出的每个文件内部排序。

  2. 全排序:首先创建一系列排好序的文件;其次,串联这些文件;最后,生成一个全局排序的文件。主要思路是使用一个分区来描述输出的全局排序

  3. 辅助排序(GroupingComparatorf分区):MapReduce框架在记录到达reducer之前按键对记录排序,但键所对相应的值并没有被排序。甚至在不同的执行轮次中,这些值的排序也不固定,因为它们来自不同的map任务且这些map任务在不同轮次中完成时间各不相同。一般来说,大多数MapReduce程序会避免让reducer函数依赖于值的排序。但是,有时也需要通过特定的方法对键进行排序和分组等以实现对值的排序。

  4. 二次排序:在自定义排序过程中,如果compareTo中的判断条件为两个即为二次排序。

2、自定义排序WritableComparable

       bean对象实现WritableComparable接口重写compareTo方法,就可以实现排序

3、自定义排序实例:根据上面实例产生的结果再次对总流量进行排序

       1.编写bean

public class FlowBean implements WritableComparable<FlowBean>{
    /**
    * 上行流量
    */
    private long upFlow;
    /**
    * 下行流量
    */
    private long downFlow;
    /**
    * 总流量
    */
    private long sumFlow;

    /**
    * 必须要有空参构造,为了后续反射用
    */
    public FlowBean() {
        super();
    }

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

    public void set(long upFlow, long downFlow){
        this.upFlow = upFlow;
        this.downFlow = downFlow;
        this.sumFlow = upFlow + downFlow;
    }

    /**
    * 序列化方法
    */
    @Override
    public void write(DataOutput out) throws IOException {
        out.writeLong(upFlow);
        out.writeLong(downFlow);
        out.writeLong(sumFlow);
    }

    /**
    * 反序列化方
    * 注意:序列化顺序和反序列化顺序必须一致
    */
    @Override
    public void readFields(DataInput in) throws IOException {
        this.upFlow = in.readLong();
        this.downFlow = in.readLong();
        this.sumFlow = in.readLong();
    }

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

    public long getUpFlow() {
        return upFlow;
    }

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

    public long getDownFlow() {
        return downFlow;
    }

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

    public long getSumFlow() {
        return sumFlow;
    }

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

    @Override
    public int compareTo(FlowBean o) {
        return (int) (this.sumFlow - o.getSumFlow());
    }
}

       2.编写Mapper

public class FlowSortMapper 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 {
        // 1 获取一行
        String line = value.toString();
        // 2 切割
        String[] fields = line.split("\t");
        // 3 封装对象
        long upFlow = Long.parseLong(fields[1]);
        long downFlow = Long.parseLong(fields[2]);
        k.set(upFlow, downFlow);
        v.set(fields[0]);
        // 4 写出
        context.write(k, v);
    }
}

       3.编写Reducer

public class FlowSortReducer extends Reducer<FlowBean, Text, Text, FlowBean>{
    @Override
    protected void reduce(FlowBean key, Iterable<Text> values, Context context)    throws IOException, InterruptedException {
        context.write(values.iterator().next(), key);
    }
}

       4.编写Driver

public class FlowSortDriver {
    public static void main(String[] args) throws IOException, ClassNotFoundException,InterruptedException {
        // 1 获取job对象
        Configuration configuration = new Configuration();
        Job job = Job.getInstance(configuration);

        // 2 设置jar包路径
        job.setJarByClass(FlowSortDriver.class);

        // 3 管理mapper和reducer类
        job.setMapperClass(FlowSortMapper.class);
        job.setReducerClass(FlowSortReducer.class);
    
        // 4 设置mapper输出的kv类型
        job.setMapOutputKeyClass(FlowBean.class);
        job.setMapOutputValueClass(Text.class);

        // 5 设置最终输出kv类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(FlowBean.class);

        // 6 设置输入输出路径
        FileInputFormat.setInputPaths(job, new Path(args[0]));
        FileOutputFormat.setOutputPath(job, new Path(args[1]));

        // 7 提交
        boolean result = job.waitForCompletion(true);
        System.exit(result ? 0 : 1);
    }
}

 

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值