mapreduce自定義inputformat

背景

無論是 hdfs 存儲文件還是 mapreduce 處理文件,對於小文件的存儲和處理都會影響效率,在實際工作中又難免面臨處理大量小文件的場景(比方說用 flume 實時採集日誌,日誌是由用戶發送請求而產生的,用戶發送請求的頻率不是固定的,有的時候頻繁請求,有的時候請求數就比較少,flume 採集數據的配置是每隔固定的一段時間產生一個文件,所以就導致在有些時間段會難免產生大量的小文件)。

在 d 盤的 input 目錄創建三個文件:

one.txt:

I love Beijign
I love China
Beijing is the capital of China

tow.txt:

I love Yantai
I love ShanDong

three.txt

I love Hangzhou
I love Shenzhen

分析

小文件的優化有如下幾種方式:

  • 在數據採集階段,就將小文件或小批數據先合併成大文件再上傳到 HDFS,即在 Flume 採集的時候進行相應文件大小的配置。

  • 在業務處理前,使用 mapreduce 程序對 HDFS 上的小文件先進行合併,再做後續的業務處理(當然,也可以使用 Java IO 流處理一下)。

  • 在業務處理時,採用 CombineTextInputFormat 將多個小文件合併成一個切片,再處理以調高效率。詳見 案例四

本例中使用第二種方式:通過自定義 InputFormat,RecordReader,指定輸出的OutputFormat 類型爲 SequenceFileOutputFormat 的方式來將多個小文件合併成一個大文件。

知識點:自定義 InputFormat,自定義 RecordReader。

實現

因爲 InputFormat 讀取文件輸入靠的是 RecordReader 來完成的,所以我們需要先創建 RecordReader。

1.自定義RecordReader

自定義的 RecordReader 需要繼承 RecordReader,泛型的類型爲 map 端輸入的 key 和 value 的類型,本例中我們的目的是合併文件,所以把文件的內容以字節序列的形式從 value 接收進來就可以,key 設爲 NullWritable 類型。

默認的 TextInputFormat 的 key 的類型是 LongWritable,表示當前所讀取到的字節的偏移量(相對於整篇文章),value 的類型是 Text,表示的是這一行文本的內容,大家可以回過頭去看之前的詞頻統計案例,就可以理解爲什麼 map 的輸入的 key 的類型是 LongWritable,輸入的 value 的類型是 Text 了。

需要重寫 6 個方法:

  • initialize:初始化RecordReader,如果在構造函數中進行了初始化,該方法可以爲空。

  • nextKeyValue:判斷當前文件是否還有下一個 key/value。

  • getCurrentKey:獲取當前讀取到的 key。

  • getCurrentValue:獲取當前讀取到的 value。

  • getProgress:返回的是一個[0.0, 1.0]之間的小數,表示讀取進度,1表示讀取完成。

  • close:關閉資源。

package top9_inputformat;

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.BytesWritable;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.NullWritable;
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 java.io.IOException;

/**
 * @author 曲健磊
 * @date 2019-09-18 10:52:32
 * @description 用於讀取切片中的數據
 */
public class WholeRecordReader extends RecordReader<NullWritable, BytesWritable> {

    private Configuration configuration;

    private FileSplit split;

    private boolean processed = false;

    private BytesWritable value = new BytesWritable();

