Hadoop案例之自定义分片策略解决大量小文件问题

探讨Hadoop处理大量小文件问题,通过自定义MyInputFormat和MyRecordReader改进分片策略,有效提升MapReduce程序性能。

Hadoop案例之自定义分片策略解决大量小文件问题

1.默认的分片策略TextInputFormat

应该都知道默认的TextInputFormat是一行行的读取文件内容,这对于一个或几个超大型的文件来说并没有什么问题,但是在实验读取大量小文件的时候,性能及其低下。

1.1实验过程

分别有5个文件夹,每个文件夹下有不同数量(1-2千个)的小文件(10+k大小),总量大概有8k+个文件,使用CLI命令上传到HDFS就花费了一个多小时

环境为本地电脑安装的伪分布式Hadoop集群,机器配置为四核I7CPU,16G的RAM。

编写简单的worldCount程序,一切默认,放到集群上跑的时候出现以下情况:

1.启动的mapper总数量为8k+个!而一个节点能同时运行的mapper数量为4 
2.整个map过程及其缓慢,50%跑了2h 
2.CPU总用率高达80%,整个机器开始发出呲呲呲的声音

可见大量的小文件对mapreduce程序性能的影响有多大。

1.2问题的根本所在

HDFS上的文件是按block来存储的。

如果一个文件很大,超出了一个block的设定,那么它就会被划分为多个block存储,mapreduce程序读取时,每个block都会对应输入一个mapper,所以大文件,默认的分片策略是可以hold住的。

但是如果是很多小文件的话,每个小文件存储的时候都会是一个block,即使它很小,远远达不到block大小(默认128M),HDFS还是会将其存储在一个block中,那么问题就来了,默认的分片策略在读取这些小文件的时候,每个block都会产生一个mapper,所以就有了上面程序中出现了8k+个mapper的情况。

1.3解决方案

既然知道了问题所在,那么就可以指定对应的解决方案,无非就是从两点入手:

1.默认每个小文件对应一个block,那么可以采取压缩等手段将多个小文件进行合并存储,以达到每个block存储的内容都是足够大的。 
2.修改mapreduce默认的分片策略,使得读取文件进行分片的时候让每个block可以对应多个小文件,而不再是仅仅一个小文件。

 

2.自定义的分片策略MyInputFormat

关于Hadoop的InputFormat类,网上有很多详细介绍的文章,默认的分片策略使用的就是其TextInputFormat子类,这里将介绍另外一个子类:CombineFileInputFormat

顾名思义,CombineFileInputFormat是用来将输入的小文件进行合并,然后输入到一个mapper中的策略。 
这是一个抽象类,只实现了InputFomat接口的getSplit方法。(P.S.所有的分片策略都要继承InputFormat,并实现getSplit和createRecordReader两个方法) 
既然我们需要用到CombineFileInputFormat,但他留了一个接口方法让我们实现,那么就可以自定义一个MyInputFormat类继承自CombineFileInputFormat,重写createRecordReader。

关于InputFormat接口的两个方法:

1.getSplit是从HDFS上读取文件,并形成逻辑的分片,在本文中,这个分片会包含多个小文件。 
2.createRecordReader会创建一个RecordReader对象,用来读取getSplit产生的分片,mapper中的键值对就是这个RecordReader输出的。

之前讨论到的自定义MyInputFormat类实现分片策略,但是分片之后如何读取分片内的数据是createRecordReader方法创建的RecordReader对象决定的

所以自定义分片策略的关键在于两点:

1.MyInputFormat类自定义分片策略 
2.MyRecordReader类自定义读取分片内的数据

 

4.源码地址

package MuiltiSmallFileCombine;

import org.apache.hadoop.fs.Path;
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.CombineFileInputFormat;
import org.apache.hadoop.mapreduce.lib.input.CombineFileRecordReader;
import org.apache.hadoop.mapreduce.lib.input.CombineFileSplit;

import java.io.IOException;

public class MyInputFormat extends CombineFileInputFormat<Text, Text>{
    
    /*
     * 重写方法,直接返回false,对所有文件都不进行切割,保持完整
     */
    @Override
    protected boolean isSplitable(JobContext context, Path file) {
        return false;
    } 
    /*
     * 重写此方法,返回的CombineFileRecordReader为处理每个分片的recordReader,
     * 在构造函数中设置自定义的RecordReader对象
     */    
    public RecordReader<Text, Text> createRecordReader(InputSplit inputSplit, TaskAttemptContext taskAttemptContext) throws IOException {
        return new CombineFileRecordReader<Text, Text>((CombineFileSplit) inputSplit, taskAttemptContext, MyRecordReader.class);
    }
}
 

package MuiltiSmallFileCombine;

import java.io.IOException;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.util.GenericOptionsParser;

public class MyCombine {

    public static class Map extends Mapper<Text, Text, Text, Text>
    {
        @Override
        protected void map(Text key, Text value, Context context)
                throws IOException, InterruptedException {
            context.write(key, value);
            
        }
    }
    
