MapReduce预处理阶段-----浅谈InputFormat接口

1.mapreduce的简介

mapreduce:基于YARN的系统,用于并行处理大型数据集,在我看来是一个计算框架,

官网对于mapreduce的解释:Hadoop MapReduce是一个软件框架,用于轻松编写应用程序,以可靠,容错的方式在大型集群(数千个节点)的商用硬件上并行处理大量数据(多TB数据集)。

MapReduce 作业通常将输入数据集拆分为独立的块,这些块由map任务以完全并行的方式处理。框架对地图的输出进行排序,然后输入到reduce任务。通常,作业的输入和输出都存储在文件系统中。该框架负责调度任务,监视它们并重新执行失败的任务。

2.mapreduce的数据流转形式(分为map阶段和reduce阶段)

(input) -> map阶段 -> -> reduce -> (output)

文字解释:我们指定处理的文件输入map时以的形式进入map

此时的数据对成为,k1表示行偏移量,v1表示该行的内容

那么我们的数据是如何转换成的形式传入map阶段的呢??这就是我们今天要说的预处理阶段

作为MapReduce过程的预处理阶段,InputFormat起到怎么的作用呢??

InputFomat接口的JAVAAPI解释:逻辑上分割作业的输入文件集。且每个切分后的inputsplit对应一个mapper,简单的说,决定我们mapper的个数,InputFomat中文直译:输入规范/输入格式,规范了我们的输入文件

* 在InputFormat接口中只有两个方法

* 1. InputSplit[] getSplits(JobConf job, int numSplits) throws IOException;

* 2. RecordReader getRecordReader(InputSplit split,

JobConf job,

Reporter reporter) throws IOException;

* 其中getSplit方法,可以决定文件是否切分。【是一整个文件进行整理(如奇偶行求和,如果文件被切分了,

* 可能造成奇偶和的错乱,所以不能切分),或是文件太大,切分成若干split,提高处理速度】

* 而getrecordReader方法,可以返回一个任意的recordReader

 

既然inputformat是一个接口,我们可以选择了解一下它的实现类,我们经常使用的FileInputFormat

FileInputFomat:抽象类,实现了InputFormat接口

JAVAAPI解释:一个基于文件的InputFormat基础类,并且提供了getSplit()的通用实现,它的子类也可以覆盖这个方法,从而达到是文件作为一个整体而不被切分。

【InputFormat抽象方法解析一:getSplit()】

那么我们的文件是如何切分的呢??

方法一:判断文件是否可切分:isSplitable()

在FileInputFormat类中,通过isSplitale方法,可以设置文件是否可切分,在此类中,恒返回ture,表示文件可被切分

protected boolean isSplitable(JobContext context, Path filename) {

return true;

}

方法二:判断文件如何切分/计算切分大小:getSplits()

public List getSplits(JobContext job) throws IOException {

StopWatch sw = new StopWatch().start();

long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));

long maxSize = getMaxSplitSize(job);

-------------->定义一些变量

// generate splits 生成切分

—---------------------->定义一个集合,存放切分后的文件切片

List splits = new ArrayList();

----------------------->定义一个集合存放文件,FileStatus在某种程度上可以理解为File

List files = listStatus(job);

------------------------>

