map端相对于client端就会复杂很多,里面包含了map的读入,重新赋值,输出等过程,
入口为;mapTask的run方法,因为我自己使用的是2.6.5的版本所以是使用的newapi,此处需要注意的地方是,如果reduce的个数为0的话,就不会设置排序的过程,之后还会有根据reduce的个数进行双向选择的地方,此处需要注意
@Override
public void run(final JobConf job, final TaskUmbilicalProtocol umbilical)
throws IOException, ClassNotFoundException, InterruptedException {
this.umbilical = umbilical;
if (isMapTask()) {
// If there are no reducers then there won't be any sort. Hence the map
// phase will govern the entire attempt's progress.
//判断reduce的个数。默认reduce的个数为1个
if (conf.getNumReduceTasks() == 0) {
mapPhase = getProgress().addPhase("map", 1.0f);
} else {
// If there are reducers then the entire attempt's progress will be
// split between the map phase (67%) and the sort phase (33%).
mapPhase = getProgress().addPhase("map", 0.667f);
//增加排序阶段-》排序结果-》内部有序外部无序--》reduce性能的提升
sortPhase = getProgress().addPhase("sort", 0.333f);
}
}
TaskReporter reporter = startReporter(umbilical);
boolean useNewApi = job.getUseNewMapper();
//初始化job
initialize(job, getJobID(), reporter, useNewApi);
// check if it is a cleanupJobTask
if (jobCleanup) {
runJobCleanupTask(umbilical, reporter);
return;
}
if (jobSetup) {
runJobSetupTask(umbilical, reporter);
return;
}
if (taskCleanup) {
runTaskCleanupTask(umbilical, reporter);
return;
}
if (useNewApi) {
runNewMapper(job, splitMetaInfo, umbilical, reporter);
} else {
runOldMapper(job, splitMetaInfo, umbilical, reporter);
}
done(umbilical, reporter);
}
接下来的这个方法中将进行map相关的基本上所有的重要流程;
/**
* 这个方法的作用;
* 初始化任务的上下文-》包含job相关的信息
* 初始化切片-》该map所要处理的切片是哪一个-》切片决定map的个数,有多少个切片就会有多少个map去执行
* 初始化读取器-》切片中的内容是以什么样格式被读到内存中去-》里面涉及的有;
* 获取切片所归属的位置信息
* 根据该map所持有的切片的位置信息和偏移量,决定归属的文件,打开文件,seek到偏移量的位置开始读取
* 进而完成计算像数据移动
* 此处需要注意的是;
* 如果该切片不是第一个切片,切片相关信息中有切片的索引值,就会抛弃首行,从第二行开始读取,并把下一个切片的首行读进来
* 初始化mapper;
* 此处是需要我们去重新定义的,定义map输出的key,value的格式和类型,如果不重写的话,会导致沿用默认的mapper,输出从文件重获取的k,v的值,没有实际意义
* 此处是必须重写的地方
* 初始化output;
* 两个方式
* 如果没有reduce过程-》初始化直接输出的output
* 如果存在reduce过程-》初始化一个NewOutputCollector-》
* 初始化一个可排序的环形缓冲区-》100m,0.8时发生溢写
* 当发生溢写的时候同时会发生排序,排序时先按分区排序,再按key排序
* 如果没有自定义排序比较器的话,会按照key值所属的类型的比较器去进行比较
*
*
*/
@SuppressWarnings("unchecked")
private
void runNewMapper(final JobConf job,
final TaskSplitIndex splitIndex,
final TaskUmbilicalProtocol umbilical,
TaskReporter reporter
) throws IOException, ClassNotFoundException,
InterruptedException {
// make a task context so we can get the classes
//任务执行的上下文实例
org.apache.hadoop.mapreduce.TaskAttemptContext taskContext =
new org.apache.hadoop.mapreduce.task.TaskAttemptContextImpl(job,
getTaskID(),
reporter);
// make a mapper
//此处说明我们必须重写mapper,不然会默认将从文件中获取的key,value直接输出。
org.apache.hadoop.mapreduce.Mapper mapper =
(org.apache.hadoop.mapreduce.Mapper)
ReflectionUtils.newInstance(taskContext.getMapperClass(), job);
// make the input format
//输入格式化类
//mapreduce.job.inputformat.class 如果没有设置这个参数的话
//->默认的格式化类为TextInputFormat
org.apache.hadoop.mapreduce.InputFormat inputFormat =
(org.apache.hadoop.mapreduce.InputFormat)
ReflectionUtils.newInstance(taskContext.getInputFormatClass(), job);
// rebuild the input split
//重建输入的 切片信息-》获取到,,,重构切片清单
org.apache.hadoop.mapreduce.InputSplit split = null;
//获取当前map需要处理的切片信息
//根据切片的索引获取当前切片的位置信息和偏移量,
split = getSplitDetails(new Path(splitIndex.getSplitLocation()),
splitIndex.getStartOffset());
LOG.info("Processing split: " + split);
//初始化文件读取器-》默认linerecordReader->按行读取
org.apache.hadoop.mapreduce.RecordReader input =
new NewTrackingRecordReader
(split, inputFormat, reporter, taskContext);
job.setBoolean(JobContext.SKIP_RECORDS, isSkipping());
org.apache.hadoop.mapreduce.RecordWriter output = null;
// get an output object
//设置输出模式-》根据reduce的个数来判断
if (job.getNumReduceTasks() == 0) {
output =
new NewDirectOutputCollector(taskContext, job, umbilical, reporter);
} else {
output = new NewOutputCollector(taskContext, job, umbilical, reporter);
}
org.apache.hadoop.mapreduce.MapContext
mapContext =
new MapContextImpl(job, getTaskID(),
input, output,
committer,
reporter, split);
org.apache.hadoop.mapreduce.Mapper.Context
mapperContext =
new WrappedMapper().getMapContext(
mapContext);
try {
//输入的初始化-》将首行扔弃,将给上一个map去获取
/**
* 输入格式化器的初始化;
* 获取切片所属的文件,位置,以及将切片内容放到mappe人的上下文中
*/
input.initialize(split, mapperContext);
//mapper程序开始执行
mapper.run(mapperContext);
mapPhase.complete();//程序计数器
setPhase(TaskStatus.Phase.SORT);
statusUpdate(umbilical);
input.close();
input = null;
output.close(mapperContext);
output = null;
} finally {
closeQuietly(input);
closeQuietly(output, mapperContext);
}
}
当初始化过程都进行完成之后,就该读取器的初始化了,首先说一下读取器的作用,内存中存储的切片的信息,也就是从开始位置到结束为止之间是一些列的字节数组,需要指定以什么样的形式去读取,我们这里默认按行读取也就是如果不指定读取器的话,系统默认使用行读取器,下面是相关选择读取器,以及格式化类的源码;
public static final String INPUT_FORMAT_CLASS_ATTR ="mapreduce.job.inputformat.class";
/**
* Get the {@link InputFormat} class for the job.
*
* @return the {@link InputFormat} class for the job.
*/
@SuppressWarnings("unchecked")
public Class extends InputFormat,?>> getInputFormatClass()
throws ClassNotFoundException {
return (Class extends InputFormat,?>>)
conf.getClass(INPUT_FORMAT_CLASS_ATTR, TextInputFormat.class);
}
/**
* Get the value of the name
property as a Class
.
* If no such property is specified, then defaultValue
is
* returned.
*
* @param name the class name.
* @param defaultValue default value.
* @return property value as a Class
,
* or defaultValue
.
*/
public Class> getClass(String name, Class> defaultValue) {
String valueString = getTrimmed(name);
if (valueString == null)
return defaultValue;
try {
//如果没有指定,则会选择TextInputFormat作为默认格式化类
return getClassByName(valueString);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
读取器的选择;
NewTrackingRecordReader(org.apache.hadoop.mapreduce.InputSplit split,
org.apache.hadoop.mapreduce.InputFormat inputFormat,
TaskReporter reporter,
org.apache.hadoop.mapreduce.TaskAttemptContext taskContext)
throws InterruptedException, IOException {
this.reporter = reporter;
this.inputRecordCounter = reporter
.getCounter(TaskCounter.MAP_INPUT_RECORDS);
this.fileInputByteCounter = reporter
.getCounter(FileInputFormatCounter.BYTES_READ);
List matchedStats = null;
if (split instanceof org.apache.hadoop.mapreduce.lib.input.FileSplit) {
matchedStats = getFsStatistics(((org.apache.hadoop.mapreduce.lib.input.FileSplit) split)
.getPath(), taskContext.getConfiguration());
}
fsStats = matchedStats;
long bytesInPrev = getInputBytes(fsStats);
//文件读取器
this.real = inputFormat.createRecordReader(split, taskContext);
long bytesInCurr = getInputBytes(fsStats);
fileInputByteCounter.increment(bytesInCurr - bytesInPrev);
}
接下来是读取器的初始化,主要完成的任务有;
根据job的信息,获取切片的位置信息;
读取文件,并seek到切片的偏移量的位置
避免hdfs存储层产生将一行数据分割开的情况;
如果切片的偏移量为0,也就是第一个切片的话,会向下多读一行数据,加到自己的流中
如果切片的偏移量不为0,也就是他不是第一个切片的话,会将自己的首行让出,此处的做法是,用一个匿名函数存储第一行的数据,并把偏移量加到切片的start上去,但是并不会对该行数据做任何处理,然后将下一个切片的首行数据读入
最终完成切片的重构,该处所谓的重构就是让首行,向下多读一行
/**
* 获取切片的位置信息,获取job的配置信息,
* 设置行读取器所读取的行的最大长度,如果没有设置的话,会默认为int的最大值
* 重要的是接下来的几点
* 1;获取切片的偏移量,开始,结束的位置,以及所在的文件block的信息
* 2;读取文件,将文件加载到内存的streaming中,通过seek操作到该切片的起始位置
* 3;如果该切片不是第一个切片的话也就是切片的开始位置不为0,
* 会将首行抛弃,从第二行开始读取,然后会将下一个切片的首行读进来
* 4;最终完成对输入数据的格式化
*
*
* @param genericSplit
* @param context
* @throws IOException
*/
public void initialize(InputSplit genericSplit,
TaskAttemptContext context) throws IOException {
//获取切片的文件信息
FileSplit split = (FileSplit) genericSplit;
//加载job的配置信息
Configuration job = context.getConfiguration();
this.maxLineLength = job.getInt(MAX_LINE_LENGTH, Integer.MAX_VALUE);
//切片属于那个文件,从哪开始,从哪结束
start = split.getStart();//开始的偏移量
end = start + split.getLength();//结束的偏移量
final Path file = split.getPath();
// open the file and seek to the start of the split
final FileSystem fs = file.getFileSystem(job);
fileIn = fs.open(file);
CompressionCodec codec = new CompressionCodecFactory(job).getCodec(file);
if (null!=codec) {
//压缩文件的处理
isCompressedInput = true;
decompressor = CodecPool.getDecompressor(codec);
if (codec instanceof SplittableCompressionCodec) {
final SplitCompressionInputStream cIn =
((SplittableCompressionCodec)codec).createInputStream(
fileIn, decompressor, start, end,
SplittableCompressionCodec.READ_MODE.BYBLOCK);
in = new CompressedSplitLineReader(cIn, job,
this.recordDelimiterBytes);
start = cIn.getAdjustedStart();
end = cIn.getAdjustedEnd();
filePosition = cIn;
} else {
in = new SplitLineReader(codec.createInputStream(fileIn,
decompressor), job, this.recordDelimiterBytes);
filePosition = fileIn;
}
} else {
fileIn.seek(start);
in = new UncompressedSplitLineReader(
fileIn, job, this.recordDelimiterBytes, split.getLength());
filePosition = fileIn;
}
// If this is not the first split, we always throw away first record
// because we always (except the last split) read one extra line in
// next() method.
if (start != 0) {
start += in.readLine(new Text(), 0, maxBytesToConsume(start));
}
this.pos = start;
}
按照方法入口的执行顺序,接下来执行的就是mapper中的run方法;
此处最主要的就是context.nextKeyValue(),底层调用的就是读取器的nextKeyValue()方法
/**
* Expert users can override this method for more complete control over the
* execution of the Mapper.
*context.nextkeyvalue实际调用的是输入格式化类的nextKeyValue()方法
* 这个方法有两个作用
* 1;判断是否还有下一条数据
* 3;提供getCurrentKey(),getCurrentValue()用来获取被重新赋值之后的k。v
* @param context
* @throws IOException
*/
public void run(Context context) throws IOException, InterruptedException {
setup(context);
try {
while (context.nextKeyValue()) {
map(context.getCurrentKey(), context.getCurrentValue(), context);
}
} finally {
cleanup(context);
}
}
接下来我们需要看一下读取器中的.nextKeyValue()到底进行了什么操作,话不多说,直接上源码;
这段代码的主要内容为以下几点;
判断当前切片是否存在下一条数据,如果存在下一条数据就会对key,value进行赋值,并且将这段数据的长度加到偏移量上,这样下一次读取的时候就会直接从当前偏移量上去获取数据
此处的key,value,就可以使用 context.getCurrentKey(),context.getCurrentValue()来获取了
如果不存在下一条数据,就返回false,key,value=null。
/**
* 判断是否还有下一条数据
* 判断当前pos是不是0.也就是判断当前切片是不是第一个切片,如果不是,让首行,读下一个切片的首行
* 如果是的话,就多读取下一个切片的首行
*
* 对key,value进行赋值
* @return
* @throws IOException
*/
public boolean nextKeyValue() throws IOException {
if (key == null) {
key = new LongWritable();
}
key.set(pos);//这一行相对于切片的偏移量加上切片相对于块的偏移量
if (value == null) {
value = new Text();
}
int newSize = 0;//行读取器读取的字节数
// We always read one extra line, which lies outside the upper
// split limit i.e. (end - 1)
while (getFilePosition() <= end || in.needAdditionalRecordAfterSplit()) {
if (pos == 0) {
newSize = skipUtfByteOrderMark();
} else {
newSize = in.readLine(value, maxLineLength, maxBytesToConsume(pos));
pos += 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;
value = null;
return false;
} else {
return true;
}
}
到此为止map的输入过程所涉及到的代码基本上算是讲完了,下一篇开始将map的输出过程,涉及到需要根据reduce的个数判断选择输出的格式化类,以及环形缓冲区,分区,排序,排序比较器,归并,commbiner等相关概念