Hadoop MapReduce编程模型

本文详细介绍 Hadoop MapReduce 编程模型,包括 MapReduce 的工作原理、核心组件及其实现方式。并通过 WordCount 和流量统计两个实战案例,演示如何使用 Hadoop MapReduce 处理大规模数据集。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

文章转自:Hadoop MapReduce编程模型

东风化宇 2015年06月26日 于 Flyne 发表

本文的行文思路如下:

05.-MapReduce编程模型32

一、MapReduce

1、什么是MapReduce

MapReduce是由Google提出的一个分布式计算模型,用来解决海量数据的计算问题。举个例子说明其解决问题的思想:

05.-MapReduce编程模型122

MapReduce由两个阶段组成:

  • Map阶段:将一个大任务分解成小任务,并分发给每个节点,每个节点并行处理这些任务,处理速度很快;
  • Reduce阶段:对Map的结果汇总即可,在不要求全局汇总的情况下Reduce阶段也可以并发;

2、Hadoop MapReduce

Hadoop中的MapReduce模块(简称Hadoop MapReduce)是对MapReduce思想的具体实现,主要有两部分组成:编程模型和运行时环境。

  • 编程模型:即Hadoop MapReduce对外提供的编程接口
  • 运行时环境:做一些较为复杂的工作,如任务分发、节点间的通信、节点失效、数据切分等,用户无需关心这些细节;

借助Hadoop MapReduce提供的编程接口,我们可以快速的写出分布式计算程序(如WordCount程序中只需实现map()和reduce()两个函数即可),而无需关注分布式环境的一些实现细节(即运行时环境)。

编程模型为用户提供了5个可编程组件,分别是InputFormatMapperPartitionerReducerOutputFormat

还有一个组件是Combiner、但它实际上也是一个Reducer

Hadoop MapReduce实现了很多可直接使用的InputFormatPartitionerOutputFormat,大部分情况下,用户只需编写MapperReducer即可。

二、Hadoop MapReduce基础组件

1、Hadoop MapReduce原理

下图展示了Hadoop MapReduce原理:

05.-MapReduce编程模型817
这里写图片描述

对上图的说明如下:

1)Mapper任务

  1. 读取输入文件内容(可以来自于本地文件系统,或HDFS文件系统等),对输入文件的每一行,解析成key-value对[K1,V1]。K1表示行起始偏移量,V1表示读取的一行内容。
  2. 调用map()方法,将[K1,V1]作为参数传入。在map()方法中封装了数据处理的逻辑,对输入的key、value进行处理。
  3. map()方法处理的结果也用key-value的方式进行输出,记为[K2, V2]。

2)Reducer任务

  1. 在执行Reducer任务之前,有一个shuffle的过程对多个mapper任务的输出进行合并、排序,输出[K2, {V2, …}]。
  2. 调用reduce()方法,将[K2, {V2, …}]作为参数传入。在reducer()方法中封装了数据汇总的逻辑,对输入的key、value进行汇总处理。
  3. reduce()方法的输出被保存到指定的目录下。

2、第一个MapReduce程序:WordCount

WordCount程序之于MapReduce就像HelloWorld程序之于任何编程语言,都是初学者接触的第一个程序。这儿不再赘述WordCount程序实现的功能。利用MapReduce实现的WordCount程序的思想如下图:

05.-MapReduce编程模型1377
这里写图片描述

下面是Hadoop MapReduce版的WordCount程序的开发过程:

1)搭建MapReduce开发环境:

本程序在CentOS虚拟机上开发,环境的搭建包括安装CentOS虚拟机、JDK、Eclipse以及网络配置,这些不再赘述。

在Eclipse中新建wordcount项目,添加项目所需jar包,有两种方式:

  • 通过build path直接添加jar包:MapReduce程序需要Hadoop Common模块MapReduce模块YARN模块的支持,因此需要添加HADOOP_HOME/share/hadoop/common、mapreduce和yarn目录下的jar包。
  • 如果你的项目是个Maven项目,直接在pom.xml文件中添加hadoop-commonmapreduceyarn的依赖即可。

本文程序采用第一种方式管理jar包。

