Hadoop--兩個簡單的MapReduce程序

轉自:http://www.linuxidc.com/Linux/2013-08/88631.htm

這周在學習Hadoop編程,以前看過《Hadoop權威指南》這本書,但是看完了HDFS這一章之後,後面的內容就難以再看懂了,說實話,之前一直對MapReduce程序敬而遠之,毫不理解這種類型的程序的執行過程。這一週花了些時間看了Hadoop的實戰,現在能夠看懂簡單的MapReduce程序,也能自己動手寫幾個簡單的例子程序。

相關閱讀:

Hadoop權威指南(中文版-帶目錄索引)PDF  http://www.linuxidc.com/Linux/2013-05/84948.htm
Hadoop權威指南(中文第2版)PDF  http://www.linuxidc.com/Linux/2012-07/65972.htm

下面是兩個簡單的MapReduce程序,用到了一些簡單的Hadoop知識點,總結如下文。

源碼下載:

**************************************************************

下載在Linux公社的1號FTP服務器裏,下載地址:

FTP地址:ftp://www.linuxidc.com/

用戶名:www.6688.cc

密碼:www.linuxidc.com

在 2013年LinuxIDC.com\8月\Hadoop--兩個簡單的MapReduce程序

下載方法見 http://www.linuxidc.net/thread-1187-1-1.html

**************************************************************

例子一  求最大數

問題描述是這樣的,從一系列數中,求出最大的那一個。這個需求應該說是很簡單的,如果不用MapReduce來實現,普通的Java程序要實現這個需求,應該說是輕而易舉的,幾行代碼就能搞定。這裏用這個例子是想說說Hadoop中的Combiner的用法。

我們知道,Hadoop使用Mapper函數將數據處理成一個一個的<key, value>鍵值對,再在網絡節點間對這些鍵值對進行整理(shuffle),然後使用Reducer函數處理這些鍵值對,並最終將結果輸出。那麼可以這樣想,如果我們有1億個數據(Hadoop就是爲大數據而生),Mapper函數將會產生1億個鍵值對在網絡中進行傳輸,如果我們只是要求出這1億個數當中的最大值,那麼顯然,Mapper只需要輸出它所知道的最大值即可。這樣一來可以減輕網絡帶寬的壓力,二來,可以減輕Reducer的壓力,提高程序的效率。

如果Reducer只是運行簡單的諸如求最大值、最小值、計數,那麼我們可以使用Combiner,但是,如果是求一組數的平均值,千萬別用Combiner,道理很簡單,你自己分析看。Combiner可以看作是Reducer的幫手,或者看成是Mapper端的Reducer,它能減少Mapper函數的輸出從而減少網絡數據傳輸並能減少Reducer上的負載。下面是Combiner的例子程序。

程序的輸入是這樣的:

12
5
9
21
43
99
65
32
10

MapReduce程序需要找到這一組數字中的最大值99,Mapper函數是這樣的:

public class MyMapper extends Mapper<Object, Text, Text, IntWritable>{
 
 @Override
 protected void map(Object key, Text value,Context context)throws IOException, InterruptedException {
  // TODO Auto-generated method stub
  context.write(new Text(), new IntWritable(Integer.parseInt(value.toString())));
 }
 
}

Mapper函數非常簡單,它是負責讀取HDFS中的數據的,負責將這些數據組成<key, value>對,然後傳輸給Reducer函數。Reducer函數如下:

public class MyReducer extends Reducer<Text, IntWritable, Text, IntWritable>{

 @Override
 protected void reduce(Text key, Iterable<IntWritable> values,Context context)throws IOException, InterruptedException {
  // TODO Auto-generated method stub
  int temp = Integer.MIN_VALUE;
  for(IntWritable value : values){
   if(value.get() > temp){
    temp = value.get();
   }
  }
  context.write(new Text(), new IntWritable(temp));
 }
}

Reducer函數也很簡單,就是負責找到從Mapper端傳來的數據中找到最大值。那麼在Mapper函數與Reducer函數之間,有個Combiner,它的代碼是這樣的:

public class MyCombiner extends Reducer<Text, IntWritable, Text, IntWritable> {

 @Override
 protected void reduce(Text key, Iterable<IntWritable> values,Context context)throws IOException, InterruptedException {
  // TODO Auto-generated method stub
  int temp = Integer.MIN_VALUE;
  for(IntWritable value : values){
   if(value.get() > temp){
    temp = value.get();
   }
  }
  context.write(new Text(), new IntWritable(temp));
 }
}

