带你手写MR中的自定义Partitioner分区

588 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第6天,点击查看活动详情

上文我们说了在Reduce阶段可以自定义分区来防止数据倾斜情况的发生,那我们这篇文章呢,就详细的讲解下Partition分区,并且实现一个自定义分区的具体需求。

Partition分区

对于分区来说,我们首先需要知道 MR程序中的 ReduceTask实例进程 和 Partition 之间的关系。

  • 如果 ReduceTask的数量 > getPatition的结果数,则会多产生几个空的输出文件part-r-000xx;

  • 如果 1 < ReduceTask的数量 < getPatition的结果数,则有一部分分区数据无处安放,程序会报错Exception;

  • 如果 ReduceTask的数量 = 1,则不管MapTask端输出多少个分区文件,最终结果都交给这一个RecceTask,最终也就只会产生─个结果文件part-r-00000 ;

这里还有两点我们需要注意:

  • 如果分区数不是1,但是ReduceTask为1,程序是否会执行分区过程呢? 那么答案是: 程序不执行分区过程。因为在MapTask的源码中,执行分区的前提是先判断ReduceNum个数是否大于1,不大于1肯定不执行。
  • 如果 ReduceTask = 0,表示程序中没有Reduce阶段,输出文件个数与 Map 个数一致;

自定义Partition步骤

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

public class CustomPartitioner extends Partitioner<Text,FlowBean> {
    @override
    public int getPartition(Text key,FlowBean value, int numPartiti ons){
        // 控制分区代码逻辑
        ......
                return partition ;
    }
}

(2) 在Job驱动中,设置自定义Partitioner

job.setPartitionerClass(CustomPartitioner.class);

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

job.setNumR educeTasks(5);

Partition自定义分区demo

(1)需求 将统计结果按照手机归属地不同省份输出到不同文件中(分区)。
以 136、137、138、139开头的手机号都单独存放到4个文件中,剩余的手机号放到一个文件中。输入数据如下。 image.png

(2)自定义分区类

package com.mapreduce.partitioner;

import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;

public class ProvincePartitioner extends Partitioner<Text, FlowBean> {

    @Override
    public int getPartition(Text text, FlowBean flowBean, int numPartitions) {
        //获取手机号前三位prePhone
        String phone = text.toString();
        String prePhone = phone.substring(0, 3);

        //定义一个分区号变量partition,根据prePhone设置分区号
        int partition;

        if("136".equals(prePhone)){
            partition = 0;
        }else if("137".equals(prePhone)){
            partition = 1;
        }else if("138".equals(prePhone)){
            partition = 2;
        }else if("139".equals(prePhone)){
            partition = 3;
        }else {
            partition = 4;
        }

        //最后返回分区号partition
        return partition;
    }
}

(3)Mapper类

package com.mapreduce.writable;

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

public class FlowMapper extends Mapper<LongWritable, Text, Text, FlowBean> {
    private Text outK = new Text();
    private FlowBean outV = new FlowBean();

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

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

        //3 抓取我们需要的数据
        String phone = split[1];
        String up = split[split.length - 3];
        String down = split[split.length - 2];
 
        //4 封装outK outV
        outK.set(phone);
        outV.setUpFlow(Long.parseLong(up));
        outV.setDownFlow(Long.parseLong(down));
        outV.setSumFlow();

        //5 写出outK outV
        context.write(outK, outV);
    }
}

(3)Reducer类

package com.mapreduce.writable;

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

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

        long totalUp = 0;
        long totalDown = 0;

        //1 遍历values
        for (FlowBean flowBean : values) {
            totalUp += flowBean.getUpFlow();
            totalDown += flowBean.getDownFlow();
        }

        //2 封装outKV
        outV.setUpFlow(totalUp);
        outV.setDownFlow(totalDown);
        outV.setSumFlow();

        //3 写出outK outV
        context.write(key,outV);
    }
}

(4)Driver驱动类

package com.mapreduce.partitioner;

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;
import java.io.IOException;

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

        //2 关联本Driver类
        job.setJarByClass(FlowDriver.class);

        //3 关联Mapper和Reducer
        job.setMapperClass(FlowMapper.class);
        job.setReducerClass(FlowReducer.class);

        //4 设置Map端输出数据的KV类型
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(FlowBean.class);

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

        // 指定自定义分区器
        job.setPartitionerClass(ProvincePartitioner.class);

        // 同时指定相应数量的ReduceTask
        job.setNumReduceTasks(5);

        //6 设置输入输出路径
        FileInputFormat.setInputPaths(job, new Path("D:\\inputflow"));
        FileOutputFormat.setOutputPath(job, new Path("D\\partitionout"));

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

总结

通过以上案例,我们可以看出,MapReduce代码的编写有基本的三个类,Map类、Reducer类以及Driver类,其中Driver驱动类中有固定7个步骤代码,

  • 获取Job对象
  • 关联本Drive类
  • 关联Mapper和Reducer
  • 设置Map端输出数据的KV类型
  • 设置程序最终输出的KV类型
  • 设置输入输出路径
  • 提交Job