2)编写Mapper类:
public class WordCountMapper extends Mapper<LongWritable, Text, Text, LongWritable> {
    /**
     * 在执行Mapper任务之前,MapReduce框架中有一个组件(FileInputFormat的一个实例对象)会读取文件中的一行,将这一行的起始偏移量和行内容封装成key-value,作为参数传入map()方法。
     */
    protected void map(LongWritable key, Text value, Context context)
            throws IOException, InterruptedException {
        //拿到一行的内容
        String line = value.toString();
        //将行内容切分成单词数组
        String[] words = StringUtils.split(line, ' ');

        for (String word : words) {
            //输出  <单词,1> 这样的key-value对
            context.write(new Text(word), new LongWritable(1L));
        }
    }
}

编写Mapper类应注意:

  • Mapper类中的四个泛型分别表示KEYIN、VALUEIN、KEYOUT和VALUEOUT。其中KEYIN、VALUEIN的类型默认为LongWritableText,表示MR程序输入文件中一行的起始偏移量和行内容。KEYOUT和VALUEOUT同map阶段的输出key-value类型一致。
  • Hadoop中的LongWritableText就相当于Java中的LongString类型,它是Hadoop利用自己的序列化机制对LongString的封装。在Hadoop中有自己的序列化机制(实现Writable接口),它比Java中的序列化机制更加简洁高效。
3)编写WordCountReducer类
public class WordCountReducer extends Reducer<Text, LongWritable, Text, LongWritable> {
    /**
     * 在调用reduce方法之前有个shuffle过程
     * reduce()方法的输入数据来自于shuffle的输出,格式形如:<hello, {1,1,……}>
     */
    protected void reduce(Text key, Iterable<LongWritable> values, Context context)
            throws IOException, InterruptedException {
        //定义一个累加计数器
        long counter = 0;

        //统计单词出现次数
        for (LongWritable value : values) {
            counter += value.get();
        }

        //输出key表示的单词的统计结果
        context.write(key, new LongWritable(counter));
    }
}

下图是Mapper、Reducer类中泛型的说明:

05.-MapReduce编程模型3194
这里写图片描述

4)编写WordCountRunner类
/**
 * 定义一个MapReduce程序的入口
 * 本类中有一个Job对象,该对象用于描述一个MapReduce作业,如Mapper类、Reducer类,以及map/reduce过程的输出类型等等
 */
public class WordCountRunner {
    public static void main(String[] args) throws Exception {
        //实例化一个Job对象
        Configuration conf = new Configuration(); //加载配置文件
        Job job = Job.getInstance(conf);

        //设置Job作业所在jar包
        job.setJarByClass(WordCountRunner.class);

        //设置本次作业的Mapper类和Reducer类
        job.setMapperClass(WordCountMapper.class);
        job.setReducerClass(WordCountReducer.class);

        //设置Mapper类的输出key-value类型
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(LongWritable.class);

        //设置Reducer类的输出key-value类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(LongWritable.class);

        //指定本次作业要处理的原始文件所在路径(注意,是目录)
        FileInputFormat.setInputPaths(job, new Path("/home/hadoop/wordcount/words-file"));
        //指定本次作业产生的结果输出路径(也是目录)
        FileOutputFormat.setOutputPath(job, new Path("/home/hadoop/wordcount/dest"));

        //提交本次作业,并打印出详细信息
        job.waitForCompletion(true);
    }
}

编写WordCountRunner类应注意:

  • 如果没有单独设置Mapper类的输出key-value类型,那么setOutputKeyClass()、setOutputValueClass()方法对Mapper类也奏效。

  • 在本例中MapReduce作业读写的是本地文件,但MR程序也可以读写HDFS上的文件,而且后者使用的更多。

  • setInputPaths()指定输入的原始文件的路径时,是指文件所在的目录,该目录下的所有文件都会被当成输入文件;setOutputPath()方法指定输出文件的目录,一定要注意的是,不能指定一个已存在的目录作为输出目录,否则会报FileAlreadyExistsException的错误。
  • 本例直接在程序中指定了路径仅仅是说明方便。。。。。但是实际开发时一般不直接在程序中指定路径,而是将输入输出路径作为main方法的参数传入(☆)。
