HDFS小文件的合併優化

HDFS小文件的合併優化

我們都知道,HDFS設計是用來存儲海量數據的,特別適合存儲TB、PB量級別的數據。但是隨着時間的推移,HDFS上可能會存在大量的小文件,這裏說的小文件指的是文件大小遠遠小於一個HDFS塊(128MB)的大小;HDFS上存在大量的小文件至少會產生以下影響:

消耗NameNode大量的內存
延長MapReduce作業的總運行時間

本文將介紹如何在MapReduce作業層面上將大量的小文件合併,以此減少運行作業的Map Task的數量;關於如何在HDFS上合併這些小文件

Hadoop內置提供了一個 CombineFileInputFormat 類來專門處理小文件,其核心思想是:根據一定的規則,將HDFS上多個小文件合併到一個 InputSplit中,然後會啓用一個Map來處理這裏面的文件,以此減少MR整體作業的運行時間。
CombineFileInputFormat類繼承自FileInputFormat,主要重寫了List getSplits(JobContext job)方法;這個方法會根據數據的分佈,mapreduce.input.fileinputformat.split.minsize.per.node、mapreduce.input.fileinputformat.split.minsize.per.rack以及mapreduce.input.fileinputformat.split.maxsize 參數的設置來合併小文件,並生成List。其中mapreduce.input.fileinputformat.split.maxsize參數至關重要:

如果用戶沒有設置這個參數(默認就是沒設置),那麼同一個機架上的所有小文件將組成一個InputSplit,最終由一個Map Task來處理;
如果用戶設置了這個參數,那麼同一個節點(node)上的文件將會組成一個InputSplit。

同一個 InputSplit 包含了多個HDFS塊文件,這些信息存儲在 CombineFileSplit 類中,它主要包含以下信息:
private Path[] paths;
private long[] startoffset;
private long[] lengths;
private String[] locations;
private long totLength;

從上面的定義可以看出,CombineFileSplit類包含了每個塊文件的路徑、起始偏移量、相對於原始偏移量的大小以及這個文件的存儲節點,因爲一個CombineFileSplit包含了多個小文件,所以需要使用數組來存儲這些信息。

CombineFileInputFormat是抽象類,如果我們要使用它,需要實現createRecordReader方法,告訴MR程序如何讀取組合的InputSplit。內置實現了兩種用於解析組合InputSplit的類:org.apache.hadoop.mapreduce.lib.input.CombineTextInputFormat 和 org.apache.hadoop.mapreduce.lib.input.CombineSequenceFileInputFormat,我們可以把這兩個類理解是 TextInputFormat 和 SequenceFileInputFormat。爲了簡便,這裏主要來介紹CombineTextInputFormat。

在 CombineTextInputFormat 中創建了 org.apache.hadoop.mapreduce.lib.input.CombineFileRecordReader,具體如何解析CombineFileSplit中的文件主要在CombineFileRecordReader中實現。

CombineFileRecordReader類中其實封裝了TextInputFormat的RecordReader,並對CombineFileSplit中的多個文件循環遍歷並讀取其中的內容,初始化每個文件的RecordReader主要在initNextRecordReader裏面實現;每次初始化新文件的RecordReader都會設置mapreduce.map.input.file、mapreduce.map.input.length以及mapreduce.map.input.start參數,這樣我們可以在Map程序裏面獲取到當前正在處理哪個文件。

現在我們就來看看如何使用CombineTextInputFormat類,如下:

package com.hqdps.hadoop.mr.merge;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.fs.FileStatus;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.lib.input.CombineTextInputFormat;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;

import java.io.IOException;

public class MergeSmallFile extends Configured implements Tool {
private static final Log LOG = LogFactory.getLog(MergeSmallFile.class);
private static final long ONE_MB = 1024 * 1024L;

static class TextFileMapper extends
        Mapper<LongWritable, Text, Text, NullWritable> {
    //
    @Override
    protected void map(LongWritable key, Text value, Context context)
            throws IOException, InterruptedException {
        context.write(value, NullWritable.get());
    }
}

// 刪除原來小文件
protected static void deleteSmallFiles(String deletepath)
        throws IOException, InterruptedException {
    Configuration conf = new Configuration();
    FileSystem fs = FileSystem.get(conf);
    Path filePath = new Path(deletepath);

    try {
        FileStatus[] status = fs.listStatus(filePath);
        LOG.info("共有" + status.length + "個文件");
        for (FileStatus st : status) {
            LOG.info("開始刪除:" + st.getPath().getName() + "文件");
            fs.delete(st.getPath(), false);
        }
        fs.delete(filePath, false);
    } catch (Exception e) {
        e.printStackTrace();
        LOG.error(e.getMessage());
    }
}
//修改路徑
protected static void modifyPath(String src,String dst)
        throws IOException, InterruptedException {
    Configuration conf = new Configuration();
    FileSystem fs = FileSystem.get(conf);
    Path srcPath = new Path(src);
    Path dstPath = new Path(dst);
    try {
        fs.rename(srcPath, dstPath);
        LOG.info("修改成"+dst+"路徑,成功!!!");
    } catch (Exception e) {
        e.printStackTrace();
        LOG.error(e.getMessage());
    }
}

public static void main(String[] args) throws Exception {
    int exitCode = ToolRunner.run(new MergeSmallFile(), args);
    deleteSmallFiles(args[0]);
    modifyPath(args[1], args[0]);
    System.exit(exitCode);
}

@Override
public int run(String[] args) throws Exception {
    Configuration conf = new Configuration(getConf());

    String inputDir = args[0];
    String outputDir = args[1];
    int maxSplitSize = Integer.valueOf(args[2]);
    LOG.info("inputDir--->>" + inputDir);
    LOG.info("outputDir--->>" + outputDir);
    LOG.info("maxSplitSize--->>" + maxSplitSize + "M");

    Job job = Job.getInstance(conf);
    FileInputFormat.setInputPaths(job, inputDir);
    FileOutputFormat.setOutputPath(job, new Path(outputDir));
    job.setJarByClass(MergeSmallFile.class);

    // 設置最大輸入分片大小,與運行的map、及生產的文件數密切相關
    CombineTextInputFormat.setMaxInputSplitSize(job, ONE_MB * maxSplitSize);
    job.setInputFormatClass(CombineTextInputFormat.class);
    job.setOutputFormatClass(TextOutputFormat.class);
    job.setOutputKeyClass(Text.class);
    job.setMapOutputValueClass(NullWritable.class);
    job.setMapperClass(TextFileMapper.class);
    job.setNumReduceTasks(0);
    return job.waitForCompletion(true) ? 0 : 1;
}

}

可以看到最終結果將三個文件裏面的內容合併到一個文件中。注意體會mapreduce.input.fileinputformat.split.maxsize(CombineTextInputFormat.setMaxInputSplitSize)參數的設置,大家可以不設置這個參數並且和設置這個參數運行情況對比,觀察Map Task的個數變化。

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