    public static class Reduce extends Reducer<Text, Text, Text, Text>
    {
        @Override
        protected void reduce(Text key, Iterable<Text> value, Context context)
                throws IOException, InterruptedException {
            
            for(Text v : value)
            {
                context.write(key, v);
            }
        }
    }
    
    public static void main(String[] args) throws Exception {
        Configuration conf = new Configuration();
        String[] otherArgs = new GenericOptionsParser(conf,args).getRemainingArgs();
        if (otherArgs.length != 2) {
            System.err.println("Usage: MyCombine <in>  <out>");
            System.exit(2);
        }
        
        Job job = new Job(conf,"MyCombine");
        job.setJarByClass(MyCombine.class);
        job.setMapperClass(Map.class);
        job.setReducerClass(Reduce.class);
        
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(Text.class);
        
        job.setInputFormatClass(MyInputFormat.class);
        
        FileInputFormat.addInputPath(job, new Path(otherArgs[0]));
        FileOutputFormat.setOutputPath(job, new Path(otherArgs[1]));
        
        System.exit(job.waitForCompletion(true)? 0 : 1);
    }
}
 

package MuiltiSmallFileCombine;

import java.io.IOException;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FileStatus;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
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.CombineFileSplit;

/*
 * 自定义的RecordReader类,用来处理CombineFileInputSplit返回的每个分片
 */
public class MyRecordReader extends RecordReader<Text, Text>{

    private CombineFileSplit combineFileSplit; // 当前处理的分片
    private Configuration conf; //系统信息
    private int curIndex;//当前处理到第几个分片
    
    private Text curKey = new Text();//当前key
    private Text curValue = new Text();//当前value
    private boolean isRead = false;//是否已经读取过该分分片
    private float currentProgress = 0;//当前读取进度
    
    private FSDataInputStream fsInputStream;//HDFS文件读取流
    
    /*
     * 构造函数必须的三个参数,自定义的InputFormat类每次读取新的分片时,
     * 都会实例化自定义的RecordReader类对象来对其进行读取
     * @param combineFileSplit   当前读取的分片
     * @param taskAttemptContext 系统上下文环境
     * @param index              当前分片中处理的文件索引
     */
    public MyRecordReader(CombineFileSplit combineFileSplit, TaskAttemptContext taskAttemptContext, Integer index)
    {
        this.combineFileSplit = combineFileSplit;
        this.conf = taskAttemptContext.getConfiguration();
        this.curIndex = index;
    }
    
    @Override
    public void close() throws IOException {
        if (fsInputStream != null) {
            fsInputStream.close();
        }
        
    }

    /**
     * 返回当前key的方法
     */
    public Text getCurrentKey() throws IOException, InterruptedException {
        return curKey;
    }

    /**
     * 返回当前value的方法
     */
    public Text getCurrentValue() throws IOException, InterruptedException {
        return curValue;
    }

    /**
     * 返回当前的处理进度
     */
    public float getProgress() throws IOException, InterruptedException {
        //获得当前分片中的总文件数
        int splitFileNum = combineFileSplit.getPaths().length;
        if (curIndex > 0 && curIndex < splitFileNum) {
            //当前处理的文件索引除以文件总数得到处理的进度
            currentProgress = (float)curIndex / splitFileNum;
        }
        return currentProgress;
    }

    /*
     * 初始化RecordReader的一些设置(non-Javadoc)
     */
    @Override
    public void initialize(InputSplit arg0, TaskAttemptContext arg1) throws IOException, InterruptedException {
        // TODO Auto-generated method stub
        
    }

    /*
     * 返回true就取出key和value,之后index前移,返回false就结束循环表示没有文件内容可读取了
     */
    @Override
    public boolean nextKeyValue() throws IOException, InterruptedException {

        //没被读取过的文件才进行读取
        if (!isRead) {
            //只实现了读取输入目录下的文件,没有实现读取输入目录下子目录里的文件
            //默认的TextFileInputFormat里调用的RecoderReader,是可以读取子目录里的文件的
            
            //根据当前的文件索引从当前分片中找到对应的文件路径
            Path path = combineFileSplit.getPath(curIndex);
            
            //获取父目录名作为Key值
            curKey.set(path.getParent().getName());
            //从当前分片中获得当前文件的长度
            byte[] content = new byte[(int) combineFileSplit.getLength(curIndex)];
            try {
                //读取该文件内容
                FileSystem fs = path.getFileSystem(conf);
                FileStatus st = fs.getFileStatus(path);
                if (!st.isFile()) {
                    return false;
                }
                fsInputStream = fs.open(path);
                fsInputStream.readFully(content);
            } catch (Exception ignored) {
            }finally {
                if (fsInputStream != null) {
                    fsInputStream.close();
                }
            }
            //整个文件内容作为value值
            curValue.set(content);
            isRead = true;
            return true;
        }
        return false;
    }

}
 

转载:

http://blog.youkuaiyun.com/qq1010885678/article/details/50771361

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值