5)运行WordCount程序
  • 在Eclipse中直接运行WordCountRunner类:该模式下可以对MapReduce程序进行断点调试。
  • 打成Jar包,提交到集群上运行:将程序打成jar包,命名为wordcount.jar。执行该jar包的命令为:hadoop jar xxx.jar MainClass。此时一定要注意路径的问题!!!

执行结束后,在setOutputPath指定的目录下可以看到MR作业的输出文件:_SUCCESS和part-r-00000。part-r-00000文件内容如下:

This 1

WordCount 1

a 1

file 1

flyne 1

hello 3

is 1

jack 1

just 1

of 1

program. 1

test 1

tom 1

**Question:**setInputPaths和setOutputPath中的路径是本地路径还是HDFS路径由什么决定?即判断一个MR作业使用何种文件系统

**Answer:**MR作业即job对象,一个job对象是通过Job.getInstance(conf)获得,在conf中可以设置fs.defaultFS属性,该属性即表示了MR作业所使用的文件系统。默认情况下MR作业使用本地文件系统,如果设置了fs.defaultFS为hdfs://xxxx:9000,则使用HDFS文件系统。

本例中的输入输出路径明显是Linux下的本地路径,如果非要将本例中的wordcount.jar提交到hadoop集群上执行,必须要在HDFS上创建对应的目录并提交原始单词文件。执行命令如下图:

05.-MapReduce编程模型5301
这里写图片描述

MR程序中可以使用本地文件系统和HDFS,这也表明MR程序和底层文件系统是解耦的,实际上在MR框架中是通过使用不同的FileSystem实现类访问不同的文件系统。

3、第二个MapReduce程序:TrafficStatistics

业务场景:我们每天在使用智能手机时,都会产生大量有用的日志,流量日志就是其中的一种。流量日志记录了用户的上网行为,包括浏览内容、上行/下行流量等等。下图截取了某运营商流量日志的一部分:

05.-MapReduce编程模型5514
这里写图片描述

其中倒数第二、第三行分别记录了用户的下行和上行流量。完整的流量日志文件请点此下载:百度网盘

由于运营商的流量日志内容巨多,下面考虑使用MapReduce统计用户上行/下行流量,即TrafficStatistics程序。程序中包含的Java类具体如下:

1)定义一个Bean,封装上行流量、下行流量和总流量
public class TrafficBean implements Writable{
    private long up_traffic; //上行流量
    private long down_traffic; //下行流量
    private long sum_traffic; //总流量

    public TrafficBean(long up_traffic, long down_traffic) {
        this.up_traffic = up_traffic;
        this.down_traffic = down_traffic;
        this.sum_traffic = up_traffic + down_traffic;
    }

    //反序列化时需要用到反射机制,所以必须有一个默认构造方法
    public TrafficBean() {}

    /**
     * 序列化时,将对象的各个组成部分按byte流顺序写入output
     */
    public void write(DataOutput out) throws IOException {
        out.writeLong(up_traffic);
        out.writeLong(down_traffic);
        out.writeLong(sum_traffic);
        //So, the flow sequence is: --> sum_traffic --> down_traffic --> up_traffic -->
    }

    /**
     * 反序列化时,从一个数据输入流中按顺序读出对象中的各个组成部分
     * 读取的顺序一定要与写入的顺序保持一致!!!
     */
    public void readFields(DataInput in) throws IOException {
        up_traffic = in.readLong();
        down_traffic = in.readLong();
        sum_traffic = in.readLong();
    }

    public long getUp_traffic() {
        return up_traffic;
    }

    public long getDown_traffic() {
        return down_traffic;
    }

    public long getSum_traffic() {
        return sum_traffic;
    }

    /**
     * 将TrafficBean对象写入文件时写入的内容
     */
    public String toString() {
        return up_traffic + "\t" + down_traffic + "\t" + sum_traffic;
    }
}