    @Override
    public void initialize(InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException {
        // 接收讀取到的切片信息以及配置信息
        this.split = (FileSplit) split;
        configuration = context.getConfiguration();
    }

    // InputFormat會爲每一個輸入文件創建一個RecordReader
    // 每一個RecordReader循環調用nextKeyValue方法讀取改文件所產生的所有切片
    // 在本例中每個文件將會調用兩次nextKeyValue方法:
    // 第一次:讀取該文件中的所有內容放入緩存把processed標記置爲true
    // 第二次:標記爲true,結束方法(可在nextKeyValue方法內打斷點調試)
    @Override
    public boolean nextKeyValue() throws IOException, InterruptedException {
        // 在讀取每個文件中的數據的時候判斷是否存在下一個key/value,如果存在返回true,否則返回false
        if (!processed) {
            // 1.定義緩存區
            byte[] contents = new byte[(int)split.getLength()];

            FileSystem fs = null;
            FSDataInputStream fis = null;

            try {
                // 2.獲取文件系統
                Path path = split.getPath();
                fs = path.getFileSystem(configuration);

                // 3.讀取數據
                fis = fs.open(path);

                // 4.讀取文件內容進緩衝區
                IOUtils.readFully(fis, contents, 0, contents.length);

                // 5.將數據保存到 value 中
                value.set(contents, 0, contents.length);
            } catch (Exception e) {

            } finally {
                IOUtils.closeStream(fis);
            }
            processed = true;

            return true;
        }

        return false;
    }

    @Override
    public NullWritable getCurrentKey() throws IOException, InterruptedException {
        // 獲取當前讀取到的數據的key
        return NullWritable.get();
    }

    @Override
    public BytesWritable getCurrentValue() throws IOException, InterruptedException {
        // 獲取當前讀取到的數據的value
        return value;
    }

    @Override
    public float getProgress() throws IOException, InterruptedException {
        // 獲取當前進度信息
        return processed ? 1 : 0;
    }

    @Override
    public void close() throws IOException {}
    
}

2.自定義InputFormat

key 和 value 的類型仍然爲 NullWritable,BytesWritable,表示 map 的輸入的 key 和value 的類型。

package top9_inputformat;

import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.NullWritable;
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;

import java.io.IOException;

/**
 * @author 曲健磊
 * @date 2019-09-18 10:43:20
 * @description 自定義的InputFormat,用於讀取輸入文件
 */
public class WholeFileInputFormat extends FileInputFormat<NullWritable, BytesWritable> {
    @Override
    protected boolean isSplitable(JobContext context, Path filename) {
        // FileInputFormat用isSplitable方法來指定對應的文件是否支持數據的切分,默認情況下都是支持的,也就是true
        // 返回false表示不可以切分,不可以劃分成多個切片,也就是說只有一個切片
        return false;
    }

    @Override
    public RecordReader<NullWritable, BytesWritable> createRecordReader(InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException {
        // 用來創建RecordReader讀取切片中的數據
        WholeRecordReader recordReader = new WholeRecordReader();
        // 初始化RecordReader
        recordReader.initialize(split, context);
        return recordReader;
    }

}

3.Mapper:

package top9_inputformat;

import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;

import java.io.IOException;

/**
 * @author 曲健磊
 * @date 2019-09-18 11:22:30
 */
public class SequenceFileMapper extends Mapper<NullWritable, BytesWritable, Text, BytesWritable> {

    Text k = new Text();

    // mapper初始化
    @Override
    protected void setup(Context context) throws IOException, InterruptedException {
        // 1.獲取文件切片信息
        FileSplit inputSplit = (FileSplit) context.getInputSplit();
        // 2.獲取切片文件名稱
        String name = inputSplit.getPath().toString();
        // 3.設置map輸出的key的值
        k.set(name);
    }

    @Override
    protected void map(NullWritable key, BytesWritable value, Context context) throws IOException, InterruptedException {
        context.write(k, value);
    }

}

4.Reducer:

package top9_inputformat;

import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

import java.io.IOException;

/**
 * @author 曲健磊
 * @date 2019-09-18 11:28:29
 */
public class SequenceFileReducer extends Reducer<Text, BytesWritable, Text, BytesWritable> {

    @Override
    protected void reduce(Text key, Iterable<BytesWritable> values, Context context) throws IOException, InterruptedException {
        context.write(key, values.iterator().next());
    }

}

5.Driver:

package top9_inputformat;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.mapreduce.lib.output.SequenceFileOutputFormat;

/**
 * @author 曲健磊
 * @date 2019-09-18 11:29:45
 */
public class SequenceFileDriver {
    public static void main(String[] args) throws Exception {

        args = new String[]{"d:/input", "d:/output"};

        Configuration conf = new Configuration();
        Job job = Job.getInstance(conf);

        job.setJarByClass(SequenceFileDriver.class);
        job.setMapperClass(SequenceFileMapper.class);
        job.setReducerClass(SequenceFileReducer.class);

        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(BytesWritable.class);

        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(BytesWritable.class);

        // 設置輸入的inputFormat
        job.setInputFormatClass(WholeFileInputFormat.class);
        // 設置輸出的OutputFormat,輸出字節序列
        job.setOutputFormatClass(SequenceFileOutputFormat.class);

        FileInputFormat.setInputPaths(job, new Path(args[0]));
        FileOutputFormat.setOutputPath(job, new Path(args[1]));

        job.waitForCompletion(true);
    }
}

程序運行結果如下:

可以大體看出是把三個文件合併到了一起,實現了需求,那麼如何讀取這種 sequence file 呢?

敬請期待(偷笑.gif)!

關注我的微信公衆號(曲健磊的個人隨筆),觀看更多精彩內容:
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章