for (FileStatus file: files) {

------------------------>获取文件路径和长度

Path path = file.getPath();

long length = file.getLen();

if (length != 0) {

------------------------>如果文件不为空,判断是否为本地文件,并获取块地址信息

BlockLocation[] blkLocations;

if (file instanceof LocatedFileStatus) {

blkLocations = ((LocatedFileStatus) file).getBlockLocations();

} else {

FileSystem fs = path.getFileSystem(job.getConfiguration());

blkLocations = fs.getFileBlockLocations(file, 0, length);

}

------------------------>如果文件可切分,计算切分大小!!!!敲黑板敲黑板

if (isSplitable(job, path)) {

long blockSize = file.getBlockSize();

long splitSize = computeSplitSize(blockSize, minSize, maxSize);

------------------------>就是这个computeSplitSize方法!!!这个方法决定了切分大小

我们先捋顺代码,在下边解释这个方法

long bytesRemaining = length;

------------------------>定义一个变量,表示剩余字节数,现在它等于文件总长度

while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {

------------------------>当剩余的字节数/切分大小>1.1,此时我们才能切分,不然不能切分!!!

int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);

------------------------>定义变量,存放块下标【解释一下,比如一个文件1000字节,1-100切一下下标为0,以后同上,就根据文件的偏移量,确定文件下标】

------------------------->向split集合中添加一个文件切分,该切分由makeSplit方法获得,最后在分析这个方法,现在我们知道这个方法创建了一个文件切分即可

splits.add(makeSplit(path, length-bytesRemaining, splitSize,

blkLocations[blkIndex].getHosts(),

blkLocations[blkIndex].getCachedHosts()));

------------------------->切分之后,文件的剩余字节数减少

bytesRemaining -= splitSize;

}

------------------------->如果文件字节数不为0,也就是说文件还有剩余,继续切咯

if (bytesRemaining != 0) {

int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);

splits.add(makeSplit(path, length-bytesRemaining, bytesRemaining,

blkLocations[blkIndex].getHosts(),

blkLocations[blkIndex].getCachedHosts()));

}

} else { // not splitable 不允许切分

------------------------->如果文件不允许切分,整个文件作为一个split

splits.add(makeSplit(path, 0, length, blkLocations[0].getHosts(),

blkLocations[0].getCachedHosts()));

}

} else {

------------------------->如果文件长度为0

//Create empty hosts array for zero length files //为零长度文件创建空的主机数组。

splits.add(makeSplit(path, 0, length, new String[0]));

}

}

// Save the number of input files for metrics/loadgen 保存输入文件的个数

job.getConfiguration().setLong(NUM_INPUT_FILES, files.size());

sw.stop();

//日志的一些操作

if (LOG.isDebugEnabled()) {

LOG.debug("Total # of splits generated by getSplits: " + splits.size()

+ ", TimeTaken: " + sw.now(TimeUnit.MILLISECONDS));

}

return splits;

}

 

---------------------------------------------------------------------------------------------------------

继续看下我们刚才跳过的两个方法:

computeSplitSize(blockSize, minSize, maxSize);

-------------------------->crtl跟进

protected long computeSplitSize(long blockSize, long minSize,

long maxSize) {

return Math.max(minSize, Math.min(maxSize, blockSize));

}

说明:a.先将块大小与默认最大值大小做比较,取小值;结果为128m

b.将上一步的计算结果与输入文件的最小值比较,取大值,结果为128m;

总结:默认情况小,split切分大小与block大小相同;

makeSplit():

------------------------->跟进

protected FileSplit makeSplit(Path file, long start, long length,

String[] hosts, String[] inMemoryHosts) {

return new FileSplit(file, start, length, hosts, inMemoryHosts);

}

通过这文件的开始坐标长度等各种信息,new了一个FileSplit,作为一个切分

======================================================================

以上就是我对InputFormat接口和FileInputFormat类中getSplit()方法的理解了

然后是第二个方法:getRecordReader()

getrecordReader方法,可以返回一个任意的recordReader

* RecordReader JAVAapi解释:用于从文件切分中读取键值对,通常从输入中读取下一个键/值对进行处理。

* TextInputSplit默认的recordReader(我理解为记录流,就是一个简单的流)是LineRecordReader(按行读的流),

* 看了一下recordReader的源码,RecordReader是一个接口,有一下方法

* boolean next(K key, V value) throws IOException;//从输入中读取下一个键/值对进行处理。

* K createKey(); //创建要用作键的适当类型的对象。

* V createValue(); .//同上