关于Hadoop中序列化机制的说明:

  • Hadoop中并没有直接使用JAVA序列化机制,而是提供了Writable接口。Hadoop的序列化机制较Java中的序列化更为简洁高效,这可以提高系统中的网络IO效率。举个例子:JAVA中的对象序列化时,对象的继承结构也被序列化,反序列化后还会还原出其继承结构,因此序列化一个Dog,还需要序列化Animal。所以Java序列化一个类时,需要多序列化很多东西,在网络中传输时需要占用额外的带宽,而Hadoop序列化时就不需要序列化这种继承结构,因为Hadoop中传输的对象都是为了封装数据,没有太多的逻辑,也不需要继承结构。

  • Java基本类型对应的Hadoop中的序列化类如下图所示:05.-MapReduce编程模型7234这里写图片描述

  • MapReduce中的任意key和value都要实现Writable接口,并且key还要实现Comparable接口。因此定义了一个WritableComparable接口,让key实现该接口即可。

    public interface WritableComparable<T> extends Writable, Comparable<T> {}

本例中将TrafficBean作为value进行传输,因此只需要实现Writable接口即可。

2)TrafficStatisticsMapper
public class TrafficStatisticsMapper extends
        Mapper<LongWritable, Text, Text, TrafficBean> {

    protected void map(LongWritable key, Text value, Context context)
            throws IOException, InterruptedException {
        //拿到一行的内容
        String line = value.toString();
        //切分出各个字段
        String[] fields = StringUtils.split(line, '\t');

        //拿到代表号码、上行/下行流量的字段值
        String telNumer = fields[1];
        long up_traffic = Long.parseLong(fields[fields.length - 3]);
        long down_traffic = Long.parseLong(fields[fields.length - 2]);

        //封装到bean
        TrafficBean tb = new TrafficBean(up_traffic, down_traffic);

        //将这个手机号的号码和流量bean输出为一堆key-value
        context.write(new Text(telNumer), tb);
    }
}
3)TrafficStatisticsReducer
public class TrafficStatisticsReducer extends
        Reducer<Text, TrafficBean, Text, TrafficBean> {

    /**
     * reduce输入的数据形如:<手机号,{trafficBean1, trafficBean2……}>
     */
    protected void reduce(Text key, Iterable<TrafficBean> values, Context context)
            throws IOException, InterruptedException {
        //定义上下行流量的计数器
        long up_traffic_counter = 0;
        long down_traffic_counter = 0 ;

        //循环遍历values进行累加
        for (TrafficBean value : values) {
            up_traffic_counter = value.getUp_traffic();
            down_traffic_counter = value.getDown_traffic();
        }

        //将统计结果封装成trafficBean并输出
        TrafficBean resultBean = new TrafficBean(up_traffic_counter, down_traffic_counter);
        context.write(key, resultBean);
    }
}

感觉原文对于流量累加有问题,应改为如下:

for (TrafficBean value : values) {
  up_traffic_counter += value.getUp_traffic();
    down_traffic_counter += value.getDown_traffic();
}
4)TrafficStatisticsRunner
public class TrafficStatisticsRunner {
    public static void main(String[] args) throws Exception {
        Configuration conf = new Configuration();
        Job job = Job.getInstance(conf);

        job.setJarByClass(TrafficStatisticsRunner.class);

        job.setMapperClass(TrafficStatisticsMapper.class);
        job.setReducerClass(TrafficStatisticsReducer.class);

        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(TrafficBean.class);

        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(TrafficBean.class);

        FileInputFormat.setInputPaths(job, new Path("/home/hadoop/traffic-statistics/traffic-file"));
        FileOutputFormat.setOutputPath(job, new Path("/home/hadoop/traffic-statistics/dest"));

        job.waitForCompletion(true);
    }
}
5)运行TrafficStatistics

运行TrafficStatistics程序,结果输出到/home/hadoop/traffic-statistics/dest/part-r-00000文件中,如下:

13480253104 180 180 360

13502468823 7335 110349 117684

13560436666 1116 954 2070

13560439658 918 4938 5856

13602846565 1938 2910 4848

13660577991 6960 690 7650

13719199419 240 0 240

13726230503 2481 24681 27162

13726238888 2481 24681 27162

13760778710 120 120 240

13826544101 264 0 264

13922314466 3008 3720 6728

13925057413 11058 48243 59301

13926251106 240 0 240

