前言
在計算機領域,排序的重要性不用多說。而排序的演算法,效率分析等也一直是研究的熱點。
本文將給出使用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的優越性。當對海量資料進行排序的時候,它的速度價值才能真正體現出來。