Data Mining,NLP,Search Engine
Hadoop的InputFormats和OutputFormats
InputFormat
InputFormat類用來產生InputSplit,並把它切分成record。
public interface InputFormat<K, V> { InputSplit[] getSplits(JobConf job, int numSplits) throws IOException; RecordReader<K, V> getRecordReader(InputSplit split,JobConf job,Reporter reporter) throws IOException; }
有時候一個文件雖然大於一個block size,但是你不希望它被切分,一種辦法上把mapred.min.split.size提高到比該文件的長度還要大,另一個辦法是自定義FileInputFormat的子類,讓isSplitable()方法返回false。
InputSplit是由客戶端計算出來提交給JobTracker的,JobTracker把它存放在Configuration中,mapper可以從中獲取split的信息。
map.input.file String 被map處理的文件的路徑
map.input.start long 從split的開頭偏移的字節量
map.input.length long split的長度
FileInputFormat
有3個類實現了InputFormat接口中:DBInputFormat, DelegatingInputFormat, FileInputFormat。FileInputFormat是所有數據來自文件的InputFormat的基類。
通過FileInputFormat可以指定輸入文件有哪些,而且FileInputFormat實現了InputFormat中的getSplits()方法,getRecordReader()則需要由FileInputFormat的子類來實現。
Path可以代表一個文件、一個目錄、文件或目錄的集合(通過使用glob)。
如果把一個目錄傳遞給FileInputFormat.addPath(),這不是遞歸的模式,即該目錄下的目錄是不會作爲輸入數據源的,並且這種情況下會引發錯誤。解決辦法是使用glob或filter來僅選擇目錄下的文件。下面看一下如何使用FileInputFormat的靜態方法設置filter:
static void setInputPathFilter(Job job, Class<? extends PathFilter> filter)
即使你不設置filter,默認情況下FileInputFormat也會把隱藏文件過濾掉。
你或許會問,通過調用FileInputFormat.addPath()得到了很多輸入文件,FileInputFormat如何把它們切分成input split呢?事實上,FileInputFormat只切分大文件,對於小於一個HDFS block的文件它獨自產生一個input split。HDFS block的大小可以通過ds.block.size來設置。
CombineFileInputFormat
前文已經說過很多小文件帶來2個麻煩:namenode上要維護很多文件的metadata,容易造成namenode內存溢出;很多小文件就會產生很多inputsplit,從而產生很多map task,而map task的啓動、銷燬、監控都會帶來額外的開銷。使用Hadoop Archive(HAR)可以解決第一個問題,但不能解決第2個問題,因爲對MapReduce來說看到的還歸檔前的文件。使用SequenceFile不僅可以合併文件,還有壓縮的作用,節省磁盤空間。但是如果你現在已經有了諸多小文件,在進行MapReduce之前把它們合併成一個SequenceFile可能還得不償失,這時候你可以用CombineFileInputFormat。CombineFileInputFormat並沒的把原文件合併,它只是對於MapReduce來說產生了較少的InputSplit(通過把多個文件打包到一個inputsplit中--當然是把鄰近的文件打包進同一個split,這樣map task離它要處理的文件都比較近)。使用CombineFileInputFormat時你可能需要調節split的大小,一個split的最大(默認爲Long.MAX_VALUE)和最小長度(默認爲1B)可以通過mapred.min.split.size和mapred.max,split.size來設置。
CombineFileInputFormat是抽象類,它裏面有一個抽象方法需要子類來實現:
abstract RecordReader<K,V> createRecordReader(InputSplit split, TaskAttemptContext context)
自定義InputFormat
TextInputFormat是默認的InputFormat,一行爲一個record,key是該行在文件中的偏移量,value是該行的內容。
經常我們需要自定義InputFormat。我們希望一行爲一個record,但key不是該在文件中的偏移量,而是行號,下面的代碼是讀取存儲了一個二維矩陣的文件,我們需要知道每行的行號,從而知道矩陣中的行索引。
package basic; import java.io.IOException; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.IntWritable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.InputSplit; import org.apache.hadoop.mapreduce.JobContext; import org.apache.hadoop.mapreduce.RecordReader; import org.apache.hadoop.mapreduce.TaskAttemptContext; import org.apache.hadoop.mapreduce.lib.input.FileInputFormat; public class MatrixInputFormat extends FileInputFormat<IntWritable,Text>{ @Override public RecordReader<IntWritable, Text> createRecordReader( InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException { return new MatrixLineRecordReader(); } /*因爲讀入時要記錄行號,所以要保證中有一個mapper,這樣行號纔是一致的*/ @Override protected boolean isSplitable(JobContext context, Path filename){ return false; } }
package basic; import java.io.IOException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FSDataInputStream; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.IntWritable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.InputSplit; import org.apache.hadoop.mapreduce.RecordReader; import org.apache.hadoop.mapreduce.TaskAttemptContext; import org.apache.hadoop.mapreduce.lib.input.FileSplit; import org.apache.hadoop.util.LineReader; public class MatrixLineRecordReader extends RecordReader<IntWritable, Text> { private LineReader in; private int lineno=0; private boolean more=true; private IntWritable key=null; private Text value=null; private Log LOG=LogFactory.getLog(MatrixLineRecordReader.class); @Override public void initialize(InputSplit inputSplit, TaskAttemptContext context) throws IOException, InterruptedException { FileSplit split=(FileSplit)inputSplit; Configuration conf=context.getConfiguration(); Path file=split.getPath(); FileSystem fs=file.getFileSystem(conf); FSDataInputStream fileIn=fs.open(file); in=new LineReader(fileIn,conf); } @Override public boolean nextKeyValue() throws IOException, InterruptedException { LOG.info("line number is "+lineno); if(key==null) key=new IntWritable(); if(value==null) value=new Text(); int readsize=in.readLine(value); LOG.info("line content is "+value.toString()); if(readsize==0){ more=false; return false; } key.set(lineno); lineno++; return true; } @Override public IntWritable getCurrentKey() throws IOException, InterruptedException { return key; } @Override public Text getCurrentValue() throws IOException, InterruptedException { return value; } @Override public float getProgress() throws IOException, InterruptedException { if(more) return 0.0f; else return 100f; } @Override public void close() throws IOException { in.close(); } }
有一個細節問題:如果一個split剛好從一個record的中間切開怎麼辦?放心,這種情況下split會自動擴容以容納下最後一條record,也就是說split會比block size略長。
Multiple Inputs
有時數據來源於不同的文件,它們包含的信息相同,但格式(InputFormat)不同,我們需要用不同的RecordReader來解析。這時可以調用MultipleInputs的靜態方法:
static void addInputPath(Job job, Path path, Class<? extends InputFormat> inputFormatClass)
甚至有時候對不同的數據來源需要使用不同的map來處理:
static void addInputPath(Job job, Path path, Class<? extends InputFormat> inputFormatClass, Class<? extends Mapper> mapperClass)
注意不同的map output要有相同的類型,因爲它們要被同一個reducer處理,且這些mapper Emit(key,value)的順序是不可預知的。這時候就不要再調用FileInputFormat.addInputPath()和job.setMpperClass()了。
自定義OutputFormat
TextOutputFormat是默認的OutputFormat,每條record佔一行,其key 和value都是Text類型,用tab隔開(這個分隔符可以通過mapred.textoutputformat.separator設置)。你還可設置key或者value爲空,只要設爲類型NullWritable(這是一個單例模式)就可以了,此時分隔符也就不會被輸出了。
一般我們並不需要實現一個FileOutputFormat的子類(這之間還要實現一個RecordWriter的子類),而只需要實現一個Writable的子類,重寫它的toString()方法就可以了--這種方法確實更簡單一些。
Multiple Outputs
一般情況下一個reducer產生一個輸出文件(文件大小與block size無關),命名爲part-r-xxxxx,xxxxx是從0開始的序號。FileOutputFormat及其子類產生的輸出文件都在同一個目錄下。有時候你可能想控制輸出文件的名稱,或者想讓一個reducer產生多個輸出文件。
MultipleOutputFormat(按行劃分到不同的文件)
假如MapReduce輸出瞭如下的一些record:
Jim Class1 male
Lili Class2 female
Tom Class1 male
Mei Class2 female
我們想按class把record輸出到不同的文件,只需要自定義一個MultipleOutputFormat類,然後把設置爲OutputFormat就可以了。
public class PertitionByClass extends MultipleTextOutputFormat<NullWritable,Text>{ @Override protected String generateFileNameForKeyValue(NullWritable key,Text value,String filenmae){ String []arr=value.toString().split("\\s+"); return arr[1]; } }
job.setOutputFormat(PertitionByClass);
MultipleTextOutputFormat繼承自MultipleOutputFormat。
MultipleOutputs(按列分到不同的文件)
如果想輸出爲2個文件,一個文件存放“姓名,班級”,另一個文件存放“姓名,性別”。
在Reducer中
public class MyReducer extends Reducer<NullWritable, Text, NullWritable, Text> { private MultipleOutputs<NullWritable, Text> mos; @Override public void setup(Context context){ mos=new MultipleOutputs<NullWritable, Text>(context); } @Override public void reduce(NullWritable key,Iterable<Text> values,Context context)throws IOException,InterruptedException{ while(values.iterator().hasNext()){ Text text=values.iterator().next(); String []arr=text.toString().split("\\s+"); String nc=arr[0]+","+arr[1]; //姓名,班級 String nf=arr[0]+","+arr[2]; //姓名,性別 mos.write("nc", NullWritable.get(), new Text(nc)); mos.write("nf", NullWritable.get(), new Text(nf)); } } @Override public void cleanup(Context context)throws IOException,InterruptedException{ mos.close(); } }
然後在driver中調用MultipleOutputs的靜態方法addNamedOutput()
MultipleOutputs.addNamedOutput(job,"nc",TextOutputFormat.class,NullWritable.class,Text.class); MultipleOutputs.addNamedOutput(job,"nf",TextOutputFormat.class,NullWritable.class,Text.class);
輸出文件名中包含"nc"或"nf"作爲區別。
解釋一下上面用到的幾個函數
static void addNamedOutput(Job job, String namedOutput, Class<? extends OutputFormat> outputFormatClass, Class<?> keyClass, Class<?> valueClass) Adds a named output for the job. <K,V> void write(String namedOutput, K key, V value) Write key and value to the namedOutput. <K,V> void write(String namedOutput, K key, V value, String baseOutputPath) Write key and value to baseOutputPath using the namedOutput.