Hadoop Map/Reduce 心得小结(一)

MapReduce 的数据类型

Hadoop 虽然是由java实现的,但MapReduce的输入输出并不支持对java常用的数据类型(如byte,int,long,String等)的直接操作,而是通过一个接口org.apache.hadoop.io.Writable实现一批间接的数据类型来取代之,比如BooleanWritable,IntWritable,ByteWritable,Text等等,另外,程序员还可以因需自行实现某种Writable类型。

采用Writable类型的理由简单而明显:使数据可转化为类型无关的数据流或反之,便于进行网络通信,类似的行为在很多远程调用(RPC)框架中十分常见。

Writable接口主要包含两个方法:
void write(DataOutput out)和void readFields(DataInput in)

从名称上即可很容易理解其含义与作用,具体解释请参阅hadoop的API文档。

MapReduce的两个版本

Hadoop目前实现了两个MapReduce版本,一个是Package org.apache.hadoop.mapred,另一个是Packageorg.apache.hadoop.mapreduce,前者为Hadoop早期的实现,后者是近期发布的版本中的新成员。经本人的考察,两者在使用上并不兼容(虽然在底层上他们共用了不少的代码)在编写自己的MaReduce程序时,二者只能选其一,不能混合。

本人在阅读了一些范例以及自己所作的编程尝试,认为org.apache.hadoop.mapreduce比org.apache.hadoop.mapred优越得多,思路更简明清晰,更易于掌握,代码复杂度也有所下降。

范例分析1:WordCount

几乎所有学习Hadoop的文章都是从这个范例开始着手的,这个范例既简单易理解又具有代表性,而且目前下载的Hadoop包的范例中,只有这个例子是采用org.apache.hadoop.mapreduce包的,其余均采用较老版本的org.apache.hadoop.mapred包,下面就通过代码来对MapReduce的编程基本思路作一些分析:

//WordCount.java:
package org.apache.hadoop.examples;

import java.io.IOException;
import java.util.StringTokenizer;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.util.GenericOptionsParser;

public class WordCount {
  //这就是传说中那神奇的MapReduce中的Mapper<KEYIN,VALUEIN,KEYOUT,VALUEOUT>入参、出参的类型并不强求一致,够自由。
  public static class TokenizerMapper 
       extends Mapper<Object, Text, Text, IntWritable>{
    
    private final static IntWritable one = new IntWritable(1);
    private Text word = new Text();
    //只需要实现一个函数,够省心。
    public void map(Object key, Text value, Context context
                    ) throws IOException, InterruptedException {
    //这个函数的输入<key,value>对中的key用不上,value是某文本文件中的某一行,这说明了一个问题,key或者value并不是必需的,可以缺少其中一样,具体看你需要map为你做什么。
      StringTokenizer itr = new StringTokenizer(value.toString());//把一行文字分拆为词汇迭代。
      while (itr.hasMoreTokens()) {
        word.set(itr.nextToken());
        //context有很多用途,不过主要还是为了收集输出的<key,value>对。
        context.write(word, one);
      }
    }
  }
  //这个Reduce的目的很简单,就是把key相同的<key,value>对集合在一起,统计它们出现的次数。
  public static class IntSumReducer 
       extends Reducer<Text,IntWritable,Text,IntWritable> {
    private IntWritable result = new IntWritable();

    public void reduce(Text key, Iterable<IntWritable> values, 
                       Context context
                       ) throws IOException, InterruptedException {
      int sum = 0;
      for (IntWritable val : values) {
        sum += val.get();
      }
      result.set(sum);
      context.write(key, result);
    }
  }

  public static void main(String[] args) throws Exception {
    Configuration conf = new Configuration();
    String[] otherArgs = new GenericOptionsParser(conf, args).getRemainingArgs();
    if (otherArgs.length != 2) {
      System.err.println("Usage: wordcount <in> <out>");
      System.exit(2);
    }
    //最能表达开发者的意图的就是这个Job了,对Job的设置可以控制MapReduce的行为,同时又不必考虑分布计算的复杂问题。
    Job job = new Job(conf, "word count");
    job.setJarByClass(WordCount.class);
    job.setMapperClass(TokenizerMapper.class);
    job.setCombinerClass(IntSumReducer.class);
    job.setReducerClass(IntSumReducer.class);
    job.setOutputKeyClass(Text.class);
    job.setOutputValueClass(IntWritable.class);
    FileInputFormat.addInputPath(job, new Path(otherArgs[0]));
    FileOutputFormat.setOutputPath(job, new Path(otherArgs[1]));
    System.exit(job.waitForCompletion(true) ? 0 : 1);
  }
}