我們可以看到,combiner也是繼承了Reducer類,其寫法與寫reduce函數一樣,reduce和combiner對外的功能是一樣的,只是使用時的位置和上下文(Context)不一樣而已。定義好了自己的Combiner函數之後,需要在Job類中加入一行代碼,告訴Job你使用要在Mapper端使用Combiner:

job.setCombinerClass(MyCombiner.class);

那麼這個求最大數的例子的Job類是這樣的:

public class MyMaxNum {

public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
Configuration conf = new Configuration();
Job job = new Job(conf,"My Max Num");
job.setJarByClass(MyMaxNum.class);
job.setMapperClass(MyMapper.class);
job.setReducerClass(MyReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
job.setCombinerClass(MyCombiner.class);
FileInputFormat.addInputPath(job, new Path("/huhui/nums.txt"));
FileOutputFormat.setOutputPath(job, new Path("/output"));
System.exit(job.waitForCompletion(true) ? 0:1);
}
}

當然你還可以對輸出進行壓縮。只要在函數中添加兩行代碼,就能對Reducer函數的輸出結果進行壓縮。當然這裏沒有必要對結果進行壓縮,只是作爲一個知識點而已。

//對輸出進行壓縮
conf.setBoolean("mapred.output.compress", true);
conf.setClass("mapred.output.compression.codec", GzipCodec.class, CompressionCodec.class);



例子二  自定義Key的類型

這個例子主要講述如果自定義<key, value>的key的類型,以及如果如何使用Hadoop中的比較器WritableComparator和輸入格式KeyValueTextInputFormat。

需求是這樣的,給定下面一組輸入:

str1 2
str2 5
str3 9
str1 1
str2 3
str3 12
str1 8
str2 7
str3 18

希望得到的輸出如下:

str1    1,2,8
str2    3,5,7
str3    9,12,19

請注意,輸入格式KeyValueTextInputFormat只能針對key和value中間使用製表符\t隔開的數據,而逗號是不行的。
對於這個需求,我們需要自定義一個key的數據類型。在Hadoop中,自定義的key值類型都要實現WritableComparable接口,然後重寫這個接口的三個方法。這裏我們定義IntPaire類,它實現了WritableComparable接口:
public class IntPaire implements WritableComparable<IntPaire> {
 
 private String firstKey;
 private int secondKey;
 @Override
 public void readFields(DataInput in) throws IOException {
  // TODO Auto-generated method stub
  firstKey = in.readUTF();
  secondKey = in.readInt();
 }
 @Override
 public void write(DataOutput out) throws IOException {
  // TODO Auto-generated method stub
  out.writeUTF(firstKey);
  out.writeInt(secondKey);
 }
 @Override
 public int compareTo(IntPaire o) {
  // TODO Auto-generated method stub
  return o.getFirstKey().compareTo(this.firstKey);
 }
 public String getFirstKey() {
  return firstKey;
 }
 public void setFirstKey(String firstKey) {
  this.firstKey = firstKey;
 }
 public int getSecondKey() {
  return secondKey;
 }
 public void setSecondKey(int secondKey) {
  this.secondKey = secondKey;
 }
}

上面重寫的readFields方法和write方法,都是這樣寫的,幾乎成爲模板。

由於要將相同的key的鍵/值對送到同一個Reducer哪裏,所以這裏要用到Partitioner。在Hadoop中,將哪個key到分配到哪個Reducer的過程,是由Partitioner規定的,這是一個類,它只有一個抽象方法,繼承這個類時要覆蓋這個方法:

getPartition(KEY key, VALUE value, int numPartitions)

其中,第一個參數key和第二個參數value是Mapper端的輸出<key, value>,第三個參數numPartitions表示的是當前Hadoop集羣一共有多少個Reducer。輸出則是分配的Reducer編號,就是指的是Mapper端輸出的鍵對應到哪一個Reducer中去。我們一般實現Partitioner是哈希散列的方式,它以key的hash值對Reducer的數目取模,得到對應的Reducer編號。這樣就能保證相同的key值,必定會分配到同一個reducer上。如果有N個Reducer,那麼編號就是0,1,2,3......(N-1)。

那麼在本例子中,Partitioner是這樣實現的:

public class PartitionByText extends Partitioner<IntPaire, IntWritable> {