13926435656 132 1512 1644

15013685858 3659 3538 7197

15920133257 3156 2936 6092

15989002119 1938 180 2118

18211575961 1527 2106 3633

18320173382 9531 2412 11943

84138413 4116 1432 5548

三、Hadoop MapReduce其他组件

1、局部汇总:Combiner

mapper可能会产生大量的输出,此时带宽容易成为性能瓶颈,Combiner的作用就是在mapper任务的本地对输出先做一次合并,以减少系统中的数据传输量。

Combiner和Reducer写法完全一样,都是继承一个Reducer,但是它在mapper的本地运行,做一些局部汇总操作,减少中间结果。其作用有:

  1. 减少网络传输量(网络IO)
  2. 减少Reducer的计算压力。

在上面WordCount例子中,mapper任务会产生大量形如

//设置本次作业的Mapper类和Reducer类
job.setMapperClass(WordCountMapper.class);
job.setReducerClass(WordCountReducer.class);
//设置Combiner类
job.setCombinerClass(WordCountReducer.class);

使用Combiner时一定要注意,不是什么情况都能用Combiner,前提是不能影响最终逻辑运算结果才行。如下图所示的计算场景(求平均数)就不能使用Combiner:

05.-MapReduce编程模型11022
这里写图片描述

2、结果分组:Partitioner

业务场景:在上面的TrafficStatistics程序中,最后的结果都会输出到一个文件中,每次查询都要查询整个结果集,如果数据特别多就很不方便。因此想到对结果分文件存储,如根据省份存储结果,在查询时,根据手机号所属省份直接查询对应的结果文件即可。这种需求可以利用Partitioner进行实现。

**如何实现:**MapReduce程序生成的结果文件数跟reduce任务个数有关,由于TrafficStatistics程序中只有一个reduce任务,因此所有的结果都输出到一个文件中(即part-r-00000)。如果对每个省份的数据分配一个reduce任务进行汇总,并且定义这样一个Partitioner——根据每条数据中的key值,选择对应的reduce任务,即可完成该场景中的需求。

下面的TrafficStatistics_Partition程序在上面TrafficStatistics程序的基础上进行修改:

1)新增AreaPartitioner类:定义分组的逻辑

public class AreaPartitioner extends Partitioner<Text, TrafficBean>{

    /**
     * 此处用areaMap模拟“手机号-省份”的数据库
     */
    private static HashMap<String,Integer> areaMap = new HashMap<String, Integer>();

    static{ //初始化areaMap
        areaMap.put("137", 0);
        areaMap.put("138", 1);
        areaMap.put("139", 2);
        areaMap.put("135", 3);
        areaMap.put("159", 4);
    }

    /**
     * Partitioner中的逻辑:根据不同的“省份”,返回不同的数字,“数据库”找不到的返回5
     */
    @Override
    public int getPartition(Text key, TrafficBean value, int numPartitions) {
        Integer areaCode = areaMap.get(key.toString().substring(0, 3));
        return areaCode!=null ? areaCode : 5;
    }

}

2)重写TrafficStatisticsRunner:将Partitioner类注册到MapReduce作业中,并指定reduce任务数。

public class TrafficStatisticsRunner {

    public static void main(String[] args) throws Exception {
        Configuration conf = new Configuration();
        Job job = Job.getInstance(conf);

        job.setJarByClass(TrafficStatisticsRunner.class);

        job.setMapperClass(TrafficStatisticsMapper.class);
        job.setReducerClass(TrafficStatisticsReducer.class);

        //将Partitioner类注册到job对象中
        job.setPartitionerClass(AreaPartitioner.class);

        //设置reduce任务数(从args参数传入)
        job.setNumReduceTasks(Integer.parseInt(args[2]));

        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(TrafficBean.class);

        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(TrafficBean.class);

        //MapReduce作业的输入输出路径从参数传入
        FileInputFormat.setInputPaths(job, new Path(args[0]));
        FileOutputFormat.setOutputPath(job, new Path(args[1]));

        job.waitForCompletion(true);
    }

}

3)运行TrafficStatistics_Partition程序

