Hadoop(9) MapReduce-2 InputFormat详见和自定义InputFormat

14 篇文章 0 订阅
3 篇文章 0 订阅

Hadoop(9) MapReduce-2 InputFormat详见和自定义InputFormat

InputFormat

切片和提交的过程

  1. 首先在客户端将文件进行数据切片(split)规划, 每个切片大小默认是128M

说明 为什么是切片是128M?

因为在Hadoop中,每个块的大小是128M, 如果切片大小就是128M的话, 就可以直接在这个block所在的DataNode上执行Mapper了, 如果块的大小和切片大小不一致, 就会出现数据传输,如下图, 假设split大小为100M,而block大小为128M, 这样第一个DataNode会把28M数据传输给第二个DataNode进行处理,这样就会消耗网络流量,影响效率

注意

  1. 每一个split切片分配一个MapTask(所有MapTask并行执行)
  2. 默认情况下, split 大小=block大小
  3. 切片时只考虑单个文件,即只对大于128M的文件进行切片,小于128M的文件就看做一个split
  4. 切片的时候,如果剩下的部分<指定split大小*1.1, 就不再进行切片了, 这样可以防止出现过小的切片
  1. 遍历Input目录下的每一个文件, 获取每个文件的大小,然后计算切片大小,整个切片的核心过程在getSplit()方法中完成,InputSplit只记录了切片的元数据信息
  2. 提交切片规划信息到Yarn上,Yarn上的MapReduceAppMaster根据规划文件开启MapTask个数


InputFormat介绍

InputFormat是所有Mapper输入机制类的父类, 里面有2个抽象方法

  • getSplits() 切片方法

这个方法主要作用是定义切片规则, 在客户端对输入文件夹中的文件进行切片(split)时, 就是调用这个方法, 重写这个方法, 可以自定义切片规则

  • createRecordReader() 获取键值对方法

这个方法主要是返回一个RecorderReader类, 每个RecorderReader对应一个切片, RecorderReader类里面有几个方法, 这几个方法主要负责将该切片中的数据解析成键值对, 然后传入Mapper中的**map()**方法(每个键值对调用一次map()方法)



FileInputFormat机制及其实现类

FileInputFormat介绍

FileInputFormat继承了InputFormat, 并重写了其中的切片方法getSplits(), 其中定义了默认切片大小=block大小

但是没有实现createRecordReader()方法, 索引FileInputFormat有很多实现类, 这些实现类就是重写了FileInputFormat里的createRecordReader()方法

继承并实现getSplits
继承并实现createRecorderReader
继承并实现createRecorderReader
继承并实现createRecorderReader
继承并实现createRecorderReader
InputFormat
FileInputFormat
TextInputFormat
KeyValueTextInputFormat
NlineInputFormat
......
源码中计算大小的公式
Math.max(minSize, Math.min(maxSize, blockSize));

说明

mapreduce. input fileimputformat.split minsize=1默认值为1
mapreduce .input fileinputformat. split maxsize= Long MAXValue默认值Long MAXValue

这里说白了就是默认取block块的大小,如果想要自定义大小,可以将maxSize调的比blockSize小(<128M)或者将minSize调的比blockSize大(>128M)


FileInputFormat的实现类
TextInputFormat

这是Hadoop默认的FileInputFormat实现类,如果不指定,就默认使用这个, 按行读取文件, 然后生成一个键值对,其中key是该行在整个文件中的偏移量(LongWrite类型), value是该行的内容(Text类型)

KeyValueTextInputFormat

按行读取文件,按照分割分来划分key和value

可以在驱动类中设置划分符号

举例 使用 / 来划分键值对(默认就\t)

conf.set(KeyValueLineRecordReader.KEY_VALUE_SEPERATOR,"\t");

如果有以下文件:

1/oneone
2/twotwo
3/threethree

将被划分成:

(1,oneone)
(2,twotwo)
(3,threethree)
NlineInputFormat

使用这个实现类,map进程处理的InputSplit就不再按照block大小去划分,而是按照行数去划分,即

切片数=文件总行数/N(如果不整除就向上取整)

CombineTextInputFormat机制