* long getPos() throws IOException;//返回输入中的当前位置。

* public void close() throws IOException;//关闭切分

* float getProgress() throws IOException;//获取进度

* 总的来说,recordReader就是处理键值对的,由recordreader作为流,读取我们的文件,然后决定谁是key谁是value

* 常用的RecordReader是LineRecordReader,上源码

* 我们想知道,为什么LineRecordReader可以按行读取呢?或者说LineRecordReader和其他RecordReader的区别在哪呢

* 1.可以只读一行,在初始化方法中有介绍,上代码

*

fileIn.seek(start);

in = new UncompressedSplitLineReader(

fileIn, job, this.recordDelimiterBytes, split.getLength());

filePosition = fileIn;

* 先给输入流传入各种参数

*

* if (start != 0) {

start += in.readLine(new Text(), 0, maxBytesToConsume(start));

}

this.pos = start;

满足各种条件之后,readLine读一行

* 再看nextKeyValue()方法

* public boolean nextKeyValue() throws IOException {

if (key == null) {

key = new LongWritable();

//如果key为空,就new一个LongWritable类型对象【这就是为什么k1是LongWritable类型的值】,刚开始肯定没有key啊,new一个

【具体为什么要new一个对象,我在一个论坛上过看到过一次,好像是为了防止重复之类的,具体的记不太清了,以后想起来会过来补充的】

}

key.set(pos); //pos是该类中的静态参数,

 

--------------------------初始化方法中代码,设定pos的值,代码如下------------------------------------

if (start != 0) {

start += in.readLine(new Text(), 0, maxBytesToConsume(start));

}

this.pos = start;

//流读完一行之后,返回读出字节数,并将结构传给pos,此时的pos即行字节数,也就是行偏移量,这就是为是么k1是行偏移量的原因

-----------------------------------------------------------------------------------------------------------------------

if (value == null) {

value = new Text();

------------------------->如果value不存在就new一个,现在当然不存在了,new一个

}

int newSize = 0;

------------------------->定义一个参数newSize=0

// We always read one extra line, which lies outside the upper

------------------------->我们总是读一条额外的线,它位于上方。意思是,我们总是多读一行

// split limit i.e. (end - 1)

------------------------->split的界限是end-1

while (getFilePosition() <= end || in.needAdditionalRecordAfterSplit()) {

if (pos == 0) {

newSize = skipUtfByteOrderMark();

} else {

newSize = in.readLine(value, maxLineLength, maxBytesToConsume(pos));

pos += newSize;

}

------------------------->整个方法就是给newSize赋了一个值,标明一行数据的长度

if ((newSize == 0) || (newSize < maxLineLength)) {

break;

}

-------------------------> line too long. try again //行太长,再执行一次 ,应该是写到日志里了

LOG.info("Skipped line of size " + newSize + " at pos " +

(pos - newSize));

}

if (newSize == 0) {

key = null;

------------------------->如果这行长度为0

value = null;

------------------------->key是空的,值是空的,就说明没有下一行了,执行到文件的最后了,返回false【没有下一个键和值了】

------------------------->然后执行getcurrentkey和getcurrentvalue方法,取出当前的键值就ok了

return false;

} else {

return true;

------------------------->如果有,就说明当前行不是最后一行,取出当前行的键值后,取下一行的

}

}

这就是LineReader的源代码解析了,总之你想知道的,代码里都有

* */

 

=============================================================================

总结:

作为mapreduce阶段的预处理阶段,首先需要对文件进行第一步的处理,将文件切成若干split,提高之后处理文件的速度,这是getSpliet的作用

其次我们还需要根据实际情况,设置好键值对,因为整个mapreduce的数据流转都是通过键值对进行传输的,这就是makeRecordReader方法的作用

处理后的文件就是我们需要的,体积不大,且是形式的数据了

你可能感兴趣的:(MapReduce预处理阶段-----浅谈InputFormat接口)