1.mapreduce的简介
mapreduce:基于YARN的系统,用于并行处理大型数据集,在我看来是一个计算框架,
官网对于mapreduce的解释:Hadoop MapReduce是一个软件框架,用于轻松编写应用程序,以可靠,容错的方式在大型集群(数千个节点)的商用硬件上并行处理大量数据(多TB数据集)。
MapReduce 作业通常将输入数据集拆分为独立的块,这些块由map任务以完全并行的方式处理。框架对地图的输出进行排序,然后输入到reduce任务。通常,作业的输入和输出都存储在文件系统中。该框架负责调度任务,监视它们并重新执行失败的任务。
2.mapreduce的数据流转形式(分为map阶段和reduce阶段)
(input)
文字解释:我们指定处理的文件输入map时以
此时的数据
那么我们的数据是如何转换成
作为MapReduce过程的预处理阶段,InputFormat起到怎么的作用呢??
InputFomat接口的JAVAAPI解释:逻辑上分割作业的输入文件集。且每个切分后的inputsplit对应一个mapper,简单的说,决定我们mapper的个数,InputFomat中文直译:输入规范/输入格式,规范了我们的输入文件
* 在InputFormat接口中只有两个方法
* 1. InputSplit[] getSplits(JobConf job, int numSplits) throws IOException;
* 2. RecordReader
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
StopWatch sw = new StopWatch().start();
long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));
long maxSize = getMaxSplitSize(job);
-------------->定义一些变量
// generate splits 生成切分
—---------------------->定义一个集合,存放切分后的文件切片
List
----------------------->定义一个集合存放文件,FileStatus在某种程度上可以理解为File
List
------------------------>
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方法的作用
处理后的文件就是我们需要的,体积不大,且是