之前我们介绍过,如果Hadoop中出现很多小文件,将会占用大量的资源(因为每个小文件在处理的时候会被划分成一个split,从而占用一个MapTask), 在处理小文件的时候,我们可以使用CombineTextInputFormat机制,在切片之前先将小文件合并一次

具体流程如下, 首先设置一个虚拟存储最大值(这里是4M):

图示说明

  1. 虚拟存储过程

​ 将输入目录下所有文件大小, 依次和设置的setMaxInputSplitSize值比较, 如果不大于设置的最大值, 逻辑上划分一个块。如果输入文件大于设置的最大值且大于两倍, 那么以最大值切割一块;当剩余数据大小超过设置的最大值且不大于最大值2倍, 此时将文件均分成2个虚拟存储块(防止出现太小切片)。

​ 例如setMaxInputSplitSize值为4M, 输入文件大小为8.02M, 则先逻辑上分成一个4M。剩余的大小为4.02M, 如果按照4M逻辑划分, 就会出现0.02M的小的虚拟存储文件, 所以将剩余的4.02M文件切分成(2.01M和2.01M)两个文件。

  1. 切片过程:

(a)判断虚拟存储的文件大小是否大于setMaxInputSplitSize值, 大于等于则单独形成一个切片。

(b)如果不大于则跟下一个虚拟存储文件进行合并, 共同形成一个切片。

(c)测试举例:有4个小文件大小分别为1.7M、5.1M、3.4M以及6.8M这四个小文件, 则虚拟存储之后形成6个文件块, 大小分别为:1.7M, (2.55M、2.55M), 3.4M以及(3.4M、3.4M)

最终会形成3个切片, 大小分别为:(1.7+2.55)M, (2.55+3.4)M, (3.4+3.4)M

SequenceFile

有时候我们只用一个MapReduce可能无法完成想要的工作,需要多个MapReduce串行执行,SequenceFile就可以充当各个MapReduce的中间文件,用于在MapReduce之间传递数据,SequenceFile可以存储各个值的类型,所以比String要方便


如何修改默认的切片方法

在Driver驱动类中添加以下代码:

job.setInputFormatClass(<切片方法类名>.class);

举例

使用CombineTextInputFormat

//定义使用的InputFormat
job.setInputFormatClass(CombineTextInputFormat.class);
//设置切片最大值(setMaxInputSplitSize)
CombineTextInputFormat.setMaxInputSplitSize(job, 4194304); //4M


自定义InputFormat

有时候官方提供的InputFormat无法满足我们的需求,这个时候我们可以自定义一个InputFormat

自定义InputFormat的步骤
  1. 自定义一个类继承InputFormat, 实现其2个方法: getSplits()createRecordReader, 其中getSplites()的作用是将文件切片, createRecordReader是将切片的文件生成键值对
  2. 如果默认切片方法跟FileInputFormat一样, 也可以直接继承InputFormat的实现类(如FileInputFormat), 这样可以省好多事
  3. 自定义一个类继承RecordReader, 实现其中的方法(系统默认的是LineRecordReader,即按行读取)文件
  4. 在Driver驱动类里面定义该MapReduce使用的InputFormat

自定义InputFormat举例

举例 自定义一个InputFormat, 将多个小文件合并成一个SequenceFile文件(SequenceFile文件是Hadoop用来存储二进制形式的key-value对的文件格式), SequenceFile里面存储着多个文件, 存储的形式为文件路径+名称, 其中文件路径为key, 文件内容为value

方便起见, 新建一个类继承FileInputFormat, 这样我们只要实现一个方法就可以了,因为在FileInputFormat中, 已经实现了getSplits()方法了, 但是这里题目要求文件不能切片, 然而 FileInputFormat里定义的是如果文件大小>block块的大小, 就要切片, 这里我们只需要修改一下就可以了, 代码如下:

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

import java.io.IOException;

/**
 * BytesWritable 是表示一个二进制文件
 */
public class MyInputFormat extends FileInputFormat<Text, BytesWritable> {

