第八篇:經典案例 - 排序

穆晨發表於2017-05-20

前言

       在計算機領域,排序的重要性不用多說。而排序的演算法,效率分析等也一直是研究的熱點。

       本文將給出使用Hadoop分散式方案進行排序的例子,這能極大提高排序的速度,是需要重點掌握的一個案例。

需求

       對輸入檔案中的資料進行排序。

       輸入檔案中的每行內容都是一個數字,要求在輸出檔案中每行有兩個數字,第一個數字代表位次,第二個數字為原始資料。

       比如檔案1包含以下資料:

       1

       3

       5

       2

       4

       6

       檔案2包含以下資料:

       2

       4

       6

       3

       1

       5

       那麼輸出檔案應當為:

       1  1

       2  1

       3  2

       4  2

       ...

方案制定

       表面上看,這是一個非常簡單的例子 - Hadoop中存放的鍵值對本身就是有序的,直接將輸入存放進來然後再取出來就完成排序了。

       但事實上,直接這樣做行不通。為何?因為預設的排序過程是在單個的節點上完成的。也就是說,每個reduce節點收到鍵值對是在該節點區域性有序,而不是在所有reduce節點裡全域性有序。

       解決之道是重寫Partition方法,請仔細閱讀以下內容:

       在shuffle階段之後(或者說是shuffle最後),將根據map中間輸出鍵值對中的key值來決定將此鍵值對劃分給哪個Partition區間,或者說哪個reduce節點。

       可以根據資料的最大最小值將資料劃分為多個區間,這樣,每個reduce節點就能獲取到某個資料段的完整的資料,而且根據hadoop特性,這些資料在單個的reduce節點之內都是有序存放的。

       因此每個reduce節點的任務很簡單,輸出結果就可以了。

       至於說位次,只需要在reduce類中宣告一個static變數,讓這個static變數在不同的reduce呼叫之間共享就可以了。

       要說明的是這裡統計的只是資料在每個reduce節點之內的位次,如果要獲得全域性位次,則需要再遍歷一次所有reduce輸出檔案。時間複雜度僅為O(n)。

程式碼實現

  1 package org.apache.hadoop.examples;
  2 
  3 import java.io.IOException;
  4 
  5 //匯入各種Hadoop包
  6 import org.apache.hadoop.conf.Configuration;
  7 import org.apache.hadoop.fs.Path;
  8 import org.apache.hadoop.io.IntWritable;
  9 import org.apache.hadoop.io.Text;
 10 import org.apache.hadoop.mapreduce.Job;
 11 import org.apache.hadoop.mapreduce.Mapper;
 12 import org.apache.hadoop.mapreduce.Partitioner;
 13 import org.apache.hadoop.mapreduce.Reducer;
 14 import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
 15 import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
 16 import org.apache.hadoop.util.GenericOptionsParser;
 17 
 18 // 主類
 19 public class Sort {
 20         
 21     // Mapper類
 22     public static class Map extends Mapper<Object, Text, IntWritable, IntWritable>{
 23         
 24         // new一個值為1的IntWritable物件
 25         private static IntWritable data = new IntWritable(1);
 26                 
 27         // 實現map函式
 28         public void map(Object key, Text value, Context context) throws IOException, InterruptedException {
 29             
 30             // 將切分後的value作為中間輸出的key,然後value值為1。
 31             String line = value.toString();
 32             data.set(Integer.parseInt(line));
 33             context.write(data, new IntWritable(1));
 34         }
 35     }
 36         
 37     // Reducer類
 38     public static class Reduce extends Reducer<IntWritable, IntWritable, IntWritable, IntWritable> {
 39     
 40         // new一個值為空的IntWritable物件
 41         private static IntWritable linenum = new IntWritable();
 42                 
 43         // 實現reduce函式
 44         public void reduce(IntWritable key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
 45                 
 46             // 寫入結果鍵值對
 47             for (IntWritable val : values) {
 48                 context.write(linenum, key);
 49                 linenum = new IntWritable(linenum.get()+1);
 50             }
 51         }
 52     }
 53 
 54     // 重寫Partitioner類
 55     public static class Partition extends Partitioner <IntWritable, IntWritable> {
 56         
 57         // 過載getPartition方法。下面的三個引數分別為map中間輸出的鍵,值,以及分割區間的個數。
 58         public int getPartition(IntWritable key, IntWritable value, int numPartitions) {
 59             
 60             // 依次將鍵值對分配到各個分割區間
 61             int MaxNumber = 65223;
 62             int bound = MaxNumber/numPartitions + 1;
 63             int keynumber = key.get();
 64             
 65             for (int i=0; i<numPartitions; i++) {
 66                 if (keynumber < bound * (i+1) && keynumber >= bound*i) {
 67                     
 68                     // 返回的 i 就是分配到的區間號
 69                     return i;
 70                 }
 71             }
 72             
 73             return -1;
 74         }
 75     }
 76     
 77     // 主函式
 78     public static void main(String[] args) throws Exception {
 79     
 80         // 獲取配置引數
 81         Configuration conf = new Configuration();
 82         String[] otherArgs = new GenericOptionsParser(conf, args).getRemainingArgs();
 83                 
 84         // 檢查命令語法
 85         if (otherArgs.length != 2) {
 86             System.err.println("Usage: Dedup <in> <out>");
 87             System.exit(2);
 88         }
 89 
 90         // 定義作業物件
 91         Job job = new Job(conf, "Sort");
 92         // 註冊分散式類
 93         job.setJarByClass(Sort.class);
 94         // 註冊Mapper類
 95         job.setMapperClass(Map.class);
 96         // 註冊Reducer類
 97         job.setReducerClass(Reduce.class);
 98         // 註冊Partition類
 99         job.setPartitionerClass(Partition.class);
100         // 註冊輸出格式類
101         job.setOutputKeyClass(IntWritable.class);
102         job.setOutputValueClass(IntWritable.class);
103         // 設定輸入輸出路徑
104         FileInputFormat.addInputPath(job, new Path(otherArgs[0]));
105         FileOutputFormat.setOutputPath(job, new Path(otherArgs[1]));
106                 
107         // 執行程式
108         System.exit(job.waitForCompletion(true) ? 0 : 1);
109     }
110 }

執行結果

       輸入檔案1,2分別為:

       

       

小結

1. 掌握Partitioner方法的重寫技巧,這是本程式最核心的部分。

2. 熟悉hadoop的key預設有序的性質。

3. 本文采取的是偽分散式,故只有1個reduce節點,體現不出hadoop的優越性。當對海量資料進行排序的時候,它的速度價值才能真正體現出來。

相關文章