前面分析了MapReduce客户端作业提交、计算MapTask数量,现在开始分析作业提交之后,MapTask是如何运行的。ResourceManager收到客户端提交的作业后,会启动MrAppMaster,MrAppmaster运行程序时向ResouceManager 请求maptask/reduceTask。这个请求最终会在NodeManager上产生一个Yarn child进程,Yarn child会反射出MapTask类,这个MapTask类就是Map任务的主类,这个类里有一个run()
方法,我们从MapTask类的run()方法开始分析。
注:本人使用的版本为Hadoop2.6.5
,所以源码分析也是基于Hadoop2.6.5
,如果对源码感兴趣的话可以点击这里下载源码
@Override
public void run(final JobConf job, final TaskUmbilicalProtocol umbilical)
throws IOException, ClassNotFoundException, InterruptedException {
this.umbilical = umbilical;
//这里判断它是否为MapTask
if (isMapTask()) {
// 从conf中获取ReduceTask的数量,如果没有ReduceTask,那么map阶段占100%
if (conf.getNumReduceTasks() == 0) {
mapPhase = getProgress().addPhase("map", 1.0f);
} else {
// 如果有ReduceTask,Map会多一个排序阶段,map阶段占67%,排序阶段占33%
mapPhase = getProgress().addPhase("map", 0.667f);
sortPhase = getProgress().addPhase("sort", 0.333f);
}
}
TaskReporter reporter = startReporter(umbilical);
boolean useNewApi = job.getUseNewMapper();
initialize(job, getJobID(), reporter, useNewApi);
// 检查和清理JobTask
if (jobCleanup) {
runJobCleanupTask(umbilical, reporter);
return;
}
if (jobSetup) {
runJobSetupTask(umbilical, reporter);
return;
}
if (taskCleanup) {
runTaskCleanupTask(umbilical, reporter);
return;
}
//新旧API的判断
if (useNewApi) {
//我们使用的是新的API,所以进入runNewMapper()方法
runNewMapper(job, splitMetaInfo, umbilical, reporter);
} else {
runOldMapper(job, splitMetaInfo, umbilical, reporter);
}
done(umbilical, reporter);
}
private <INKEY,INVALUE,OUTKEY,OUTVALUE>
void runNewMapper(final JobConf job,
final TaskSplitIndex splitIndex,
final TaskUmbilicalProtocol umbilical,
TaskReporter reporter
) throws IOException, ClassNotFoundException,
InterruptedException {
// 创建MapTask的上下文,注意,job里面已经有从HDFS中下载的各种配置信息
org.apache.hadoop.mapreduce.TaskAttemptContext taskContext =
new org.apache.hadoop.mapreduce.task.TaskAttemptContextImpl(job,
getTaskID(),
reporter);
// 创建一个mapper,这个mapper就是用户手写的类的实例,taskContext会从job中获取用户设定的MapperClass,然后传给mapper
org.apache.hadoop.mapreduce.Mapper<INKEY,INVALUE,OUTKEY,OUTVALUE> mapper =
(org.apache.hadoop.mapreduce.Mapper<INKEY,INVALUE,OUTKEY,OUTVALUE>)
//这里通过反射来创建类,反射的类就是用户手写的Mapper类
ReflectionUtils.newInstance(taskContext.getMapperClass(), job);
// 创建一个输入格式化类实例inputformat
org.apache.hadoop.mapreduce.InputFormat<INKEY,INVALUE> inputFormat =
(org.apache.hadoop.mapreduce.InputFormat<INKEY,INVALUE>)
//这里通过反射来创建一个InputFormatClass实例,
//注意,这个getInputFormatClass()方法在客户端提交作业时就出现过,默认是TextInputFormat
ReflectionUtils.newInstance(taskContext.getInputFormatClass(), job);
/** 重新构建Inputsplit实例split,为什么要重新构建呢?因为原split中包含了所有MapTask应该要执行的文件路径,起始位置,偏移量。
而单个MapTask只需要关心自己执行哪一部分就行,所以需要重新构建应该split来存放自己要执行的split信息**/
org.apache.hadoop.mapreduce.InputSplit split = null;
split = getSplitDetails(new Path(splitIndex.getSplitLocation()),
splitIndex.getStartOffset());
LOG.info("Processing split: " + split);
//这里使用split,inputFormat, reporter, taskContext构造出了一个RecordReader实例input
//进入NewTrackingRecordReader类去看这个input里面封装了哪些东西
org.apache.hadoop.mapreduce.RecordReader<INKEY,INVALUE> input =
new NewTrackingRecordReader<INKEY,INVALUE>
(split, inputFormat, reporter, taskContext);
NewTrackingRecordReader(org.apache.hadoop.mapreduce.InputSplit split,
org.apache.hadoop.mapreduce.InputFormat<K, V> 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 <Statistics> 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);
//real就是一个记录读取器,我们进入这个createRecordReader()看它是如何被创建出来的
this.real = inputFormat.createRecordReader(split, taskContext);
long bytesInCurr = getInputBytes(fsStats);
fileInputByteCounter.increment(bytesInCurr - bytesInPrev);
}
/**
* 这个方法由TextInputFormat类实现
**/
@Override
public RecordReader<LongWritable, Text>
createRecordReader(InputSplit split,
TaskAttemptContext context) {
String delimiter = context.getConfiguration().get(
"textinputformat.record.delimiter");
byte[] recordDelimiterBytes = null;
if (null != delimiter)
recordDelimiterBytes = delimiter.getBytes(Charsets.UTF_8);
//这个方法最终返回了一个行记录读取器LineRecordReader()
//也就是说经过层层包装,其实我们的实例input包含了一个LinerecordReader
//然后我们回到NewTrackingRecordReader类
return new LineRecordReader(recordDelimiterBytes);
}
//new 了一个NewTrackingRecordReader,将split,inputFormat传进去,这样input才能读出一条记录
//此时input已经包含了一个行记录读取器
org.apache.hadoop.mapreduce.RecordReader<INKEY,INVALUE> input =
new NewTrackingRecordReader<INKEY,INVALUE>
(split, inputFormat, reporter, taskContext);
job.setBoolean(JobContext.SKIP_RECORDS, isSkipping());
org.apache.hadoop.mapreduce.RecordWriter output = null;
// 创建一个输出对象output,暂时先跳过map的输出
if (job.getNumReduceTasks() == 0) {
output =
new NewDirectOutputCollector(taskContext, job, umbilical, reporter);
} else {
output = new NewOutputCollector(taskContext, job, umbilical, reporter);
}
//创建mapContext并将参数封装进去
org.apache.hadoop.mapreduce.MapContext<INKEY, INVALUE, OUTKEY, OUTVALUE>
mapContext =
new MapContextImpl<INKEY, INVALUE, OUTKEY, OUTVALUE>(job, getTaskID(),
input, output,
committer,
reporter, split);
//创建mapperContext并将mapContext传进去
org.apache.hadoop.mapreduce.Mapper<INKEY,INVALUE,OUTKEY,OUTVALUE>.Context
mapperContext =
new WrappedMapper<INKEY, INVALUE, OUTKEY, OUTVALUE>().getMapContext(
mapContext);
try {
//input初始化,它完成了一个很重要的工作,进入initialize()方法
input.initialize(split, mapperContext);
@Override
/**
*这个方法的实现类是NewTrackingRecordReader
*/
public void initialize(org.apache.hadoop.mapreduce.InputSplit split,
org.apache.hadoop.mapreduce.TaskAttemptContext context
) throws IOException, InterruptedException {
long bytesInPrev = getInputBytes(fsStats);
//这个real是之前创建出来的记录读取器,它又有一个initialize()方法,我们进入这个方法
real.initialize(split, context);
long bytesInCurr = getInputBytes(fsStats);
fileInputByteCounter.increment(bytesInCurr - bytesInPrev);
}
/**
*这个方法的实现类是LineRecordReader
*首先这里提出一个问题:split是按块的大小切分的,那么就有可能把一个单词切分成2部分,
*比如'hello'这个单词,有可能在做切分的时候,offset刚好到'he',这样一个完整的单词就被切成了'he'和'llo'两部分,这个问题该怎么解决呢?
*这个initialize()方法就是用来解决这个问题的
*/
public void initialize(InputSplit genericSplit,
TaskAttemptContext context) throws IOException {
//拿到切片信息
FileSplit split = (FileSplit) genericSplit;
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 {
//每一个map都会seek到自己的切片的偏移量的起始位置去执行任务
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.
//除了第一个MapTask,其他MapTask都会执行这一步
if (start != 0) {
//每次起始位置将读取到的第一条记录丢弃掉,然后在末尾多读取一条记录
//比如读取到'he',会多读取一条记录(比如'llo')并将它们拼接起来('hello'),然后下一个MapTask会丢弃'llo',这样就避免了重复读取
//下面回到NewTrackingRecordReader类
start += in.readLine(new Text(), 0, maxBytesToConsume(start));
}
//pos是第二条记录的偏移量
this.pos = start;
}
input.initialize(split, mapperContext);
//到这里我们发现,MapTask的run()方法调用了Mapper的run()方法,mapper的run()方法又调用了用户重写的run()方法,
//这个mapperContext里面包含了input,output,split,lineRecorder等信息
//现在再来看这个run()方法是怎么读取记录的,进入run()方法
mapper.run(mapperContext);
mapPhase.complete();
//用户的run()方法结束,开始走排序阶段
setPhase(TaskStatus.Phase.SORT);
statusUpdate(umbilical);
input.close();
input = null;
output.close(mapperContext);
output = null;
} finally {
closeQuietly(input);
closeQuietly(output, mapperContext);
}
}
public void run(Context context) throws IOException, InterruptedException {
setup(context);
try {
//context里面包含了行读取器,map()方法为用户手写的map方法,通过context.nextKeyValue()来获取一条记录,我们进入context.nextKeyValue()方法
while (context.nextKeyValue()) {
map(context.getCurrentKey(), context.getCurrentValue(), context);
}
} finally {
cleanup(context);
}
}
@Override
/**
*这个方法的实现类是LineRecordReader
*/
public boolean nextKeyValue() throws IOException {
if (key == null) {
key = new LongWritable();
}
//将第二条记录的偏移量赋值给Key,也就是说从第二条记录开始读
key.set(pos);
if (value == null) {
value = new Text();
}
int newSize = 0;
// 在这里可以看到,在读取完end的偏移量之后,还会再读取一条记录
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));
}
//如果newSize==0,说明没有key-value了,也就没有记录了,返回false
//回到mapper.run(mapperContext)方法
if (newSize == 0) {
key = null;
value = null;
return false;
} else {
return true;
}
}
public void run(Context context) throws IOException, InterruptedException {
setup(context);
try {
//context里面包含了行读取器,map()方法为用户手写的map方法,通过context.nextKeyValue()来获取一条记录,我们进入context.nextKeyValue()方法
while (context.nextKeyValue()) {
//假设有记录,执行map方法,我们进入map(context.getCurrentKey(), context.getCurrentValue(), context)方法
map(context.getCurrentKey(), context.getCurrentValue(), context);
}
} finally {
cleanup(context);
}
}
/**
*这个map方法就是用户要实现的map方法
*/
protected void map(KEYIN key, VALUEIN value,
Context context) throws IOException, InterruptedException {
context.write((KEYOUT) key, (VALUEOUT) value);
}
从对上面的源码的分析过程中我们发现,其实大部分工作都由LineRecordReader这个类来完成。input使用行记录读取器来实现读取文件数据。这段代码解释了每一个MapTask是如何去读取自己要执行的数据,如何避免完整的记录被切分成两部分,以及用户实现的map方法是如何被加载的。