    /**
     * 这个方法是判断该文件能不能切片,如果是true,就可以,false就不能切片
     * 因为题目要求文件不能切片,所以这里直接返回false就可以
     */
    @Override
    protected boolean isSplitable(JobContext context, Path filename) {
        return false;
    }

    @Override
    public RecordReader createRecordReader(InputSplit inputSplit, TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {
        return new MyFileRecordReader();
    }
}

然后创建一个类继承RecordReader,并实现它的全部抽象方法

mport 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.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 java.io.IOException;

/**
 * Description: 自己定义一个RecordReader,每个RecordReader对应一个切片,这里就是对应一个文件
 * RecorderReader的作用是向MapTask返回键值对
 */
public class MyFileRecordReader extends RecordReader<Text, BytesWritable> {
    //创建一个flag,表示文件有没有读过,在getProgress用到
    private boolean notRead = true;
    //定义用到的属性
    private Text key = new Text();
    private BytesWritable value = new BytesWritable();
    //创建一个数据流
    private FSDataInputStream fsDataInputStream;
    //文件的路径
    private FileSplit fs;

    /**
     * 初始化该类时会调用一次
     */
    @Override
    public void initialize(InputSplit inputSplit, TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {
        //转换切片类型到文件切片
        fs = (FileSplit) inputSplit;
        //通过切片获取路径
        Path path= fs.getPath();
        //通过路径获取文件系统
        FileSystem fileSystem = path.getFileSystem(taskAttemptContext.getConfiguration());
        //开流
        fsDataInputStream = fileSystem.open(path);

    }

    /**
     * 尝试读取下一个键值对,如果读到了返回true,否则返回false
     *
     * @return
     * @throws IOException
     * @throws InterruptedException
     */
    @Override
    public boolean nextKeyValue() throws IOException, InterruptedException {
        if (notRead) {
            //具体读文件的过程
            //读key
            key.set(fs.getPath().toString());
            //读value
            byte[] buf = new byte[(int) fs.getLength()];
            fsDataInputStream.read(buf);
            value.set(buf, 0, buf.length);

            //将notRead标记为false
            notRead = false;
            //返回true
            return true;
        } else {
            //没有读到,返回false
            return false;
        }
    }

    /**
     * 获取当前读到的Key
     *
     * @return
     * @throws IOException
     * @throws InterruptedException
     */
    @Override
    public Text getCurrentKey() throws IOException, InterruptedException {
        return key;
    }

    /**
     * 获取当前读到的value
     *
     * @return
     * @throws IOException
     * @throws InterruptedException
     */
    @Override
    public BytesWritable getCurrentValue() throws IOException, InterruptedException {
        return value;
    }

    /**
     * 当前读取的进度,因为一个文件对应一个KV值,所以进度上只能是0和1(读了和没读)
     *
     * @return 当前进度
     * @throws IOException
     * @throws InterruptedException
     */
    @Override
    public float getProgress() throws IOException, InterruptedException {
        return notRead ? 0 : 1;
    }

    /**
     * 关闭资源
     *
     * @throws IOException
     */
    @Override
    public void close() throws IOException {
        //关流
        IOUtils.closeStream(fsDataInputStream);
    }
}

最后定义一个驱动类,测试运行这个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.mapred.SequenceFileOutputFormat;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import java.io.IOException;

/**
 * Description: 因为这里只是做一个InputFormat的测试,所以不需要额外自定义MapReduce,
 * 直接使用默认的就可以了,默认的MapReduce就只是把数据原封不动的输出
 */
public class MyFileInputDriver {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
        //获取Job实例
        Job job = Job.getInstance(new Configuration());
        //配置类路径
        job.setJarByClass(MyFileInputDriver.class);
        //设置map的输出类型
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(BytesWritable.class);
        //设置reduce的输出类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(BytesWritable.class);

        // 一定要注意这里!!!! 配置InputFormat的类型和输出格式
        job.setInputFormatClass(MyInputFormat.class);
        job.setOutputValueClass(SequenceFileOutputFormat.class);

        //配置输入输出路径
        FileInputFormat.setInputPaths(job, new Path("input"));
        FileOutputFormat.setOutputPath(job, new Path("output"));

        boolean b = job.waitForCompletion(true);
        System.out.println(b);
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值