mapreduce是一個很精巧的構思和設計,對於很多計算問題,程序員通常可以使用默認設置取處理諸多底層的細節,但是,這並不意味着在解決復雜問題時,程序員就可以完全使用這二個函數就可以搞定一切,它還需要更加復雜和靈活的處理機制以及高級的編程技術和方法。本節介紹hadoop中mapreduce比較高級的方法---用戶制定類。為什么要用戶自定義類,一種直觀的猜測就是基本的mapreduce處理不了或者處理的效果不好,才有可能用到用戶制定,隨着深入的學習,這些問題到處可見。比如文檔的倒排索引,制定(優化)的目的就是減少中間鍵,從而減少每次的讀寫I/O和網絡的壓力。
1. 用戶自定義數組類型
雖然hadoop中內置8種數據類型,我見過8種,但不一定就8種,這里就認為是8種吧,他們都實現了WritableComparable接口,這種好處就是可以被序列化進行網路傳輸和文件存儲。BooleanWritable, ByteWritable, FloatWritable, IntWritable,LongWritable,Text, NullWritble,前幾種大家都可以猜到表示是什么數據類型,Text表示使用UTF格式的存儲的文本。好像visual stdio 中text控件,也是對String類型的一種包裝。
自定義類型必須符合二個條件,第一:實現Writable接口。第二:如果該函數需要作為主鍵Key使用,或者要比較數值大小時,則要實現WritableComparable接口。下例子是一個圓為例。circle
1 public class Cricle implements Writable<Circle>{ 2 private float radius,x,y; 3 public float GetRadius(){return radius;} 4 public float GetX(){return x;} 5 public float GetY(){return y;} 6 public void readFields(DataInput in)throws IOException{ 7 radius=in.readFloat(); 8 x=in.readFloat(); 9 y=in.readFloat(); 10 } 11 public void write(DataOutput out)throws IOException{ 12 out.writeFloat(radius); 13 out.writeFloat(x); 14 out.writeFloat(y); 15 } 16 public int CompareTo(Circle cl){ 17 if(cl.radius==this.radius) 18 return 0; 19 if(cl.x>this.x) 20 return 1; 21 if(cl.y<this.y) 22 return -1; 23 } 24 }
以上代碼就是手寫的,沒有進行測試,肯定還有錯誤,只是向說明問題。readFields() 和write()實現Writable接口中定義的二中方法。
2. 用戶制定輸入/輸出
盡管Hadoop提供了較為豐富的數據輸入/輸出格式,可以滿足很多應用的需要,但是,對於特殊的要求,還是要用戶自己制定。比如說,Hadoop默認的數據輸入格式為TextInputFormat,可以將文本文件分塊並逐行讀入以便Map節點進行處理,每行所產生的Key是文本的字節位置的偏移量,而value就是該行的內日內個。但現在需要文件名@文本偏移量作為key,所以就需要制定輸入類,假定現在制定一個數據輸入格式FileNameInputFormat和fileNameRecordReader,比便直接產生FileName@lineoffset.
public class FileNameInputFormat extends FileInputForamt<Text,Text>{ public RecordReader<Text,Text>createRecordReader(InputSplit split,TaskAttemptContext context)throws IOException{ FileNameRecordReader fnrr = new FileNameRecordReader(); fnrr.initialize(split,context); } }
public class FileNameRecordReader extends RecordReader<Text, Text> { String FileName; LineRecordReader lrr = new LineRecordReader(); public Text getCurrentKey() throws IOException { return new Text("(" + FileName + "@" + lrr.getCurrentKey() + ")"); } public Text getCurrentValue() throws IOException, InterruptedException { return new Text(lrr.getCurrentKey().toString()); } public void initialize(InputSplit arg0, TaskAttemptContext arg1) throws IOException { lrr.initialize(arg0, arg1); FileName = ((FileSplit) arg0).getPath().getName(); } }
沒事什么好講的,仔細看都能看的懂,輸出就不講了,和輸入差不多。
3. 用戶制定Patritioner和Combiner
Patritioner來完成中間結果向Reduce節點分區處理,通常是利用hash進行分配,hash划分主要是靠主鍵進行划分,即相同的主鍵划分到相同桶中,在mapreduce中就是划分到相同的reduce中。那么現在設想一個問題,我需要將這樣的數據划分到一個reduce中,該怎么辦? 數據為 hello:1, hello:2,hello:3,hello:4,......., 顯然,用hash直接划分是不行的,這是我們可以觀察到這些數據都具有一個hello,我們只需要在提取hello作為主鍵,在用hash划分,就可以把這些數據都划分到同一個reduce中,下面就個例子,給出Patritioner代碼:
public class NParterioner extends HashPartitioner<Text,Text> { public getPartition(Text key,Text value, int numReduceTasks){ String t=key.toString().split(":")[0]; Text term; term.set(t); super.getPartition(term,value,numReduceTasks); } }
還有像map,reduce, combiner的制定都是一樣的道理。
總結:
用戶類的制定主要取決於特定的應用的場合,但其目標都是不變的(減少中間結果來減小I/O和網絡傳輸數據的壓力)。
作者:BIGBIGBOAT/Liqizhou