 @Override
 public int getPartition(IntPaire key, IntWritable value, int numPartitions) {//reduce的個數
  // TODO Auto-generated method stub
  return (key.getFirstKey().hashCode() & Integer.MAX_VALUE) % numPartitions;
 }
}

本例還用到了Hadoop的比較器WritableComparator,它實現的是RawComparator接口。

public class TextIntComparator extends WritableComparator {
 
 public TextIntComparator(){
  super(IntPaire.class,true);
 }

 @Override
 public int compare(WritableComparable a, WritableComparable b) {
  // TODO Auto-generated method stub
  IntPaire o1 = (IntPaire) a;
  IntPaire o2 = (IntPaire) b;
  if(!o1.getFirstKey().equals(o2.getFirstKey())){
   return o1.getFirstKey().compareTo(o2.getFirstKey());
  }else{
   return o1.getSecondKey() - o2.getSecondKey();
  }
 }
 
}

由於我們在key中加入的額外的字段,所以在group的時候需要手工設置,手工設置很簡單,因爲job提供了相應的方法,在這裏,我們的group比較器是這樣實現的:

public class TextComparator extends WritableComparator {
 
 public TextComparator(){
  super(IntPaire.class,true);
 }

 @Override
 public int compare(WritableComparable a, WritableComparable b) {
  // TODO Auto-generated method stub
  IntPaire o1 = (IntPaire) a;
  IntPaire o2 = (IntPaire) b;
  return o1.getFirstKey().compareTo(o2.getFirstKey());
 }
 
}

下面將寫出Mapper函數,它是以KeyValueTextInputFormat的輸入形式讀取HDFS中的數據,設置輸入格式將在job中。

public class SortMapper extends Mapper<Object, Text, IntPaire, IntWritable>{

 public IntPaire intPaire = new IntPaire();
 public IntWritable intWritable = new IntWritable(0);
 
 @Override
 protected void map(Object key, Text value,Context context)throws IOException, InterruptedException {
  // TODO Auto-generated method stub
  int intValue = Integer.parseInt(value.toString());
  intPaire.setFirstKey(key.toString());
  intPaire.setSecondKey(intValue);
  intWritable.set(intValue);
  context.write(intPaire, intWritable);//key:str1  value:5
 }
}

下面是Reducer函數,

public class SortReducer extends Reducer<IntPaire, IntWritable, Text, Text> {

 @Override
 protected void reduce(IntPaire key, Iterable<IntWritable> values,Context context)throws IOException, InterruptedException {
  // TODO Auto-generated method stub
  StringBuffer combineValue = new StringBuffer();
  Iterator<IntWritable> itr = values.iterator();
  while(itr.hasNext()){
   int value = itr.next().get();
   combineValue.append(value + ",");
  }
  int length = combineValue.length();
  String str = "";
  if(combineValue.length() > 0){
   str = combineValue.substring(0, length-1);//去除最後一個逗號
  }
  context.write(new Text(key.getFirstKey()), new Text(str));
 }
 
}

Job類是這樣的:

public class SortJob {
 public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
  Configuration conf = new Configuration();
  Job job = new Job(conf, "Sortint");
  job.setJarByClass(SortJob.class);
  job.setMapperClass(SortMapper.class);
  job.setReducerClass(SortReducer.class);
  
  //設置輸入格式
  job.setInputFormatClass(KeyValueTextInputFormat.class);
  
  //設置map的輸出類型
  job.setMapOutputKeyClass(IntPaire.class);
  job.setMapOutputValueClass(IntWritable.class);
  
  //設置排序
  job.setSortComparatorClass(TextIntComparator.class);
  
  //設置group
  job.setGroupingComparatorClass(TextComparator.class);//以key進行grouping
  
  job.setPartitionerClass(PartitionByText.class);
  job.setOutputKeyClass(Text.class);
  job.setOutputValueClass(Text.class);
  FileInputFormat.addInputPath(job, new Path("/huhui/input/words.txt"));
  FileOutputFormat.setOutputPath(job, new Path("/output"));
  System.exit(job.waitForCompletion(true)?0:1);
 }
}

這樣一來,程序就寫完了,按照需求,完成了相應的功能。

後記

剛開始接觸MapReduce程序可能會感到無從下手,這可能是因爲你還沒有理解MapReduce的機制和原理。自己動手寫寫簡單的MapReduce函數會有助於理解,然後逐步的深入學習。

更多Hadoop相關信息見Hadoop 專題頁面 http://www.linuxidc.com/topicnews.aspx?tid=13



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章