将TrafficStatistics_Partition程序打成Jar包,命名为ts.jar。提交ts.jar到Hadoop集群运行,并指定reduce任务数为6,如下:

#在HDFS上创建输入路径,并上传日志文件
[hadoop@hadoop01 ~]$ hadoop fs -mkdir -p /ts/input
[hadoop@hadoop01 ~]$ hadoop fs -put traffic-statistics/traffic-file/traffic-log.txt /ts/input
#利用hadoop jar命令将任务提交到集群上。
[hadoop@hadoop01 ~]$ hadoop jar ts.jar org.flyne.mapreduce.trafficstatistics.partitioner.TrafficStatisticsRunner /ts/input /ts/output 6

运行完毕后查看程序的输出路径/ts/output,共生成6个输出文件,这和启动的reduce任务数一致。并随机检查一个文件中的内容(如part-r-00003),可以发现和我们期望的结果一致。

05.-MapReduce编程模型13578
这里写图片描述

对结果分组的一些说明:

  1. 本例中设置了reduce任务数为6:job.setNumReduceTasks(6),这和分组数目保持了一致。关于reduce任务数和分组数的关系说明如下:

    • 如果reduce任务数超过分组的数量,不报错,但是多余的reducer输出空文件
    • 如果reduce任务数少于分组的数量,会报错,因为有一些分组找不到对应的reducer
    • 如果reduce任务数为1,也不会报错,这时getPartition()方法失效,所有的分组都到一个reducer中去
  2. HashPartitioner是MapReduce指定的默认分组方式。其定义如下:

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

3、排序sort:Comparable

在TrafficStatistics程序中提到:MR程序中的key除了要实现Writable接口外,还需要实现一个Comparable接口。注:Comparable接口不是Hadoop MapReduce提供的编程接口。

如何实现:在shuffle的过程中有一个排序的过程,它是根据Mapper任务输出的key进行排序的,这也是前文中提到key需要实现Comparable接口的原因。由于本例需要根据总流量排序,因此让TrafficBean实现Comparable接口并作为key进行传输,可以实现该需求。

下面是TrafficStatistics_Sort程序的开发过程:

1)重新定义TrafficBean:增加手机号字段;增加实现Comparable接口

public class TrafficBean implements WritableComparable<TrafficBean>{
    //手机号
    private String phoneNbr;
    private long up_traffic;
    private long down_traffic;
    private long sum_traffic;

    public TrafficBean(String phoneNbr, long up_traffic, long down_traffic) {
        this.phoneNbr = phoneNbr;
        this.up_traffic = up_traffic;
        this.down_traffic = down_traffic;
        this.sum_traffic = up_traffic + down_traffic    ;
    }

    public TrafficBean() {}

    @Override
    public void write(DataOutput out) throws IOException {
        out.writeUTF(phoneNbr);
        out.writeLong(up_traffic);
        out.writeLong(down_traffic);
        out.writeLong(sum_traffic);
    }

    @Override
    public void readFields(DataInput in) throws IOException {
        phoneNbr = in.readUTF();
        up_traffic = in.readLong();
        down_traffic = in.readLong();
        sum_traffic = in.readLong();
    }

    public String getPhoneNbr(){
        return phoneNbr;
    }

    public long getUp_traffic() {
        return up_traffic;
    }

    public long getDown_traffic() {
        return down_traffic;
    }

    public long getSum_traffic() {
        return sum_traffic;
    }

    /**
     * 根据sum_traffic降序排序的逻辑
     */
    @Override
    public int compareTo(TrafficBean o) {
        return this.sum_traffic > o.getSum_traffic() ? -1 : 1;
    }

    public String toString() {
        return phoneNbr + "\t" + up_traffic + "\t" + down_traffic + "\t" + sum_traffic;
    }
}

2)SortMapper、SortReducer:将TrafficBean作为key传输

public class SortMapper extends Mapper<LongWritable, Text, TrafficBean, NullWritable>{

