Hadoop(9) MapReduce-2 InputFormat详见和自定义InputFormat
InputFormat
切片和提交的过程
- 首先在客户端将文件进行数据切片(split)规划, 每个切片大小默认是128M
说明 为什么是切片是128M?
因为在Hadoop中,每个块的大小是128M, 如果切片大小就是128M的话, 就可以直接在这个block所在的DataNode上执行Mapper了, 如果块的大小和切片大小不一致, 就会出现数据传输,如下图, 假设split大小为100M,而block大小为128M, 这样第一个DataNode会把28M数据传输给第二个DataNode进行处理,这样就会消耗网络流量,影响效率
注意
- 每一个split切片分配一个MapTask(所有MapTask并行执行)
- 默认情况下, split 大小=block大小
- 切片时只考虑单个文件,即只对大于128M的文件进行切片,小于128M的文件就看做一个split
- 切片的时候,如果剩下的部分<指定split大小*1.1, 就不再进行切片了, 这样可以防止出现过小的切片
- 遍历Input目录下的每一个文件, 获取每个文件的大小,然后计算切片大小,整个切片的核心过程在getSplit()方法中完成,InputSplit只记录了切片的元数据信息
- 提交切片规划信息到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()方法
源码中计算大小的公式
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):
图示说明
- 虚拟存储过程
将输入目录下所有文件大小, 依次和设置的setMaxInputSplitSize值比较, 如果不大于设置的最大值, 逻辑上划分一个块。如果输入文件大于设置的最大值且大于两倍, 那么以最大值切割一块;当剩余数据大小超过设置的最大值且不大于最大值2倍, 此时将文件均分成2个虚拟存储块(防止出现太小切片)。
例如setMaxInputSplitSize值为4M, 输入文件大小为8.02M, 则先逻辑上分成一个4M。剩余的大小为4.02M, 如果按照4M逻辑划分, 就会出现0.02M的小的虚拟存储文件, 所以将剩余的4.02M文件切分成(2.01M和2.01M)两个文件。
- 切片过程:
(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的步骤
- 自定义一个类继承
InputFormat
, 实现其2个方法:getSplits()
和createRecordReader
, 其中getSplites()
的作用是将文件切片,createRecordReader
是将切片的文件生成键值对 - 如果默认切片方法跟
FileInputFormat
一样, 也可以直接继承InputFormat
的实现类(如FileInputFormat
), 这样可以省好多事 - 自定义一个类继承
RecordReader
, 实现其中的方法(系统默认的是LineRecordReader
,即按行读取)文件 - 在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);
}
}