理论上,开发者可以实现若干的Mapper类和若干的Reducer类随意打包到任意的jar文件中,然后通过job.setJarByClass()、job.setMapperClass()、job.setReducerClass()使job加载合适的Mapper与Reducer来达到目的。

以上代码看起来是很简单,条理也清晰,然而问题来了,数据是怎么输入到Mapper中的,又是如何从Reducer里输出来的呢?

看看文档中有关Job的内容,原来Mapper的输入是由public void setInputFormatClass()来决定的,范例中之所以没有使用这个设置是因为Hadoop默认情况下设置了org.apache.hadoop.mapreduce.lib.input. TextInputFormat这个类,而Reducer的输出是由public void setOutputFormatClass()来决定的,默认情况下设置了org.apache.hadoop.mapreduce.lib.output.TextOutputFormat这个类,范例中有两行代码
FileInputFormat.addInputPath(job,new Path(otherArgs[0]));

FileOutputFormat.setOutputPath(job, newPath(otherArgs[1]));

是设置输入文件与输出文件的路径的,job就是通过继承自FileInputFormat的TextInputFormat读入文件数据,交给Mapper去处理,然后把Reducer的输出交给继承自FileOutputFormat的TextOutputFormat类来写到输出文件中。

研读了FileInputFormat.java,InputFormat.java,FileOutputFormat.java,OutputFormat,TextInputFormat.java,TextOutputFormat.java的代码之后,可以摸清Hadoop对job的数据输入、输出细节:

由setInputFormatClass()指定的类创建实例,首先由实例中的getSplits()将数据分为若干片段(splits),然后将这些splits分发给各个TastTracker,由这些TaskTracker分别启用Mapper去处理,开发者可以通过编写getSplits()实现自己的数据输入途径以及如何将数据进行分割,范例中每个split只包含了三个内容:文件名、起始位置、长度,这是一种聪明的做法,因为split是需要通过网络发送给各个TastTracker的,传送文件名、起始位置、长度这三个信息比传送文件内容要节省网络的带宽消耗。

TaskTracker在得到split之后首先做的是将split进一步分解为<key,value>对,然后为每一对<key,value>调用一次Mapper中的map方法,范例中通过org.apache.hadoop.mapreduce.lib.input.LineRecordReader根据split的文件名、起始位置和长度来读取文本文件数据块,然后每行文本当做一个<key,value>对来调用map()。
Reducer的输出由setOutputFormatClass ()指定的类创建实例,这个实例通过一个org.apache.hadoop.mapreduce.RecordWriter来进行实质的输出动作,范例中使用的是LineRecordWriter类。

MapReduce的细节干预

上例中展示了MapReduce应用编程的基本思路与内容,当然Hadoop提供的远不止这些,通过更细致的设置,可以让Hadoop的表现更佳,上例中有一行:job.setCombinerClass(IntSumReducer.class);居然和job.setReducerClass(IntSumReducer.class);的参数是一样的,这又是干什么用的呢?

参看文档的解释:map得到中间结果之后,可以对中间结果先做 combine,即将中间结果中有相同 key的 <key, value> 对合并成一对。combine 的过程与 Reduce 的过程类似,很多情况下就可以直接使用 Reduce 函数,但 combine 是作为 Map 任务的一部分,在执行完 Map 函数后紧接着执行的。Combine 能够减少中间结果中 <key, value> 对的数目,从而减少网络流量。

public void setSortComparatorClass():如果job设置了这个项,程序可以实现一个org.apache.hadoop.io.RawComparator类更细致地控制如何对中间结果进行排序处理。
public void setPartitionerClass():如果job设置了这个项,程序可以实现一个org.apache.hadoop.mapreduce.Partitioner类更细致地控制如何把中间结果分派给多个Reducer来进行后续的处理。
public void setGroupingComparatorClass():默认情况下,每个Reducer实例处理key相同的value集合,如果job设置了这个项,程序可以实现一个org.apache.hadoop.io.RawComparator类把多个key相应的value组合在一起交给同一个Reducer实例来处理。


Hadoop的org.apache.hadoop.mapreduce.InputFormatorg.apache.hadoop.mapreduce.OutputFormat类都是抽象类,在现有的Hadoop实现中,派生出了若干输入输出类,但都具有一个共同的特点:以文件为输入输出对象。Hadoop的examples代码集中全部都是以磁盘文件作为输入源以及输出目标,如果我们希望像大多数普通的程序那样把内存数据作为输入源和输出目标,Hadoop建议采用Stream或者Pipe的方法,然后这两种方法都只适合于操作文本数据(包括中间结果),假如是任意的二进制数据,这两种方法都不适用,因此,我写了一个尝试将内存数据直接作为MapReduce传入传出参数的项目。(待续)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值