    @Override
    protected void map(LongWritable key, Text value, Context context)
            throws IOException, InterruptedException {
        String[] fields = StringUtils.split(value.toString(), "\t");

        //提取需要的字段
        String phoneNbr = fields[0];
        long up_traffic = Long.parseLong(fields[1]);
        long down_traffic = Long.parseLong(fields[2]);

        TrafficBean tBean = new TrafficBean(phoneNbr, up_traffic, down_traffic);

        context.write(tBean, NullWritable.get());
    }
}

public class SortReducer extends Reducer<TrafficBean, NullWritable, TrafficBean, NullWritable>{

    @Override
    protected void reduce(TrafficBean key, Iterable<NullWritable> values, Context context)
            throws IOException, InterruptedException {
        context.write(key, NullWritable.get());
    }

}

需要指出的是,本程序依赖于TrafficStatistics程序,即输入数据来源于TrafficStatistics程序的输出文件。

3)SortRunner

public class SortRunner {
    public static void main(String[] args) throws Exception {
        Configuration conf = new Configuration();
        Job job = Job.getInstance(conf);

        job.setJarByClass(SortRunner.class);

        job.setMapperClass(SortMapper.class);
        job.setReducerClass(SortReducer.class);

        job.setOutputKeyClass(TrafficBean.class);
        job.setOutputValueClass(NullWritable.class);

        FileInputFormat.addInputPath(job, new Path(args[0]));
        FileOutputFormat.setOutputPath(job, new Path(args[1]));

        job.waitForCompletion(true);
    }
}

4)运行TrafficStatistics_Sort程序

将程序打成jar包,命名为ts_sort.jar。提交ts.jar到Hadoop集群运行,如下:

#TrafficStatistics_Sort程序的输入数据即TrafficStatistics程序的输出文件
[hadoop@hadoop01 ~]$ hadoop fs -put traffic-statistics/dest/part-r-00000 /ts-sort/input
#提交任务至集群
[hadoop@hadoop01 ~]$ hadoop jar ts_sort.jar org.flyne.mapreduce.trafficstatistics.sort.SortRunner /ts-sort/input /ts-sort/output

程序输出的文件即为我们所需的排序好的结果。

4、InputFormat、OutputFormat
  • InputFormat:MR框架读取输入时所用组件。默认的实现类为TextInputFormat,每次读取一行,并将<行起始偏移量,行内容>作为一组key-value传给map()函数。
  • OutputFormat:MR框架输出结果时所用组件。默认的实现类为TextOutputFormat,将结果输出到HDFS上。

前面的例子中,MR程序的输入都是文本文件,当然也可以自己实现InputFormat,从MySQL、Oracle甚至Socket端口读取输入数据;同样的,也可以实现OutputFormat将结果输出到数据库等。。

四、一些概念

1、文件切片:Split

1)reduce任务数可以通过job.setNumReduceTask()设置,那map任务数由什么决定?

问题场景:在运行TrafficStatistics_Sort程序时,采用TrafficStatistics程序的输出文件作为输入,运行时只启动一个map任务(可以通过clone session,并在新的窗口执行jps命令观察);如果采用TrafficStatistics_Partition程序的输出文件(共6个)作为输入,此时启动了6个map任务。那map任务数是由输入文件的数量决定的么?

Answer:说法错误Map任务数是由文件切片(Split)决定的。切片是MapReduce框架在做任务规划时所使用的一个概念,同文件分块(Block)一样,也是对文件的一种切分。区别在于Block是HDFS在存储文件时对文件进行分块并存储,是物理的概念;而Split是MapReduce在任务规划时对输入文件进行分片,一个分片就对应一个map任务进行处理,它是一个逻辑的概念。

2)Split大小和Block大小之间的关系?

Answer:默认情况下,一个Split对应一个Block。TrafficStatistics_Partition程序的输出6个文件,这6个文件用6个block存储,对应着6个split,因此会启动6个map任务。下面是一些设置Split大小的经验:

  • 如果Block大小适中,保持默认即可。
  • 如果Block特别大,为了提高Map的并发数,可以将Split的设置的比Block小。
  • 如果要处理的文件都是一些小文件,那么这些小文件所产生的Block也很小,为了提高效率,可以让一个Map任务多处理一些Block,这是就应该把Split设置得更大一些(小文件优化)。之所以设计出Split这个概念,一个重要的场景就是优化小文件。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值