hadoop如何自定義InputFormats和OutputFormats

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接口中:DBInputFormatDelegatingInputFormatFileInputFormat。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.
複製代碼
原文來自:博客園(華夏35度)http://www.cnblogs.com/zhangchaoyang 作者:Orisun
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章