前文已经分析了Map Task的输入,这次我们来分析较为复杂的输出,看看Map Task的输出到底做了哪些事情,分析完之后,将会对我们学习MapReduce有很大的帮助
这里依旧用的Hadoop的版本为2.7.2 ,工具是IDEA
由于上文我们已经有输入的分析,所以,这里直接找到MatpTask的run方法
我们直接往下看 ,
private <INKEY,INVALUE,OUTKEY,OUTVALUE>
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
org.apache.hadoop.mapreduce.Mapper<INKEY,INVALUE,OUTKEY,OUTVALUE> mapper =
(org.apache.hadoop.mapreduce.Mapper<INKEY,INVALUE,OUTKEY,OUTVALUE>)
ReflectionUtils.newInstance(taskContext.getMapperClass(), job);
// make the input format
org.apache.hadoop.mapreduce.InputFormat<INKEY,INVALUE> inputFormat =
(org.apache.hadoop.mapreduce.InputFormat<INKEY,INVALUE>)
ReflectionUtils.newInstance(taskContext.getInputFormatClass(), job);
// rebuild the input split
org.apache.hadoop.mapreduce.InputSplit split = null;
split = getSplitDetails(new Path(splitIndex.getSplitLocation()),
splitIndex.getStartOffset());
LOG.info("Processing split: " + split);
// 这里map的输入上文已经分析的很透彻了。所以我们直接往下看
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;
// get an output object
// 如果reduce的数量为0,map的输出就直接是hdfs了
if (job.getNumReduceTasks() == 0) {
output =
new NewDirectOutputCollector(taskContext, job, umbilical, reporter);
} else {
//我们来分析有reduce的情况下,map是怎么输出的
output = new NewOutputCollector(taskContext, job, umbilical, reporter);
}
org.apache.hadoop.mapreduce.MapContext<INKEY, INVALUE, OUTKEY, OUTVALUE>
mapContext =
new MapContextImpl<INKEY, INVALUE, OUTKEY, OUTVALUE>(job, getTaskID(),
input, output,
committer,
reporter, split);
org.apache.hadoop.mapreduce.Mapper<INKEY,INVALUE,OUTKEY,OUTVALUE>.Context
mapperContext =
new WrappedMapper<INKEY, INVALUE, OUTKEY, OUTVALUE>().getMapContext(
mapContext);
try {
input.initialize(split, mapperContext);
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);
}
}
我们进去至少有一到多个reduce的那个方法里面,看一看
NewOutputCollector(org.apache.hadoop.mapreduce.JobContext jobContext,
JobConf job,
TaskUmbilicalProtocol umbilical,
TaskReporter reporter
) throws IOException, ClassNotFoundException {
//这个可排序的集合我们先不看,先把下面的给看了
collector = createSortingCollector(job, reporter);
// map输出的时候,其实输入的是KVP,我们可以说reduce Task的别名,就是Partition分区
// 我们通过这个代码也可以看出来
partitions = jobContext.getNumReduceTasks();
//reduce分区数大于1
if (partitions > 1) {
// 这里,反射创建,一般都是如果用户设置就取用户的,用户没有设置就取框架的默认值,
// 我们看一下框架默认的分区器是什么
partitioner = (org.apache.hadoop.mapreduce.Partitioner<K,V>)
ReflectionUtils.newInstance(jobContext.getPartitionerClass(), job);
} else {
// reduce分区数等于1
//partitioner 分区器
partitioner = new org.apache.hadoop.mapreduce.Partitioner<K,V>() {
@Override
//获取分区号
public int getPartition(K key, V value, int numPartitions) {
// 这里返回是一定是 0 因为进到这个里面,Partitions一定是1.
// 这里我们可以得知,当我们只有1个reduce的时候,我们输出的任何key,他们的分区号都是0
// 他们都会在一个reduce里
return partitions - 1;
}
};
}
}
我们进去JobContext的实现类JobContextImpl,看一下默认的分区器是什么
public Class<? extends Partitioner<?,?>> getPartitionerClass()
throws ClassNotFoundException {
return (Class<? extends Partitioner<?,?>>)
//如果用户配置了分区器,就取用户的,如果用户没有设置,就取HashPartitioner,哈希
// 我们进去看一下HashPartitioner是怎么分区的
conf.getClass(PARTITIONER_CLASS_ATTR, HashPartitioner.class);
}
public int getPartition(K key, V value,
int numReduceTasks) {
// key的哈希值与上integer的最大值,这一步就是为了得到一个正整数
// 然后取模 如果是 reduce数量是3 ,那么取模就是 0 1 2
return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
}
我们知道,hash算法属于映射的,如果我们想要我们的数据均匀的散列,那么我们就可以使用hash算法,如果我们要保证我们数据的原有特征,这个时候用hash就不合适了,这个时候我们可以自定义分区器,如果我们自定义分区器,框架就不会取默认的hash,而是我们自己写的。
这里我们看完了,我们先出去看一下这个分区器在哪里被用的,我们的这个 NewOutputCollector方法就是输出的,我们看一看它的方法
我们看见了一个write,见名知意,我们context调用的write方法其实是NewOutputCollector的write方法,我们进去看看
public void write(K key, V value) throws IOException, InterruptedException {
//调用collect方法,往容器里面放东西,把我们的key,value,通过我们写的分区器获得我们的分区号
// 都放到里面,最终,map调用write传的是kv,但是在里面会转换成kvp,然后往collector容器里面放
collector.collect(key, value,
partitioner.getPartition(key, value, partitions));
}
我们现在往前看这个collector容器
NewOutputCollector(org.apache.hadoop.mapreduce.JobContext jobContext,
JobConf job,
TaskUmbilicalProtocol umbilical,
TaskReporter reporter
) throws IOException, ClassNotFoundException {
//现在我们进到这个createSortingCollector方法里面看看
collector = createSortingCollector(job, reporter);
......
}
private <KEY, VALUE> MapOutputCollector<KEY, VALUE>
createSortingCollector(JobConf job, TaskReporter reporter)
throws IOException, ClassNotFoundException {
MapOutputCollector.Context context =
new MapOutputCollector.Context(this, job, reporter);
//我们看到job.getClasses,就是往配置文件里面取,如果我们配置了,就取我们配置的
// 如果我们没有配置,就是默认的MapOutputBuffer,map输出缓冲区。
Class<?>[] collectorClasses = job.getClasses(
JobContext.MAP_OUTPUT_COLLECTOR_CLASS_ATTR, MapOutputBuffer.class);
int remainingCollectors = collectorClasses.length;
Exception lastException = null;
//迭代这个输出缓冲区
for (Class clazz : collectorClasses) {
try {
if (!MapOutputCollector.class.isAssignableFrom(clazz)) {
throw new IOException("Invalid output collector class: " + clazz.getName() +
" (does not implement MapOutputCollector)");
}
Class<? extends MapOutputCollector> subclazz =
clazz.asSubclass(MapOutputCollector.class);
LOG.debug("Trying map output collector class: " + subclazz.getName());
//输出缓冲区赋到我们的这个容器里
MapOutputCollector<KEY, VALUE> collector =
ReflectionUtils.newInstance(subclazz, job);
// 输出初始化,,,,我们在输入的时候有输入初始化,这里是输出初始化
// 我们看一下容器的初始化,看collector的初始化就是MapOutputBuffer的初始化
collector.init(context);
LOG.info("Map output collector class = " + collector.getClass().getName());
// 最终返回这个容器
return collector;
} catch (Exception e) {
String msg = "Unable to initialize MapOutputCollector " + clazz.getName();
if (--remainingCollectors > 0) {
msg += " (" + remainingCollectors + " more collector(s) to try)";
}
lastException = e;
LOG.warn(msg, e);
}
}
throw new IOException("Initialization of all the collectors failed. " +
"Error in last collector was :" + lastException.getMessage(), lastException);
}
我们要进去 collector.init方法,看一下MapOutputBuffer的初始化,选择MapOutputBuffer的init方法
public void init(MapOutputCollector.Context context
) throws IOException, ClassNotFoundException {
job = context.getJobConf();
reporter = context.getReporter();
mapTask = context.getMapTask();
mapOutputFile = mapTask.getMapOutputFile();
sortPhase = mapTask.getSortPhase();
spilledRecordsCounter = reporter.getCounter(TaskCounter.SPILLED_RECORDS);
partitions = job.getNumReduceTasks();
rfs = ((LocalFileSystem)FileSystem.getLocal(job)).getRaw();
//sanity checks
// 在这里,定义了一个float类型的溢写的阈值百分比
final float spillper =
// 在这里,job从配置文件当中取,
//如果用户配置了,就取用户的,如果没有,就是默认的0.8,百分之80的意思
job.getFloat(JobContext.MAP_SORT_SPILL_PERCENT, (float)0.8);
//定义了一个sortmb,从配置文件中去取,如果用户配置了就取用户的,如果没有就是默认的100Mb
// 这里解释一下,map完事之后,会放到一个内存缓冲区里面,这个100Mb就是这个内存缓冲区的大小
//上边那个0.8的阈值,说的就是只要使用到百分之80了,就可以做溢写,写到磁盘去。
// 这个100Mb要根据具体情况去分析,如果kv很大,这个100Mb就不合适,用户可以适当的调大
//这里要百分之80的阈值做溢写的原因是:如果没有这百分之80做溢写这个事情
// 整个100M来做缓冲区,map一直输出把缓冲区给占满,没有空闲了,这个时候map会阻塞,就不能往后写了
// 会把空间给锁住,锁完之后开始做溢写,写完磁盘把空间腾出来之后,才可以继续往里写,这是个线性的时间
// 如果我们做了这百分之80的阈值,map输出向这百分之80写,写满了,还有百分之20,
// 这个时候,map向剩下的百分之20去写的同时,这百分之80做溢写,这个会有一部分时间轴重叠了
// 会压缩我们的时间,这就是为什么要80做阈值。当然我们可以根据数据来做调整。
final int sortmb = job.getInt(JobContext.IO_SORT_MB, 100);
indexCacheMemoryLimit = job.getInt(JobContext.INDEX_CACHE_MEMORY_LIMIT,
INDEX_CACHE_MEMORY_LIMIT_DEFAULT);
if (spillper > (float)1.0 || spillper <= (float)0.0) {
throw new IOException("Invalid \"" + JobContext.MAP_SORT_SPILL_PERCENT +
"\": " + spillper);
}
if ((sortmb & 0x7FF) != sortmb) {
throw new IOException(
"Invalid \"" + JobContext.IO_SORT_MB + "\": " + sortmb);
}
sorter = ReflectionUtils.newInstance(job.getClass("map.sort.class",
QuickSort.class, IndexedSorter.class), job);
// buffers and accounting
int maxMemUsage = sortmb << 20;
maxMemUsage -= maxMemUsage % METASIZE;
kvbuffer = new byte[maxMemUsage];
bufvoid = kvbuffer.length;
kvmeta = ByteBuffer.wrap(kvbuffer)
.order(ByteOrder.nativeOrder())
.asIntBuffer();
setEquator(0);
bufstart = bufend = bufindex = equator;
kvstart = kvend = kvindex;
maxRec = kvmeta.capacity() / NMETA;
softLimit = (int)(kvbuffer.length * spillper);
bufferRemaining = softLimit;
LOG.info(JobContext.IO_SORT_MB + ": " + sortmb);
LOG.info("soft limit at " + softLimit);
LOG.info("bufstart = " + bufstart + "; bufvoid = " + bufvoid);
LOG.info("kvstart = " + kvstart + "; length = " + maxRec);
// k/v serialization
comparator = job.getOutputKeyComparator();
keyClass = (Class<K>)job.getMapOutputKeyClass();
valClass = (Class<V>)job.getMapOutputValueClass();
serializationFactory = new SerializationFactory(job);
keySerializer = serializationFactory.getSerializer(keyClass);
keySerializer.open(bb);
valSerializer = serializationFactory.getSerializer(valClass);
valSerializer.open(bb);
// output counters
mapOutputByteCounter = reporter.getCounter(TaskCounter.MAP_OUTPUT_BYTES);
mapOutputRecordCounter =
reporter.getCounter(TaskCounter.MAP_OUTPUT_RECORDS);
fileOutputByteCounter = reporter
.getCounter(TaskCounter.MAP_OUTPUT_MATERIALIZED_BYTES);
// compression
if (job.getCompressMapOutput()) {
Class<? extends CompressionCodec> codecClass =
job.getMapOutputCompressorClass(DefaultCodec.class);
codec = ReflectionUtils.newInstance(codecClass, job);
} else {
codec = null;
}
// combiner
final Counters.Counter combineInputCounter =
reporter.getCounter(TaskCounter.COMBINE_INPUT_RECORDS);
combinerRunner = CombinerRunner.create(job, getTaskID(),
combineInputCounter,
reporter, null);
if (combinerRunner != null) {
final Counters.Counter combineOutputCounter =
reporter.getCounter(TaskCounter.COMBINE_OUTPUT_RECORDS);
combineCollector= new CombineOutputCollector<K,V>(combineOutputCounter, reporter, job);
} else {
combineCollector = null;
}
spillInProgress = false;
minSpillsForCombine = job.getInt(JobContext.MAP_COMBINE_MIN_SPILLS, 3);
spillThread.setDaemon(true);
spillThread.setName("SpillThread");
spillLock.lock();
try {
spillThread.start();
while (!spillThreadRunning) {
spillDone.await();
}
} catch (InterruptedException e) {
throw new IOException("Spill thread failed to initialize", e);
} finally {
spillLock.unlock();
}
if (sortSpillException != null) {
throw new IOException("Spill thread failed to initialize",
sortSpillException);
}
}
在这里,我觉得很有必要来深入了解MRShuffle过程的缓冲区,这里先介绍一下缓冲区,明白了之后,在来做源码的分析
我们通过上面整个源码的分析,也清楚的知道了MapOutputBuffer内部使用了一个环形缓冲区来暂时存储用户输出的数据,当我们的缓冲区达到框架默认的百分之80阈值之后 ,才会将我们缓冲区内的数据写到磁盘,而缓冲区的设计也会直接影响到我们Map Task的输出效率,我们先来了解一下都有什么样的缓冲区
双向缓冲区,是对单向缓冲区的一个改进,它使用了两个缓冲区,一个用于写数据,另一个将写满的数据写到磁盘上,这样,两个缓冲区就能交替读写,从而提升了效率,不过,双向缓冲区只能一定程度上让读写并行,仍然会存在读写等待的问题
环形缓存区,是比双向缓冲区更好的,当缓冲区的使用率达到一定的阈值之后,遍开始像磁盘写入数据,同时,我们还可以继续向不断增加的剩余空间继续写入数据,进而达到了真正的读写并行。
我们MapReduce的MapOutputBuffer正是采用了环形内存缓冲区来保存数据,(其实我们的hadoop也曾经才用过双向缓冲区) 由线程SpillThread将数据写到一个临时文件中,当所有数据处理完毕后,在对所有临时文件进行一次合并以生成一个最终文件。这样使用环形缓冲区就使得Map Task的Collect阶段和Spill阶段可并行进行了(Spill即为溢写操作)
我们深入的介绍一下MapOutputBuffer采用的环形缓冲区,环形缓冲区的内部结构采用了两级索引,涉及了三个环形内存缓冲区,分别是kvoffsets、kvindices和kvbuffer,这三个环形缓冲区总共占的内存空间大小为100M(框架默认的情况,在上面的源码中我们也看了),接下来我们分别介绍一下这三个环形缓冲区的含义
kvoffsets
看见这个名字,我们就知道,它就是我们key/value的偏移量索引数组,用于保存我们的key/value信息在位置索引kvindices中的偏移量,考虑到一对key/value需占用数组kvoffsets的1个int(整型)大小,数组kvindices的3个int大小,(分别保存所在partition号、key开始位置和value开始位置),所以Hadoop按比例1 : 3将大小为{io.sort.record.percent}(默认为0.8)*{io.sort.mb}(默认为100)的内存空间分配给数组kvoffsets和kvindices,计算公式这里就不详细看了,,结果就是kvbuffer占95%,kvoffsets占3.75%,kvindices1.25%, 当该数组使用率超过io.sort.spill.percent(80%)后,便会触发线程SpillThread将数据写入磁盘。
kvindices
kvindices即位置索引数组,用于保存key/value值在数据缓冲区kvbuffer中的起始位置。
kvbuffer
kvbuffer则是数据缓冲区,用于保存真正的key/value的值,默认情况下最多可使用io.sort. mb中的95%,当使用该缓冲区使用率超过io.sort.spill.percent(80%)后,便会触发线程SpillThread将数据写入磁盘。
下面我们则说一下环形缓冲区kvoffsets和kvbuffer的数据写入的过程
(1)环形缓冲区kvoffsets
这个缓冲区通常用一个线性的缓冲区模拟实现环形缓冲区,并通过取模操作实现循环数据存储。下面介绍环形缓冲区kvoffsets的写数据过程。这个写入的过程由指针kvstart/kvend/kvindex来进行控制,其中kvstart表示存有数据的内存段初始位置,kvindex则表示未存储数据的内存段初始位置,而在正常写入的情况下,kvend=kvstart,一旦满足了溢写条件(80%),则kvend=kvindex,此时指针区间[kvstart,kvend)则为有效数据区间。具体的操作如下
操作1:写入缓冲区
直接将数据写入kvindex指针指向的内存空间,同时移动kvindex指向下一个可写入的内存空间首地址,kvindex移动公式为:kvindex=(kvindex+1)%kvoffsets.length。由于kvoffsets为环形缓冲区,因此可能涉及两种写入情况。
情况(1):kvindex > kvend,在这种情况下,指针kvindex在指针kvend后面,如果向缓冲区中写入一个字符串,则kvindex指针后移一位。
情况(2):kvindex <= kvend,如图8-11所示。在这种情况下,指针kvindex位于指针kvend前面,如果向缓冲区中写入一个字符串,则kvindex指针后移一位。
操作2:溢写到磁盘的时候。当kvoffsets内存空间使用率超过io.sort.spill.percent(默认是80%)后,需将内存中数据写到磁盘上。为了判断是否满足该条件,需先求出kvoffsets已使用内存。如果kvindex>kvend,则已使用内存大小为kvindex-kvend;否则,已使用内存大小为kvoffsets. length-(kvend-kvindex)。
(2)环形缓冲区kvbuffer
环形缓冲区kvbuffer的读写操作过程由指针bufstart/bufend/bufvoid/bufindex/bufmark控制,其中,bufstart/bufend/bufindex含义与kvstart/kvend/kvindex相同,而bufvoid指向kvbuffer中有效内存结束,bufmark表示最后写入的一个完整key/value结束位置
情况(1):初始状态。初始状态下,bufstart=bufend=bufindex=bufmark=0,bufvoid=kvbuffer.length
情况(2):写入一个key。写入一个key后,需移动bufindex指针到可写入内存初始位置
情况(3):写入一个value。写入key对应的value后,除移动bufindex指针外,还要移动bufmark指针,表示已经写入一个完整的key/value
情况(4):不断写入key/value,直到满足溢写条件,即kvoffsets或者kvbuffer空间使用率超过io.sort.spill.percent(默认值为80%)。此时需要将数据写到磁盘上
情况(5):溢写。如果达到溢写条件,则让bufend=bufindex,并将缓冲区[bufstart,bufend)之间的数据写到磁盘上
溢写完成之后,恢复正常写入状态,让bufstart=bufend
.在溢写的同时,Map Task仍可向kvbuffer中写入数据
情况(6):写入key时,发生跨界现象。当写入某个key时,缓冲区尾部剩余空间不足以容纳整个key值,此时需要将key值分开存储,其中一部分存到缓冲区末尾,另外一部分存到缓冲区首部
情况(7):调整key位置,防止key跨界现象。由于key是排序的关键字,通常需交给RawComparator进行排序,而它要求排序关键字必须在内存中连续存储,因此不允许key跨界存储。为解决该问题,Hadoop将跨界的key值重新存储到缓冲区的首位置。通常可分为以下两种情况
System.arraycopy()
src:源数组;
srcPos:源数组要复制的起始位置;
dest:目的数组;
destPos:目的数组放置的起始位置;
length:复制的长度。
int headbytelen = bufvoid - bufmark;
System.arraycopy(kvbuffer,0,kvbuffer,headbytelen,bufindex);
System.arraycopy(kvbuffer,bufvoid,kvbuffer,0,headbytelen);
//申请一个临时缓冲区
byte[] keytmp = new byte[bufindex];
System.arraycopy(kvbuffer, 0, keytmp, 0, bufindex);
bufindex = 0;
//将key值写入缓冲区开始位置
out.write(kvbuffer, bufmark, headbytelen);
out.write(keytmp);
情况(8):某个key或者value太大,以至于整个缓冲区不能容纳它。如果一条记录的key或value太大,整个缓冲区都不能容纳它,则MapTask会抛出MapBufferTooSmallException异常,并将该记录单独输出到一个文件中。
当然在从hadoop0.21开始,有两个修改点,
通过让索引和记录共享一个环形缓冲区,使得Map Task能够最大限度地利用io.sort.mb空间,进而减少磁盘溢写次数,提高效率。
这里的缓冲区就很简单了,我相信只要最开始的环形缓冲区能看懂,那么这个从0.21版本开始的优化后的缓冲区大家也能看懂,相对于之前已经变的没有那么麻烦了。
到这里,我们的缓冲区就已经介绍完了,相信也都应该有很深入的了解,接下来我们看一看环形缓冲区的源码分析
public static class MapOutputBuffer<K extends Object, V extends Object>
implements MapOutputCollector<K, V>, IndexedSortable {
// 具体的分区号
private int partitions;
private JobConf job;
private TaskReporter reporter;
//map输出端的key类型
private Class<K> keyClass;
//map输出端的value类型
private Class<V> valClass;
//默认行比较器
private RawComparator<K> comparator;
// 序列化工厂对象,,,因为保存在环形缓冲区的数据,kv会序列化为字节数组,meta也会序列化
private SerializationFactory serializationFactory;
//用来序列号key值,存储到buffer里
private Serializer<K> keySerializer;
//用来序列化value值,存储到buffer里
private Serializer<V> valSerializer;
//Combiner的运行定义
private CombinerRunner<K,V> combinerRunner;
//Combine输出集合
private CombineOutputCollector<K, V> combineCollector;
// Compression for map-outputs
// 默认map输出的压缩器
private CompressionCodec codec;
// k/v accounting
//保存元数据的buffer包装成int数组 占4byte
private IntBuffer kvmeta; // metadata overlay on backing store
//保存元数据的内存段初始位置 (第一次的位置应该是倒数第四个int元素位置)
int kvstart; // marks origin of spill metadata
//保存元数据在(内存)int数组的结束位置
int kvend; // marks end of spill metadata
//未存储数据的内存段初始位置(存有下一个元数据的起始位置)
int kvindex; // marks end of fully serialized records
//分割meta和key value内容的位置
int equator; // marks origin of meta/serialization
//保存kv对的起始位置
int bufstart; // marks beginning of spill
//保存kv对的结束位置
int bufend; // marks beginning of collectable
//保存kv对的结束位置
int bufmark; // marks end of record
//未存储数据的内存段初始位置(保存下一个kv的起始位置)
int bufindex; // marks end of collected
//buffer缓冲区的总长度
int bufvoid; // marks the point where we should stop
// reading at the end of the buffer
//环形缓冲区
byte[] kvbuffer; // main output buffer
//校验是否到边界的数组
private final byte[] b0 = new byte[0];
// 一个元数据数组由4个部分组成,在上面的图中也都已经解释过了
// valstart相对于kvindex的偏移量
private static final int VALSTART = 0; // val offset in acct
// keystart元数据相对于kvindex的偏移量
private static final int KEYSTART = 1; // key offset in acct
// parition元数据相对于kvindex的偏移量
private static final int PARTITION = 2; // partition offset in acct
//vallen元数据相对于kvindex的偏移量
private static final int VALLEN = 3; // length of value
// 一对key value的meta数据在kvmeta中占用的字节数 4
private static final int NMETA = 4; // num meta ints
// 算出元数据数组的总占字节为16字节 4 * 4 = 16b
private static final int METASIZE = NMETA * 4; // size in bytes
// spill accounting
// 为溢写操作准备的东西
private int maxRec;
private int softLimit;
boolean spillInProgress;;
// 检查buffer是否达到阈值
int bufferRemaining;
volatile Throwable sortSpillException = null;
int numSpills = 0;
private int minSpillsForCombine;
private IndexedSorter sorter;
final ReentrantLock spillLock = new ReentrantLock();
final Condition spillDone = spillLock.newCondition();
final Condition spillReady = spillLock.newCondition();
final BlockingBuffer bb = new BlockingBuffer();
volatile boolean spillThreadRunning = false;
//溢写线程
final SpillThread spillThread = new SpillThread();
//溢写的文件系统
private FileSystem rfs;
key/value序列化的数据和元数据在环形缓冲区中的存储是由equator分隔的,key/value按照索引递增的方向存储,meta则按照索引递减的方向存储,将其数组抽象为一个环形结构之后,以equator为界,key/value顺时针存储,meta逆时针存储。
接下来我们看一下环形缓冲区的初始化操作
public void init(MapOutputCollector.Context context
) throws IOException, ClassNotFoundException {
//通过上下文获取job对象
job = context.getJobConf();
reporter = context.getReporter();
//返回我们的 maptask
mapTask = context.getMapTask();
//这个对象代表我们map输出临时文件的位置
mapOutputFile = mapTask.getMapOutputFile();
//设置我们的排序阶段
sortPhase = mapTask.getSortPhase();
spilledRecordsCounter = reporter.getCounter(TaskCounter.SPILLED_RECORDS);
//获取分区个数
partitions = job.getNumReduceTasks();
//溢写文件或最后的合并文件存储到本地(默认是存储到本地磁盘)
rfs = ((LocalFileSystem)FileSystem.getLocal(job)).getRaw();
//sanity checks
//获取我们溢写的阈值(默认百分之80)
final float spillper =
job.getFloat(JobContext.MAP_SORT_SPILL_PERCENT, (float)0.8);
//获取我们默认的环形缓冲区大小(默认100mb)
final int sortmb = job.getInt(JobContext.IO_SORT_MB, 100);
// 所有的spill index 在内存所占的大小的阈值
indexCacheMemoryLimit = job.getInt(JobContext.INDEX_CACHE_MEMORY_LIMIT,
INDEX_CACHE_MEMORY_LIMIT_DEFAULT);
if (spillper > (float)1.0 || spillper <= (float)0.0) {
throw new IOException("Invalid \"" + JobContext.MAP_SORT_SPILL_PERCENT +
"\": " + spillper);
}
if ((sortmb & 0x7FF) != sortmb) {
throw new IOException(
"Invalid \"" + JobContext.IO_SORT_MB + "\": " + sortmb);
}
// 排序的实现类,这里可以自己实现,框架默认使用快排
sorter = ReflectionUtils.newInstance(job.getClass("map.sort.class",
QuickSort.class, IndexedSorter.class), job);
// buffers and accounting
//将缓冲区的内存单位转换为字节
int maxMemUsage = sortmb << 20;
// METASIZE是我们元数据的长度,元数据存储的数据由4个部分组成,分别为
// VALSTART、KEYSTART、PARTITION、VALLEN,每个占到4个字节,所以METASIZE长度为16b
// 下面则是计算buffer中最多有多少byte来存元数据
maxMemUsage -= maxMemUsage % METASIZE;
//创建kv环形缓冲区
kvbuffer = new byte[maxMemUsage];
//初始bufvoid = kvbuffer的长度
bufvoid = kvbuffer.length;
//将kvbuffer转化为int型的kvmeta 以int为单位,也就是4byte
kvmeta = ByteBuffer.wrap(kvbuffer)
.order(ByteOrder.nativeOrder())
.asIntBuffer();
//设置初始buf和kvmeta的分界线
setEquator(0);
//给kvindex进行赋值
bufstart = bufend = bufindex = equator;
kvstart = kvend = kvindex;
//kvmeta中存放元数据实体的最大个数
maxRec = kvmeta.capacity() / NMETA;
//buffer spill时的阈值 (kvbuffer.length*spiller)
softLimit = (int)(kvbuffer.length * spillper);
//bufferRemaining这个变量作为衡量是否到达阈值
bufferRemaining = softLimit;
LOG.info(JobContext.IO_SORT_MB + ": " + sortmb);
LOG.info("soft limit at " + softLimit);
LOG.info("bufstart = " + bufstart + "; bufvoid = " + bufvoid);
LOG.info("kvstart = " + kvstart + "; length = " + maxRec);
// k/v serialization
//kv序列化的工作
//获取比较器
comparator = job.getOutputKeyComparator();
//获取map输出端的key类型,用户设置取用户的,用户没有设置默认是Object类型
keyClass = (Class<K>)job.getMapOutputKeyClass();
//获取我们map输出端的value类型,用户设置取用户的,用户没有设置默认是Object类型
valClass = (Class<V>)job.getMapOutputValueClass();
//为序列化工厂赋值
serializationFactory = new SerializationFactory(job);
//获取key的序列化
keySerializer = serializationFactory.getSerializer(keyClass);
//bb跟踪源码得知是一个new的普通buffer
//将bb作为key序列化写入的output
keySerializer.open(bb);
//获取value的序列化
valSerializer = serializationFactory.getSerializer(valClass);
//bb跟踪源码得知是一个new的普通buffer
//将bb作为value序列化写入的output
valSerializer.open(bb);
// output counters
//获取map输出计数器
mapOutputByteCounter = reporter.getCounter(TaskCounter.MAP_OUTPUT_BYTES);
mapOutputRecordCounter =
reporter.getCounter(TaskCounter.MAP_OUTPUT_RECORDS);
fileOutputByteCounter = reporter
.getCounter(TaskCounter.MAP_OUTPUT_MATERIALIZED_BYTES);
// compression
//判断用户是否设置了压缩格式
// 获取默认压缩格式,默认是基于Zlib算法的压缩器 DefaultCodec
// 一旦启用了压缩机制,Hadoop会为每条记录的key和value值进行压缩
if (job.getCompressMapOutput()) {
Class<? extends CompressionCodec> codecClass =
job.getMapOutputCompressorClass(DefaultCodec.class);
codec = ReflectionUtils.newInstance(codecClass, job);
} else {
//没有设置则没有压缩
codec = null;
}
// combiner
//当所有数据处理完成后,Map Task对所有临时文件进行一次合并,以确保最终只会生成一个数据文件
final Counters.Counter combineInputCounter =
reporter.getCounter(TaskCounter.COMBINE_INPUT_RECORDS);
combinerRunner = CombinerRunner.create(job, getTaskID(),
combineInputCounter,
reporter, null);
//判断是否开启combiner
if (combinerRunner != null) {
final Counters.Counter combineOutputCounter =
reporter.getCounter(TaskCounter.COMBINE_OUTPUT_RECORDS);
combineCollector= new CombineOutputCollector<K,V>(combineOutputCounter, reporter, job);
} else {
combineCollector = null;
}
spillInProgress = false;
minSpillsForCombine = job.getInt(JobContext.MAP_COMBINE_MIN_SPILLS, 3);
spillThread.setDaemon(true);
//设置线程名字
spillThread.setName("SpillThread");
spillLock.lock();
//将数据写入缓冲区
try {
//启动溢写线程
spillThread.start();
//进行轮询的方式询问
while (!spillThreadRunning) {
//等待spillThread线程结束
spillDone.await();
}
} catch (InterruptedException e) {
throw new IOException("Spill thread failed to initialize", e);
} finally {
spillLock.unlock();
}
if (sortSpillException != null) {
throw new IOException("Spill thread failed to initialize",
sortSpillException);
}
}
下面我们看一下写入buffer的操作
//这个collect的操作就是序列化kv到缓冲区中
public synchronized void collect(K key, V value, final int partition
) throws IOException {
reporter.progress();
//着里判断序列化类型的,我们先不看
if (key.getClass() != keyClass) {
throw new IOException("Type mismatch in key from map: expected "
+ keyClass.getName() + ", received "
+ key.getClass().getName());
}
if (value.getClass() != valClass) {
throw new IOException("Type mismatch in value from map: expected "
+ valClass.getName() + ", received "
+ value.getClass().getName());
}
if (partition < 0 || partition >= partitions) {
throw new IOException("Illegal partition for " + key + " (" +
partition + ")");
}
//校验溢写有没有异常发出
checkSpillException();
//新数据collect时,先将剩余的空间减去元数据的长度,之后进行判断
bufferRemaining -= METASIZE;
if (bufferRemaining <= 0) {
// start spill if the thread is not running and the soft limit has been
// reached
// 如果到达溢写的条件,那么就先溢写
spillLock.lock();
try {
do {
//我们跟踪源码 首次溢写时,spillInProgress是false
if (!spillInProgress) {
// 得到kvindex的byte位置(因为是int,所以需 * 4)
final int kvbidx = 4 * kvindex;
// 得到kvend的byte位置 (因为是int,所以需 * 4)
final int kvbend = 4 * kvend;
// serialized, unspilled bytes always lie between kvindex and
// bufindex, crossing the equator. Note that any void space
// created by a reset must be included in "used" bytes
// 开始序列化,没有溢出的字节的位置必须位于kvindex和bufindex
// 元数据的信息必须写在equator指针后面,逆着来写
final int bUsed = distanceTo(kvbidx, bufindex);
final boolean bufsoftlimit = bUsed >= softLimit;
if ((kvbend + METASIZE) % kvbuffer.length !=
equator - (equator % METASIZE)) {
// spill finished, reclaim space
resetSpill();
bufferRemaining = Math.min(
distanceTo(bufindex, kvbidx) - 2 * METASIZE,
softLimit - bUsed) - METASIZE;
continue;
} else if (bufsoftlimit && kvindex != kvend) {
// spill records, if any collected; check latter, as it may
// be possible for metadata alignment to hit spill pcnt
startSpill();
final int avgRec = (int)
(mapOutputByteCounter.getCounter() /
mapOutputRecordCounter.getCounter());
// leave at least half the split buffer for serialization data
// ensure that kvindex >= bufindex
final int distkvi = distanceTo(bufindex, kvbidx);
final int newPos = (bufindex +
Math.max(2 * METASIZE - 1,
Math.min(distkvi / 2,
distkvi / (METASIZE + avgRec) * METASIZE)))
% kvbuffer.length;
setEquator(newPos);
bufmark = bufindex = newPos;
final int serBound = 4 * kvend;
// bytes remaining before the lock must be held and limits
// checked is the minimum of three arcs: the metadata space, the
// serialization space, and the soft limit
bufferRemaining = Math.min(
// metadata max
distanceTo(bufend, newPos),
Math.min(
// serialization max
distanceTo(newPos, serBound),
// soft limit
softLimit)) - 2 * METASIZE;
}
}
} while (false);
} finally {
spillLock.unlock();
}
}
try {
// 不溢写,将key value 及元数据信息写入缓冲区
// serialize key bytes into buffer
int keystart = bufindex;
//将key序列化,并且写入到kvbuffer中,并且移动bufindex
keySerializer.serialize(key);
if (bufindex < keystart) {
//如果key所占空间被bufvoid分隔的化,那么就移动key
// 并且要将它的值放在连续的空间内,以便于sort时候key的比较
// wrapped the key; must make contiguous
bb.shiftBufferedKey();
keystart = 0;
}
// serialize value bytes into buffer
final int valstart = bufindex;
//将value序列化,并且写入kvbuffer中,并且移动bufindex的值
valSerializer.serialize(value);
// It's possible for records to have zero length, i.e. the serializer
// will perform no writes. To ensure that the boundary conditions are
// checked and that the kvindex invariant is maintained, perform a
// zero-length write into the buffer. The logic monitoring this could be
// moved into collect, but this is cleaner and inexpensive. For now, it
// is acceptable.
bb.write(b0, 0, 0);
// the record must be marked after the preceding write, as the metadata
// for this record are not yet written
//对kvindex进行标记为元数据
int valend = bb.markRecord();
//获取计数器
mapOutputRecordCounter.increment(1);
//kv对的字节长度进行计数
mapOutputByteCounter.increment(
distanceTo(keystart, valend, bufvoid));
// write accounting info
//往我们的kv元数据里面放入partition
kvmeta.put(kvindex + PARTITION, partition);
//往我们的kv元数据里面放入keystart
kvmeta.put(kvindex + KEYSTART, keystart);
//往我们的kv元数据里面放入valstart
kvmeta.put(kvindex + VALSTART, valstart);
//往我们的kv元数据里面放入VALLEN
kvmeta.put(kvindex + VALLEN, distanceTo(valstart, valend));
// advance kvindex
// // 移动kvindex,为下一个kvmeta做准备,移动了int数组的四个位置
kvindex = (kvindex - NMETA + kvmeta.capacity()) % kvmeta.capacity();
} catch (MapBufferTooSmallException e) {
LOG.info("Record too large for in-memory buffer: " + e.getMessage());
// 当我们的kv值较大的时候(针对于80MB),会直接溢写到磁盘
spillSingleRecord(key, value, partition);
//计数
mapOutputRecordCounter.increment(1);
return;
}
}
然后执行write方法将key/value写入kvbuffer中就行,具体细节这里不多做展开,我们直接看一下最后溢写的flush方法的核心方法sortAndSpill
private void sortAndSpill() throws IOException, ClassNotFoundException,
InterruptedException {
//approximate the length of the output file to be the length of the
//buffer + header lengths for the partitions
final long size = distanceTo(bufstart, bufend, bufvoid) +
partitions * APPROX_HEADER_LENGTH;
FSDataOutputStream out = null;
try {
// create spill file
// 根据分区创建溢写文件
final SpillRecord spillRec = new SpillRecord(partitions);
final Path filename =
mapOutputFile.getSpillFileForWrite(numSpills, size);
//打开文件输出流
out = rfs.create(filename);
//当前位置能存放多少个元数据 /4
final int mstart = kvend / NMETA;
// 元数据在mstart和mend之间,(mstart - mend)则是元数据的个数
final int mend = 1 + // kvend is a valid record
(kvstart >= kvend
? kvstart
: kvmeta.capacity() + kvstart) / NMETA;
// 排序 只对元数据进行排序,只调整元数据在kvmeta中的顺序
// 排序规则是MapOutputBuffer.compare,
// 先按照分区编号partition进行排序,然后按照key进行排序。这样
// 经过排序后,数据以分区为单位聚集在一 起,且同一分区内所有数据按照key有序。
sorter.sort(MapOutputBuffer.this, mstart, mend, reporter);
int spindex = mstart;
// 存放该分区在数据文件中的信息
final IndexRecord rec = new IndexRecord();
//创建临时文件,,,,hadoop默认的临时文件为IFile格式
final InMemValBytes value = new InMemValBytes();
for (int i = 0; i < partitions; ++i) {
IFile.Writer<K, V> writer = null;
try {
long segmentStart = out.getPos();
FSDataOutputStream partitionOut = CryptoUtils.wrapIfNecessary(job, out);
writer = new Writer<K, V>(job, partitionOut, keyClass, valClass, codec,
spilledRecordsCounter);
// 往磁盘写入前先判断是否设置了combiner,如果设置,则进行一次归并操作
if (combinerRunner == null) {
// spill directly
// 开始溢写操作
DataInputBuffer key = new DataInputBuffer();
// 开始写入相同partition的数据操作
while (spindex < mend &&
kvmeta.get(offsetFor(spindex % maxRec) + PARTITION) == i) {
final int kvoff = offsetFor(spindex % maxRec);
int keystart = kvmeta.get(kvoff + KEYSTART);
int valstart = kvmeta.get(kvoff + VALSTART);
key.reset(kvbuffer, keystart, valstart - keystart);
getVBytesForOffset(kvoff, value);
writer.append(key, value);
++spindex;
}
} else {
int spstart = spindex;
while (spindex < mend &&
kvmeta.get(offsetFor(spindex % maxRec)
+ PARTITION) == i) {
++spindex;
}
// Note: we would like to avoid the combiner if we've fewer
// than some threshold of records for a partition
if (spstart != spindex) {
combineCollector.setWriter(writer);
RawKeyValueIterator kvIter =
new MRResultIterator(spstart, spindex);
combinerRunner.combine(kvIter, combineCollector);
}
}
// close the writer
// 写操作流关闭
writer.close();
// record offsets
rec.startOffset = segmentStart;
rec.rawLength = writer.getRawLength() + CryptoUtils.cryptoPadding(job);
rec.partLength = writer.getCompressedLength() + CryptoUtils.cryptoPadding(job);
spillRec.putIndex(rec, i);
writer = null;
} finally {
if (null != writer) writer.close();
}
}
//判断内存中的index文件是否超出阈值,超出则将index文件写入磁盘
// 当超出阈值的化就把当前index和之后的index写入磁盘
if (totalIndexCacheMemory >= indexCacheMemoryLimit) {
// create spill index file
// 创建index文件
Path indexFilename =
mapOutputFile.getSpillIndexFileForWrite(numSpills, partitions
* MAP_OUTPUT_INDEX_RECORD_LENGTH);
spillRec.writeToFile(indexFilename, job);
} else {
indexCacheList.add(spillRec);
totalIndexCacheMemory +=
spillRec.size() * MAP_OUTPUT_INDEX_RECORD_LENGTH;
}
LOG.info("Finished spill " + numSpills);
++numSpills;
} finally {
if (out != null) out.close();
}
}
这里总结一下这个方法的步骤
到这里,我们的环形缓冲区就已经介绍完了,相关核心的源码也都看过了
***接下来,我们继续我们的源码分析***
经过上面对环形缓冲区的认识,我们看看除了对环形缓冲区的初始化还做了哪些操作
public void init(MapOutputCollector.Context context
) throws IOException, ClassNotFoundException {
......
// 排序的实现类,通过反射获取,如果用户设置了取用户的,如果用户没有设置框架默认使用快速排序
sorter = ReflectionUtils.newInstance(job.getClass("map.sort.class",
QuickSort.class, IndexedSorter.class), job);
// buffers and accounting
//将缓冲区的内存单位转换为字节
int maxMemUsage = sortmb << 20;
// METASIZE是我们元数据的长度,元数据存储的数据由4个部分组成,分别为
// VALSTART、KEYSTART、PARTITION、VALLEN,每个占到4个字节,所以METASIZE长度为16b
// 下面则是计算buffer中最多有多少byte来存元数据
maxMemUsage -= maxMemUsage % METASIZE;
//创建kv环形缓冲区
kvbuffer = new byte[maxMemUsage];
//初始bufvoid = kvbuffer的长度
bufvoid = kvbuffer.length;
//将kvbuffer转化为int型的kvmeta 以int为单位,也就是4byte
kvmeta = ByteBuffer.wrap(kvbuffer)
.order(ByteOrder.nativeOrder())
.asIntBuffer();
//设置初始buf和kvmeta的分界线
setEquator(0);
//给kvindex进行赋值
bufstart = bufend = bufindex = equator;
kvstart = kvend = kvindex;
//kvmeta中存放元数据实体的最大个数
maxRec = kvmeta.capacity() / NMETA;
//buffer spill时的阈值 (kvbuffer.length*spiller)
softLimit = (int)(kvbuffer.length * spillper);
//bufferRemaining这个变量作为衡量是否到达阈值
bufferRemaining = softLimit;
LOG.info(JobContext.IO_SORT_MB + ": " + sortmb);
LOG.info("soft limit at " + softLimit);
LOG.info("bufstart = " + bufstart + "; bufvoid = " + bufvoid);
LOG.info("kvstart = " + kvstart + "; length = " + maxRec);
// k/v serialization
//kv序列化的工作
//获取字典比较器,我们展开看一下源码
comparator = job.getOutputKeyComparator();
//获取map输出端的key类型,用户设置取用户的,用户没有设置默认是Object类型
keyClass = (Class<K>)job.getMapOutputKeyClass();
//获取我们map输出端的value类型,用户设置取用户的,用户没有设置默认是Object类型
valClass = (Class<V>)job.getMapOutputValueClass();
//为序列化工厂赋值
serializationFactory = new SerializationFactory(job);
//获取key的序列化
keySerializer = serializationFactory.getSerializer(keyClass);
//bb跟踪源码得知是一个new的普通buffer
//将bb作为key序列化写入的output
keySerializer.open(bb);
//获取value的序列化
valSerializer = serializationFactory.getSerializer(valClass);
//bb跟踪源码得知是一个new的普通buffer
//将bb作为value序列化写入的output
valSerializer.open(bb);
// output counters
//获取map输出计数器
mapOutputByteCounter = reporter.getCounter(TaskCounter.MAP_OUTPUT_BYTES);
mapOutputRecordCounter =
reporter.getCounter(TaskCounter.MAP_OUTPUT_RECORDS);
fileOutputByteCounter = reporter
.getCounter(TaskCounter.MAP_OUTPUT_MATERIALIZED_BYTES);
// compression
//判断用户是否设置了压缩格式
// 获取默认压缩格式,默认是基于Zlib算法的压缩器 DefaultCodec
// 一旦启用了压缩机制,Hadoop会为每条记录的key和value值进行压缩
if (job.getCompressMapOutput()) {
Class<? extends CompressionCodec> codecClass =
job.getMapOutputCompressorClass(DefaultCodec.class);
codec = ReflectionUtils.newInstance(codecClass, job);
} else {
//没有设置则没有压缩
codec = null;
}
// combiner
//当所有数据处理完成后,Map Task对所有临时文件进行一次合并,以确保最终只会生成一个数据文件
final Counters.Counter combineInputCounter =
reporter.getCounter(TaskCounter.COMBINE_INPUT_RECORDS);
combinerRunner = CombinerRunner.create(job, getTaskID(),
combineInputCounter,
reporter, null);
//判断是否开启combiner
if (combinerRunner != null) {
final Counters.Counter combineOutputCounter =
reporter.getCounter(TaskCounter.COMBINE_OUTPUT_RECORDS);
combineCollector= new CombineOutputCollector<K,V>(combineOutputCounter, reporter, job);
} else {
combineCollector = null;
}
spillInProgress = false;
minSpillsForCombine = job.getInt(JobContext.MAP_COMBINE_MIN_SPILLS, 3);
spillThread.setDaemon(true);
//设置线程名字
spillThread.setName("SpillThread");
spillLock.lock();
//将数据写入缓冲区
try {
//启动溢写线程
spillThread.start();
//进行轮询的方式询问
while (!spillThreadRunning) {
//等待spillThread线程结束
spillDone.await();
}
} catch (InterruptedException e) {
throw new IOException("Spill thread failed to initialize", e);
} finally {
spillLock.unlock();
}
if (sortSpillException != null) {
throw new IOException("Spill thread failed to initialize",
sortSpillException);
}
}
我们进入到比较器中,查看一下源码
public RawComparator getOutputKeyComparator() {
Class<? extends RawComparator> theClass = getClass(
JobContext.KEY_COMPARATOR, null, RawComparator.class);
if (theClass != null)
// 如果用户配置了比较器,则直接反射用户配置的生成对象
// 这里我们可以自定义比较器,比较方便
return ReflectionUtils.newInstance(theClass, this);
return
//用户没有配置则返回key自身的比较器
WritableComparator.get(getMapOutputKeyClass().asSubclass(WritableComparable.class), this);
}
接下来我们继续往下看,会看到一个combiner ,它的功能和reduce差不多,算是一个map阶段的分量reduce。
public void init(MapOutputCollector.Context context
) throws IOException, ClassNotFoundException {
......
// combiner
//当所有数据处理完成后,Map Task对所有临时文件进行一次合并,以确保最终只会生成一个数据文件
final Counters.Counter combineInputCounter =
reporter.getCounter(TaskCounter.COMBINE_INPUT_RECORDS);
combinerRunner = CombinerRunner.create(job, getTaskID(),
combineInputCounter,
reporter, null);
//判断是否开启combiner
if (combinerRunner != null) {
final Counters.Counter combineOutputCounter =
reporter.getCounter(TaskCounter.COMBINE_OUTPUT_RECORDS);
combineCollector= new CombineOutputCollector<K,V>(combineOutputCounter, reporter, job);
} else {
combineCollector = null;
}
spillInProgress = false;
//最小的溢写数触发combiner,默认是3
minSpillsForCombine = job.getInt(JobContext.MAP_COMBINE_MIN_SPILLS, 3);
spillThread.setDaemon(true);
//设置线程名字
spillThread.setName("SpillThread");
spillLock.lock();
//将数据写入缓冲区
try {
//启动溢写线程
spillThread.start();
//进行轮询的方式询问
while (!spillThreadRunning) {
//等待spillThread线程结束
spillDone.await();
}
} catch (InterruptedException e) {
throw new IOException("Spill thread failed to initialize", e);
} finally {
spillLock.unlock();
}
if (sortSpillException != null) {
throw new IOException("Spill thread failed to initialize",
sortSpillException);
}
}
接下来,我们总结一下,在map的输出初始化环节都做了哪些事情
然后我们继续看,找到MapOutPutBuffer的collect方法,在前文的分析中,我们得知,map输入的时候真正干活的是行读取器,而输出则是这个MapOutPutBuffer的collect方法,这个方法是我们在前面已经分析的很透彻了,具体干了什么我们也都清楚了。下面我们回到前面看一下,map输出完之后溢写的小文件怎么归并成一个文件
private <INKEY,INVALUE,OUTKEY,OUTVALUE>
void runNewMapper(final JobConf job,
final TaskSplitIndex splitIndex,
final TaskUmbilicalProtocol umbilical,
TaskReporter reporter
) throws IOException, ClassNotFoundException,
InterruptedException {
......
try {
//只要map的输出结束后,输入就会关闭
input.initialize(split, mapperContext);
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 void close(TaskAttemptContext context
) throws IOException,InterruptedException {
try {
// 如果溢写了一个文件,内存中还有数据,就调用了这个容器的flush方法,我们进去看一看,很熟悉
collector.flush();
} catch (ClassNotFoundException cnf) {
throw new IOException("can't find class ", cnf);
}
collector.close();
}
接下来,我们进到这个flush方法里面,我们发现,最终是在MapOutPutBuffer这个方法里面
public void flush() throws IOException, ClassNotFoundException,
InterruptedException {
LOG.info("Starting flush of map output");
if (kvbuffer == null) {
LOG.info("kvbuffer is null. Skipping flush.");
return;
}
spillLock.lock();
try {
while (spillInProgress) {
reporter.progress();
spillDone.await();
}
checkSpillException();
final int kvbend = 4 * kvend;
if ((kvbend + METASIZE) % kvbuffer.length !=
equator - (equator % METASIZE)) {
// spill finished
resetSpill();
}
if (kvindex != kvend) {
kvend = (kvindex + NMETA) % kvmeta.capacity();
bufend = bufmark;
LOG.info("Spilling map output");
LOG.info("bufstart = " + bufstart + "; bufend = " + bufmark +
"; bufvoid = " + bufvoid);
LOG.info("kvstart = " + kvstart + "(" + (kvstart * 4) +
"); kvend = " + kvend + "(" + (kvend * 4) +
"); length = " + (distanceTo(kvend, kvstart,
kvmeta.capacity()) + 1) + "/" + maxRec);
//排序并且溢写,这个方法我们在上面已经详细分析过了。
sortAndSpill();
}
} catch (InterruptedException e) {
throw new IOException("Interrupted while waiting for the writer", e);
} finally {
spillLock.unlock();
}
assert !spillLock.isHeldByCurrentThread();
// shut down spill thread and wait for it to exit. Since the preceding
// ensures that it is finished with its work (and sortAndSpill did not
// throw), we elect to use an interrupt instead of setting a flag.
// Spilling simultaneously from this thread while the spill thread
// finishes its work might be both a useful way to extend this and also
// sufficient motivation for the latter approach.
try {
spillThread.interrupt();
spillThread.join();
} catch (InterruptedException e) {
throw new IOException("Spill failed", e);
}
// release sort buffer before the merge
kvbuffer = null;
// 最后在output关闭的时候,调用合并,把小文件合并
mergeParts();
Path outputPath = mapOutputFile.getOutputFile();
fileOutputByteCounter.increment(rfs.getFileStatus(outputPath).getLen());
}
我们进到这个 mergeParts方法里面,简单看一下是怎么合并的
private void mergeParts() throws IOException, InterruptedException,
ClassNotFoundException {
......
// 如果我们没有配置combiner,或者我们的溢写文件小于最小触发溢写Combine的话,默认是3
if (combinerRunner == null || numSpills < minSpillsForCombine) {
//直接写文件
Merger.writeFile(kvIter, writer, reporter, job);
} else {
// 配置了combiner,并且我们的溢写文件大于 3, 则调用combine做一次合并的操作
combineCollector.setWriter(writer);
combinerRunner.combine(kvIter, combineCollector);
}
//到这里,我们的输出也就完了,关流
//close
writer.close();
到达这里,我们的Map Task输出也就分析完成了,我相信看到这里的,对map task的源码已经有了大致的了解,并且对环形缓冲区的理解很到位,对环形缓冲区的源码也有不浅的理解。至于优化,这里面有很多可以优化的点,这里就不多说。