MapRedeuce源码分析
shuffle机制
将Map写出的数据通过collect收集到环形缓冲区
环形缓冲区默认100M,右边写的KV信息,左边写元数据
写至80%,或完成任务后将溢写文件。
溢写之前将缓冲区中的数据分区,并将缓冲区内的数据使用快排排序。‘
如果使用combiner会将同一个MapTask生成的所有文件的根据分区使用归并排序进行合并。
之后将文件压缩,写入磁盘
reduceTask根据所需的分区主动copy磁盘数据,写入内存,内存不足的话写入磁盘
对每个map来的数据归并排序并根据Key分组
交给Reduce。
MapTask源码分析
context.write(outK,outV);
========================
public void write(KEYOUT key, VALUEOUT value) throws IOException,
InterruptedException {
mapContext.write(key, value);
}
========================
output.write(key, value);
===========================
public void write(K key, V value) throws IOException, InterruptedException {
//收集器,需要第二次进入
collector.collect(key, value,
partitioner.getPartition(key, value, partitions));//拿到分区器用来以后设置分区
}
==============
HashPartitioner(); //默认分区器
key和value的序列化
int keystart = bufindex;
//序列化key
keySerializer.serialize(key);
if (bufindex < keystart) {
// wrapped the key; must make contiguous
bb.shiftBufferedKey();
keystart = 0;
}
// serialize value bytes into buffer
final int valstart = bufindex;
//序列化value
valSerializer.serialize(value);
//为了在不同机器上传输
记录元数据
//写元数据
kvmeta.put(kvindex + PARTITION, partition);
kvmeta.put(kvindex + KEYSTART, keystart);
kvmeta.put(kvindex + VALSTART, valstart);
kvmeta.put(kvindex + VALLEN, distanceTo(valstart, valend));
// advance kvindex
kvindex = (kvindex - NMETA + kvmeta.capacity()) % kvmeta.capacity();
collect收集所有KV后,或者到达缓冲的80%
溢写文件
//所有KV写完之后会执行
public void close(TaskAttemptContext context
) throws IOException,InterruptedException {
try {
//溢写方法(提前打断点)
collector.flush();
} catch (ClassNotFoundException cnf) {
throw new IOException("can't find class ", cnf);
}
溢写的排序
private void sortAndSpill() throws IOException, ClassNotFoundException,
InterruptedException {
//省略若干代码
//排序算法。为快排算法。在内存中
sorter.sort(MapOutputBuffer.this, mstart, mend, reporter);
int spindex = mstart;
final IndexRecord rec = new IndexRecord();
final InMemValBytes value = new InMemValBytes();
for (int i = 0; i < partitions; ++i) {//根据分区写文件
IFile.Writer<K, V> writer = null;
try {
long segmentStart = out.getPos();
mergeParts();//合并文件
合并文件
private void mergeParts() throws IOException, InterruptedException,
ClassNotFoundException {
long finalOutFileSize = 0;
long finalIndexFileSize = 0;
final Path[] filename = new Path[numSpills];
final TaskAttemptID mapId = getTaskID();
for(int i = 0; i < numSpills; i++) {
filename[i] = mapOutputFile.getSpillFile(i);
finalOutFileSize += rfs.getFileStatus(filename[i]).getLen();
}
if (numSpills == 1) { //the spill is the final output
sameVolRename(filename[0],
mapOutputFile.getOutputFileForWriteInVolume(filename[0]));
if (indexCacheList.size() == 0) {
sameVolRename(mapOutputFile.getSpillIndexFile(0),
mapOutputFile.getOutputIndexFileForWriteInVolume(filename[0]));
} else {
indexCacheList.get(0).writeToFile(
mapOutputFile.getOutputIndexFileForWriteInVolume(filename[0]), job);
}
sortPhase.complete();//
return;
}
收集器的关闭
try {
collector.flush();
} catch (ClassNotFoundException cnf) {
throw new IOException("can't find class ", cnf);
}
collector.close();
}
(1)Read阶段:MapTask通过InputFormat获得的RecordReader,从输入InputSplit中解析出一个个key/value。
(2)Map阶段:该节点主要是将解析出的key/value交给用户编写map()函数处理,并产生一系列新的key/value。
(3)Collect收集阶段:在用户编写map()函数中,当数据处理完成后,一般会调用OutputCollector.collect()输出结果。在该函数内部,它会将生成的key/value分区(调用Partitioner),并写入一个环形内存缓冲区中。
(4)Spill阶段:即“溢写”,当环形缓冲区满后,MapReduce会将数据写到本地磁盘上,生成一个临时文件。需要注意的是,将数据写入本地磁盘之前,先要对数据进行一次本地排序,并在必要时对数据进行合并、压缩等操作。
溢写阶段详情:
步骤1:利用快速排序算法对缓存区内的数据进行排序,排序方式是,先按照分区编号Partition进行排序,然后按照key进行排序。这样,经过排序后,数据以分区为单位聚集在一起,且同一分区内所有数据按照key有序。
步骤2:按照分区编号由小到大依次将每个分区中的数据写入任务工作目录下的临时文件output/spillN.out(N表示当前溢写次数)中。如果用户设置了Combiner,则写入文件之前,对每个分区中的数据进行一次聚集操作。
步骤3:将分区数据的元信息写到内存索引数据结构SpillRecord中,其中每个分区的元信息包括在临时文件中的偏移量、压缩前数据大小和压缩后数据大小。如果当前内存索引大小超过1MB,则将内存索引写到文件output/spillN.out.index中。
(5)Merge阶段:当所有数据处理完成后,MapTask对所有临时文件进行一次合并,以确保最终只会生成一个数据文件。
当所有数据处理完后,MapTask会将所有临时文件合并成一个大文件,并保存到文件output/file.out中,同时生成相应的索引文件output/file.out.index。
在进行文件合并过程中,MapTask以分区为单位进行合并。对于某个分区,它将采用多轮递归合并的方式。每轮合并mapreduce.task.io.sort.factor(默认10)个文件,并将产生的文件重新加入待合并列表中,对文件排序后,重复以上过程,直到最终得到一个大文件。
让每个MapTask最终只生成一个数据文件,可避免同时打开大量文件和同时读取大量小文件产生的随机读取带来的开销。
ReduceTask源码分析
//ReduceTask的run方法
public void run(JobConf job, final TaskUmbilicalProtocol umbilical)
throws IOException, InterruptedException, ClassNotFoundException {
job.setBoolean(JobContext.SKIP_RECORDS, isSkipping());
if (isMapOrReduce()) {//提前打断点
//reduce的三个阶段
copyPhase = getProgress().addPhase("copy");
sortPhase = getProgress().addPhase("sort");
reducePhase = getProgress().addPhase("reduce");
}
//初始化信息
itialize(job, getJobID(), reporter, useNewApi);
shuffleConsumerPlugin.init(shuffleContext);
=================
//需要提前打断点
scheduler = new ShuffleSchedulerImpl<K, V>(jobConf, taskStatus, reduceId,
===============================
public ShuffleSchedulerImpl(JobConf job, TaskStatus status,
TaskAttemptID reduceId,
ExceptionReporter reporter,
Progress progress,
Counters.Counter shuffledMapsCounter,
Counters.Counter reduceShuffleBytes,
Counters.Counter failedShuffleCounter) {
totalMaps = job.getNumMapTasks();//确定有多少MapTasks。去其中拷贝文件
准备磁盘和内存
//--->shuffle
public void init(ShuffleConsumerPlugin.Context context) {
merger = createMergeManager(context);//将拷贝的文件合并
========================
protected MergeManager<K, V> createMergeManager(
ShuffleConsumerPlugin.Context context) {
//提前断点进去其中
return new MergeManagerImpl<K, V>(reduceId, jobConf, context.getLocalFS(),
================================
//内存合并器
this.inMemoryMerger = createInMemoryMerger();
this.inMemoryMerger.start();
//磁盘合并器
this.onDiskMerger = new OnDiskMerger(this);
//意味着ReduceTask拷贝数据先存内存。内存不足再溢写到文件
开始拷贝
public RawKeyValueIterator run() throws IOException, InterruptedException {
eventFetcher.start();//开始抓取数据
============================
eventFetcher.shutDown();//抓取完成。Shuffle第107行,提前打断点
====================
copyPhase.complete();//拷贝阶段结束
==============================
taskStatus.setPhase(TaskStatus.Phase.SORT); //开始排序阶段,Shuffle第152行
=======================
taskStatus.setPhase(TaskStatus.Phase.SORT);//归并
his.onDiskMerger = new OnDiskMerger(this);
//意味着ReduceTask拷贝数据先存内存。内存不足再溢写到文件
开始拷贝
public RawKeyValueIterator run() throws IOException, InterruptedException {
eventFetcher.start();//开始抓取数据
============================
eventFetcher.shutDown();//抓取完成。Shuffle第107行,提前打断点
====================
copyPhase.complete();//拷贝阶段结束
==============================
taskStatus.setPhase(TaskStatus.Phase.SORT); //开始排序阶段,Shuffle第152行
=======================
taskStatus.setPhase(TaskStatus.Phase.SORT);//归并
(1)Copy阶段:ReduceTask从各个MapTask上远程拷贝一片数据,并针对某一片数据,如果其大小超过一定阈值,则写到磁盘上,否则直接放到内存中。
(2)Merge阶段:在远程拷贝数据的同时,ReduceTask启动了两个后台线程对内存和磁盘上的文件进行合并,以防止内存使用过多或磁盘上文件过多。
(3)Sort阶段:按照MapReduce语义,用户编写reduce()函数输入数据是按key进行聚集的一组数据。为了将key相同的数据聚在一起,Hadoop采用了基于排序的策略。由于各个MapTask已经实现对自己的处理结果进行了局部排序,因此,ReduceTask只需对所有数据进行一次归并排序即可。
(4)Reduce阶段:reduce()函数将计算结果写到HDFS上。