MapReduce源码解析
ReduceTask
reduce阶段整体概述
先去所有map任务所在的主机拉取指定分区(当前reduce)的数据(map输出),然后对拉取的所有数据做合并,合并时按照key进行排序相同的key会排在一起(这里的排序,是将map阶段排好序的数据做一次归并排序),接着将相同key的一组数据调用自定义的reduce方法进行处理,并将处理结果写到MR输出目录(hdfs中)
map和reduce阶段使用的比较器需要特别注意
map阶段:取用户定义的排序比较器;没有的话,取key自身的排序比较器
reduce阶段:取用户自定义的分组比较器;没有的话,取用户定义的排序比较器;没有的话,取key自身的排序比较器
组合方式:
- 不设置排序和分组比较器:map取key自身的排序比较器,reduce取key自身的排序比较器
- 设置了排序比较器:map取用户定义的排序比较器,reduce取用户定义的排序比较器
- 设置了分组比较器:map取key自身的排序比较器,reduce取用户自定义的分组比较器
- 设置了排序和分组比较器:map取用户定义的排序比较器,reduce取用户自定义的分组比较器
@InterfaceAudience.Private
@InterfaceStability.Unstable
public class ReduceTask extends Task {
// ...
public void run(JobConf job, final TaskUmbilicalProtocol umbilical)
throws IOException, InterruptedException, ClassNotFoundException {
job.setBoolean(JobContext.SKIP_RECORDS, isSkipping());
// reduce分三个阶段:copy、sort、reduce,copy和sort又统称为shuffle
if (isMapOrReduce()) {
copyPhase = getProgress().addPhase("copy");
sortPhase = getProgress().addPhase("sort");
reducePhase = getProgress().addPhase("reduce");
}
// 通信线程处理与父进程(任务跟踪器)的通信
TaskReporter reporter = startReporter(umbilical);
boolean useNewApi = job.getUseNewReducer();
initialize(job, getJobID(), reporter, useNewApi); // 调用父类方法,与MapTask中一样
// check if it is a cleanupJobTask
// ...
// Initialize the codec
codec = initCodec();
RawKeyValueIterator rIter = null; // 迭代器,reduce阶段使用这个迭代器获取数据
ShuffleConsumerPlugin shuffleConsumerPlugin = null; // 用于copy和sort即shuffle阶段
// combine
Class combinerClass = conf.getCombinerClass();
CombineOutputCollector combineCollector =
(null != combinerClass) ?
new CombineOutputCollector(reduceCombineOutputCounter, reporter, conf) : null;
// shuffleConsumerPlugin可以自定义,默认为Shuffle.class,用于执行shuffle阶段
Class<? extends ShuffleConsumerPlugin> clazz =
job.getClass(MRConfig.SHUFFLE_CONSUMER_PLUGIN, Shuffle.class, ShuffleConsumerPlugin.class);
shuffleConsumerPlugin = ReflectionUtils.newInstance(clazz, job);
// ...
// 执行shuffle(拉取数据,会阻塞直到shuffle完成),返回迭代器;迭代器是处理大数据时非常常用的设计模式,因为数据量很大不可能一次性加载到内存,迭代器可以在指定内存中处理超大数量的数据集
rIter = shuffleConsumerPlugin.run();
// ...
// 获取分组比较器,先判断是否使用了自定义的,如果没有自定义的,再判断是否定义了KEY_COMPARATOR key排序比较器,如果没有定义key排序比较器,则最终使用map输出key对应的WritableComparable
RawComparator comparator = job.getOutputValueGroupingComparator();
if (useNewApi) { // 默认都使用新的api
runNewReducer(job, umbilical, reporter, rIter, comparator,
keyClass, valueClass);
} else {
runOldReducer(job, umbilical, reporter, rIter, comparator,
keyClass, valueClass);
}
shuffleConsumerPlugin.close();
// 更新一些统计信息,并提交MR任务。会调用Task.commit->FileOutputCommitter.commitTask->FileOutputCommitter.mergePaths,将单个reduce任务的临时输出合并成最终的输出
done(umbilical, reporter);
}
private <INKEY,INVALUE,OUTKEY,OUTVALUE>
void runNewReducer(JobConf job,
final TaskUmbilicalProtocol umbilical,
final TaskReporter reporter,
RawKeyValueIterator rIter,
RawComparator<INKEY> comparator,
Class<INKEY> keyClass,
Class<INVALUE> valueClass
) throws IOException,InterruptedException,
ClassNotFoundException {
// 包装value迭代器以报告进度
final RawKeyValueIterator rawIter = rIter;
rIter = new RawKeyValueIterator() {
// ...
public boolean next() throws IOException {
boolean ret = rawIter.next();
reporter.setProgress(rawIter.getProgress().getProgress());
return ret;
}
};
// 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);
// 创建自定义的reducer,例如测试中使用的是MyReducer
org.apache.hadoop.mapreduce.Reducer<INKEY,INVALUE,OUTKEY,OUTVALUE> reducer =
(org.apache.hadoop.mapreduce.Reducer<INKEY,INVALUE,OUTKEY,OUTVALUE>)
ReflectionUtils.newInstance(taskContext.getReducerClass(), job);
// 在自定义reduce中context.write(key, result)最终会调用NewTrackingRecordWriter.write方法将reduce的输出写入到hdfs中的临时目录(在输出目录中创建的临时目录)
org.apache.hadoop.mapreduce.RecordWriter<OUTKEY,OUTVALUE> trackedRW =
new NewTrackingRecordWriter<OUTKEY, OUTVALUE>(this, taskContext);
// ...
try {
reducer.run(reducerContext);
} finally {
trackedRW.close(reducerContext);
}
}
}
public class Reducer<KEYIN,VALUEIN,KEYOUT,VALUEOUT> {
// ...
public void run(Context context) throws IOException, InterruptedException {
setup(context);
try {
// 这里重点是context.nextKey()、context.getCurrentKey()、context.getValues()
// context是WrappedReducer.Context,WrappedReducer是一个对给定的Reducer进行包装以允许自定义Reducer.Context实现的Reducer。默认情况下,对Context中方法的调用都委托给了reduceContext(ReduceContextImpl)
while (context.nextKey()) { // 是否还有下一个唯一key,如果有的话,调用自定义的reduce方法,参数为当前key和属于当前key的一组value(迭代器)
// 这会调用自定义的reduce方法,例如测试中使用的是MyReducer.reduce方法
reduce(context.getCurrentKey(), context.getValues(), context);
// If a back up store is used, reset it
Iterator<VALUEIN> iter = context.getValues().iterator();
if(iter instanceof ReduceContext.ValueIterator) {
((ReduceContext.ValueIterator<VALUEIN>)iter).resetBackupStore();
}
}
} finally {
cleanup(context);
}
}
}
public class MyReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
private final IntWritable result = new IntWritable();
// Reducer.run每次调用这个自定义reduce方法时,传入的key都是同一个对象,values迭代器中的所有value也都是同一个对象,只是内容不一样(参考ReduceContextImpl.nextKeyValue方法);Reducer.run方法中调用ReduceContextImpl.nextKey这个方法以及这里迭代values,实际都是在调用ReduceContextImpl.nextKeyValue方法给key/value重新设置值,复用而不是新创建key/value。reduce方法的输出也是复用相同的对象key和result,reduce方法调用的次数是很多的,这样可以避免创建大量的对象,减少GC
@Override
public void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
int sum = 0;
for (IntWritable val : values) {
sum += val.get();
}
result.set(sum);
context.write(key, result);
}
}
Shuffle
@InterfaceAudience.LimitedPrivate({"MapReduce"})
@InterfaceStability.Unstable
@SuppressWarnings({"unchecked", "rawtypes"})
public class Shuffle<K, V> implements ShuffleConsumerPlugin<K, V>,
ExceptionReporter {
// ...
// 该类用于收集和记录Shuffle阶段的客户端相关指标和性能数据。它是Shuffle阶段的客户端指标监控类,用于记录Shuffle客户端的各种统计信息,以便管理员或开发人员了解Shuffle过程的性能和运行状况
private ShuffleClientMetrics metrics;
// ...
// 该类负责管理shuffle阶段的默认实现类,它负责分区、排序、资源管理、任务调度、性能优化和错误处理等功能,保证shuffle阶段的顺利执行
private ShuffleSchedulerImpl<K, V> scheduler;
// 该类主要负责管理在Shuffle阶段产生的中间数据片段的合并
private MergeManager<K, V> merger;
// ...
// 如果这是一个基于LocalJobRunner的作业,这将是从map任务尝试到输出文件的映射;在其他情况下,这个值将为null;这个字段有值表示是本地模式运行的MR
private Map<TaskAttemptID, MapOutputFile> localMapFiles;
// ...
@Override
public void init(ShuffleConsumerPlugin.Context context) {
// ...
scheduler = new ShuffleSchedulerImpl<K, V>(jobConf, taskStatus, reduceId,
this, copyPhase, context.getShuffledMapsCounter(),
context.getReduceShuffleBytes(), context.getFailedShuffleCounter());
merger = createMergeManager(context); // 创建MergeManagerImpl
}
// ...
@Override
public RawKeyValueIterator run() throws IOException, InterruptedException {
// 缩放我们每次RPC调用获取的最大事件数,以缓解ApplicationMaster在大量reducer获取事件时的OOM问题
// TODO: This should not be necessary after HADOOP-8942
int eventsPerReducer = Math.max(MIN_EVENTS_TO_FETCH,
MAX_RPC_OUTSTANDING_EVENTS / jobConf.getNumReduceTasks());
int maxEventsToFetch = Math.min(MAX_EVENTS_TO_FETCH, eventsPerReducer);
// 创建EventFetcher线程,用于获取已完成的map列表
final EventFetcher<K, V> eventFetcher =
new EventFetcher<K, V>(reduceId, umbilical, scheduler, this,
maxEventsToFetch);
eventFetcher.start();
// 开启 map-output 拉取线程
boolean isLocal = localMapFiles != null; // 是否是本地模式运行的MR
// 如果是本地模式运行,启动1个Fetcher线程拉取数据;如果不是本地模式运行,则默认启动5个Fetcher线程拉取数据
final int numFetchers = isLocal ? 1 :
jobConf.getInt(MRJobConfig.SHUFFLE_PARALLEL_COPIES, 5);
// Fetcher线程会执行拉取数据,数据拉取到本地后会交给MergeManager执行内存或磁盘中数据的合并
Fetcher<K, V>[] fetchers = new Fetcher[numFetchers];
if (isLocal) {
fetchers[0] = new LocalFetcher<K, V>(jobConf, reduceId, scheduler,
merger, reporter, metrics, this, reduceTask.getShuffleSecret(),
localMapFiles);
fetchers[0].start();
} else {
for (int i=0; i < numFetchers; ++i) {
fetchers[i] = new Fetcher<K, V>(jobConf, reduceId, scheduler, merger,
reporter, metrics, this,
reduceTask.getShuffleSecret());
fetchers[i].start();
}
}
// 等待shuffle成功完成
while (!scheduler.waitUntilDone(PROGRESS_FREQUENCY)) {
reporter.progress();
synchronized (this) {
if (throwable != null) {
throw new ShuffleError("error in shuffle in " + throwingThreadName,
throwable);
}
}
}
// ...
// 完成正在进行的合并
RawKeyValueIterator kvIter = null;
try {
// 拉取数据结束之后,调用MergeManagerImpl.close释放资源,并做最终的合并(不是真的合并),然后返回最终合并的迭代器
kvIter = merger.close();
} catch (Throwable e) {
throw new ShuffleError("Error while doing final merge ", e);
}
// ...
return kvIter;
}
// ...
}
ShuffleSchedulerImpl
// shuffle调度器,负责管理shuffle是否完成,解析EventFetcher事件更新待处理的MapHost状态,为Fetcher提供MapHost以拉取数据,成功拉取数据到本地之后将数据添加到MergeManagerImpl待合并列表中,更新shuffle状态等
@InterfaceAudience.Private
@InterfaceStability.Unstable
public class ShuffleSchedulerImpl<K,V> implements ShuffleScheduler<K,V> {
// ...
// 从事件流中解释一个TaskCompletionEvent
// EventFetcher获取到已完成的map,会生成一个事件并调用这个方法,最终会唤醒Fetcher线程去拉取已经完成的map输出
@Override
public void resolve(TaskCompletionEvent event) {
switch (event.getTaskStatus()) {
case SUCCEEDED: // 任务执行成功
// 创建拉取数据时的URI,指定了reduceId(分区号),这样拉取数据时只会拉取指定分区的数据
URI u = getBaseURI(reduceId, event.getTaskTrackerHttp());
// 构建MapHost添加到mapLocations中,可能会更新该MapHost状态为PENDING
addKnownMapOutput(u.getHost() + ":" + u.getPort(),
u.toString(),
event.getTaskAttemptId());
maxMapRuntime = Math.max(maxMapRuntime, event.getTaskRunTime());
break;
case FAILED:
case KILLED:
case OBSOLETE:
// 任务执行失败,仍然允许重试
obsoleteMapOutput(event.getTaskAttemptId());
LOG.info("Ignoring obsolete output of " + event.getTaskStatus() +
" map-task: '" + event.getTaskAttemptId() + "'");
break;
case TIPFAILED: // 任务执行失败达到最大允许次数,不在尝试执行
tipFailed(event.getTaskAttemptId().getTaskID()); // 记录错误的任务,并更新任务状态
LOG.info("Ignoring output of failed map TIP: '" +
event.getTaskAttemptId() + "'");
break;
}
}
// ...
// 拉取数据并写入到本地(内存或磁盘)成功之后,调用这个方法,将拉取到本地的数据(内存文件或磁盘文件)添加到待合并列表中(同时会判断是否需要启动相关的合并),然后更新一些状态和统计
public synchronized void copySucceeded(TaskAttemptID mapId,
MapHost host,
long bytes,
long startMillis,
long endMillis,
MapOutput<K,V> output
) throws IOException {
// ...
if (!finishedMaps[mapIndex]) {
// InMemoryMapOutput.commit会调用MergeManagerImpl.closeInMemoryFile,将output添加到待合并的内存文件列表中,并判断是否需要进行内存到内存的合并和内存到磁盘的合并
// OnDiskMapOutput.commit会调用MergeManagerImpl.closeOnDiskFile,将output添加到待合并的磁盘文件列表中,并判断是否需要进行磁盘到磁盘的合并
output.commit();
finishedMaps[mapIndex] = true; // 标记对该map的拉取完成
shuffledMapsCounter.increment(1);
if (--remainingMaps == 0) { // 没有待拉取的map时,唤醒所有等待的方法,主要是waitUntilDone
notifyAll();
}
// ...
} else {
LOG.warn("Aborting already-finished MapOutput for " + mapId);
output.abort();
}
}
// ...
// 记录错误的任务,并更新任务状态
public synchronized void tipFailed(TaskID taskId) {
if (!finishedMaps[taskId.getId()]) {
finishedMaps[taskId.getId()] = true;
if (--remainingMaps == 0) { // 没有待拉取的map,则唤醒所有等待的线程
notifyAll();
}
updateStatus(); // 更新任务状态
}
}
// 构建MapHost添加到mapLocations中,可能会更新该MapHost状态为PENDING
public synchronized void addKnownMapOutput(String hostName,
String hostUrl,
TaskAttemptID mapId) {
MapHost host = mapLocations.get(hostName); // 一个host上可以有多个map
if (host == null) {
host = new MapHost(hostName, hostUrl);
mapLocations.put(hostName, host);
}
// 这里会更新host状态,将IDLE状态修改为PENDING(因为该host有新的已完成map加入)
host.addKnownMap(mapId); // MapHost封装了某个host中的多个map任务
// Mark the host as pending
// 将PENDING状态的host添加到pendingHosts,并唤醒Fetcher线程中对ShuffleSchedulerImpl.getHost方法的调用,继续处理PENDING状态的host
if (host.getState() == State.PENDING) {
pendingHosts.add(host);
notifyAll();
}
}
// ...
// Fetcher线程会不断调用该方法,获取PENDING状态的的MapHost
public synchronized MapHost getHost() throws InterruptedException {
while(pendingHosts.isEmpty()) {
wait();
}
Iterator<MapHost> iter = pendingHosts.iterator();
// 取一个是安全的,因为我们知道pendingHosts不是空的
MapHost host = iter.next();
// ...
return host;
}
}
@InterfaceAudience.LimitedPrivate({"MapReduce"})
@InterfaceStability.Unstable
public class MapHost {
public enum State {
IDLE, // 没有可用的map输出
BUSY, // 正在获取map输出
PENDING, // 需要获取已知的map输出
PENALIZED // host因shuffle失败而受到处罚
}
// ...
}
MergeManagerImpl
// 合并管理器,管理内存到内存、内存到磁盘、磁盘到磁盘数据的合并,返回最终的磁盘文件和内存文件组合成的迭代器给reduce方法使用
@SuppressWarnings(value={"unchecked"})
@InterfaceAudience.LimitedPrivate({"MapReduce"})
@InterfaceStability.Unstable
public class MergeManagerImpl<K, V> implements MergeManager<K, V> {
// ...
protected MapOutputFile mapOutputFile; // MROutputFiles,用于MR执行期间写入或读取临时的文件
// 存储内存到内存合并产生的InMemoryMapOutput
Set<InMemoryMapOutput<K, V>> inMemoryMergedMapOutputs =
new TreeSet<InMemoryMapOutput<K,V>>(new MapOutputComparator<K, V>());
// 内存到内存合并的线程,该线程默认不开启;如果配置开启的话,当达到内存合并阈值时,则先在内存中进行合并;当达到内存使用阈值时,则执行内存到磁盘的合并,逻辑见closeInMemoryFile方法
private IntermediateMemoryToMemoryMerger memToMemMerger;
// 内存到磁盘待合并的InMemoryMapOutput列表(即待合并的内存文件列表,会合并到磁盘)
Set<InMemoryMapOutput<K, V>> inMemoryMapOutputs =
new TreeSet<InMemoryMapOutput<K,V>>(new MapOutputComparator<K, V>());
// 内存到磁盘合并的线程,用于合并待合并的内存文件,每执行一次合并会产生一个中间文件(是待合并的磁盘文件)
private final MergeThread<InMemoryMapOutput<K,V>, K,V> inMemoryMerger;
// 磁盘到磁盘待合并的CompressAwarePath列表(即待合并的磁盘文件列表,会合并到磁盘)
Set<CompressAwarePath> onDiskMapOutputs = new TreeSet<CompressAwarePath>();
// 磁盘到磁盘合并的线程,用于合并待合并磁盘文件,每执行一次合并会产生一个中间文件(也是待合并磁盘文件)
private final OnDiskMerger onDiskMerger;
@VisibleForTesting
final long memoryLimit; // reduce阶段内存使用限制
private long usedMemory; // 已使用的内存
private long commitMemory; // 待合并的内存
// 单次shuffle内存阈值,单次拉取的map输出(指定的分区)大于该阈值时,会直接将拉取的数据写到磁盘而不是内存
@VisibleForTesting
final long maxSingleShuffleLimit;
// 内存到内存合并的阈值,待合并内存文件数量大于等于该值时,则执行内存到内存的合并
private final int memToMemMergeOutputsThreshold;
private final long mergeThreshold; // 内存合并阈值,待合并的内存大于等于该阈值时,会执行内存到磁盘的合并
// ...
// 在内存到内存合并期间运行的Combiner类(如果有定义)
private final Class<? extends Reducer> combinerClass;
// 用于combine的可复位collector
private final CombineOutputCollector<K,V> combineCollector;
// ...
public MergeManagerImpl(TaskAttemptID reduceId, JobConf jobConf,
FileSystem localFS,
LocalDirAllocator localDirAllocator,
Reporter reporter,
CompressionCodec codec,
Class<? extends Reducer> combinerClass,
CombineOutputCollector<K,V> combineCollector,
Counters.Counter spilledRecordsCounter,
Counters.Counter reduceCombineInputCounter,
Counters.Counter mergedMapOutputsCounter,
ExceptionReporter exceptionReporter,
Progress mergePhase, MapOutputFile mapOutputFile) {
// ...
boolean allowMemToMemMerge =
jobConf.getBoolean(MRJobConfig.REDUCE_MEMTOMEM_ENABLED, false); // 是否允许内存到内存的合并
if (allowMemToMemMerge) {
this.memToMemMerger =
new IntermediateMemoryToMemoryMerger(this,
memToMemMergeOutputsThreshold);
this.memToMemMerger.start(); // 启动内存到内存合并的线程
} else {
this.memToMemMerger = null;
}
this.inMemoryMerger = createInMemoryMerger();
this.inMemoryMerger.start(); // 启动内存到磁盘合并的线程
this.onDiskMerger = new OnDiskMerger(this);
this.onDiskMerger.start(); // 启动磁盘到磁盘合并的线程
this.mergePhase = mergePhase;
}
// ...
// 等待,直到merge有一些可用的空闲资源,以便它可以接受shuffle的数据。这将在建立网络连接以获得map输出之前调用
@Override
public void waitForResource() throws InterruptedException {
inMemoryMerger.waitForMerge();
}
/**
* 为待shuffle的数据预留资源。该方法将在建立网络连接后调用,以对数据进行shuffle。如果将被shuffle的数据的字节大小大于单次shuffle内存阈值则返回OnDiskMapOutput(表示shuffle到磁盘);如果超过内存限制,则返回null,暂停shuffle;否则返回InMemoryMapOutput(表示shuffle到内存)
* @param mapId 将从中shuffle数据的map
* @param requestedSize 将被shuffle的数据的字节大小。
* @param fetcher 将shuffle数据的map输出拉取器(fetcher)的id
* @return 返回一个MapOutput对象,shuffle可以使用该对象来shuffle数据。如果不能立即保留所需资源,则可以返回null(已使用内存超过阈值限制则返回null)
*/
@Override
public synchronized MapOutput<K,V> reserve(TaskAttemptID mapId,
long requestedSize,
int fetcher
) throws IOException {
// 将被shuffle的数据的字节大小大于单次shuffle内存阈值,则创建OnDiskMapOutput返回,表示shuffle数据到磁盘
if (requestedSize > maxSingleShuffleLimit) {
LOG.info(mapId + ": Shuffling to disk since " + requestedSize +
" is greater than maxSingleShuffleLimit (" +
maxSingleShuffleLimit + ")");
return new OnDiskMapOutput<K,V>(mapId, this, requestedSize, jobConf,
fetcher, true, FileSystem.getLocal(jobConf).getRaw(),
mapOutputFile.getInputFileForWrite(mapId.getTaskID(), requestedSize));
}
// 如果超过内存限制,则暂停shuffle
// 有可能所有的线程都处于停滞状态,根本没有任何进展。这种情况可能发生在:请求的大小导致已用内存超过限制 && 请求的大小 < singleShuffleLimit && 当前使用的内存大小 < mergeThreshold (合并不会被触发)
// 为了避免这种情况发生,我们只允许一个线程超过内存限制。我们检查(usedMemory > memoryLimit)而不是(usedMemory + requestedSize > memoryLimit)。当这个线程完成拉取时,这将自动触发合并,从而解锁所有阻塞的线程
if (usedMemory > memoryLimit) {
LOG.debug(mapId + ": Stalling shuffle since usedMemory (" + usedMemory
+ ") is greater than memoryLimit (" + memoryLimit + ")." +
" CommitMemory is (" + commitMemory + ")");
return null;
}
// 允许内存中进行shuffle
LOG.debug(mapId + ": Proceeding with shuffle since usedMemory ("
+ usedMemory + ") is lesser than memoryLimit (" + memoryLimit + ")."
+ "CommitMemory is (" + commitMemory + ")");
// 创建InMemoryMapOutput并返回,表示在内存中进行shuffle(即拉取数据放到内存中)
return unconditionalReserve(mapId, requestedSize, true);
}
// ...
// InMemoryMapOutput.commit方法会调用这个方法,将拉取到本地的内存文件添加到待合并的内存文件列表中,并判断是否需要进行内存到内存的合并和内存到磁盘的合并
public synchronized void closeInMemoryFile(InMemoryMapOutput<K,V> mapOutput) {
inMemoryMapOutputs.add(mapOutput); // 添加到待合并内存文件列表
LOG.info("closeInMemoryFile -> map-output of size: " + mapOutput.getSize()
+ ", inMemoryMapOutputs.size() -> " + inMemoryMapOutputs.size()
+ ", commitMemory -> " + commitMemory + ", usedMemory ->" + usedMemory);
commitMemory+= mapOutput.getSize(); // 更新待合并的内存
// Can hang if mergeThreshold is really low.
if (commitMemory >= mergeThreshold) { // 待合并的内存大于阈值,启动内存到磁盘的合并
LOG.info("Starting inMemoryMerger's merge since commitMemory=" +
commitMemory + " > mergeThreshold=" + mergeThreshold +
". Current usedMemory=" + usedMemory);
inMemoryMapOutputs.addAll(inMemoryMergedMapOutputs);
inMemoryMergedMapOutputs.clear();
inMemoryMerger.startMerge(inMemoryMapOutputs);
commitMemory = 0L; // Reset commitMemory.
}
// 如果启用了内存到内存的合并,且待合并数量大于阈值,则启动内存到内存的合并
if (memToMemMerger != null) {
if (inMemoryMapOutputs.size() >= memToMemMergeOutputsThreshold) {
memToMemMerger.startMerge(inMemoryMapOutputs);
}
}
}
// ...
// 将拉取到本地的磁盘文件添加到待合并的磁盘文件列表中,并判断是否需要进行磁盘到磁盘的合并
public synchronized void closeOnDiskFile(CompressAwarePath file) {
onDiskMapOutputs.add(file);
if (onDiskMapOutputs.size() >= (2 * ioSortFactor - 1)) {
// 待合并的磁盘文件达到阈值时,调用OnDiskMerger.startMerge会唤醒OnDiskMerger.merge这个方法执行磁盘文件的合并,磁盘文件包括中间产生的合并文件和直接拉取到磁盘的map输出文件;待合并磁盘文件会继续合并,最终会合并成一个文件
onDiskMerger.startMerge(onDiskMapOutputs);
}
}
// 拉取数据结束之后,会调用这个方法释放资源,并做最终的合并(不是真的合并),然后返回最终合并的迭代器
@Override
public RawKeyValueIterator close() throws Throwable {
// 等待正在进行的合并完成
if (memToMemMerger != null) {
memToMemMerger.close();
}
inMemoryMerger.close();
onDiskMerger.close();
// 收集还没有合并的内存文件memory(InMemoryMapOutput)和磁盘文件disk(CompressAwarePath),然后调用finalMerge完成最终的合并
List<InMemoryMapOutput<K, V>> memory =
new ArrayList<InMemoryMapOutput<K, V>>(inMemoryMergedMapOutputs);
inMemoryMergedMapOutputs.clear();
memory.addAll(inMemoryMapOutputs);
inMemoryMapOutputs.clear();
List<CompressAwarePath> disk = new ArrayList<CompressAwarePath>(onDiskMapOutputs);
onDiskMapOutputs.clear();
return finalMerge(jobConf, rfs, memory, disk); // 生成最终合并的迭代器
}
// 内存到内存合并的线程
private class IntermediateMemoryToMemoryMerger
extends MergeThread<InMemoryMapOutput<K, V>, K, V> {
// ...
@Override
public void merge(List<InMemoryMapOutput<K, V>> inputs) throws IOException {
if (inputs == null || inputs.size() == 0) {
return;
}
TaskAttemptID dummyMapId = inputs.get(0).getMapId();
List<Segment<K, V>> inMemorySegments = new ArrayList<Segment<K, V>>();
// 创建读内存的Segment添加到inMemorySegments中
long mergeOutputSize =
createInMemorySegments(inputs, inMemorySegments, 0);
int noInMemorySegments = inMemorySegments.size();
// 创建合并内存之后的InMemoryMapOutput
InMemoryMapOutput<K, V> mergedMapOutputs =
unconditionalReserve(dummyMapId, mergeOutputSize, false);
// 创建写内存的Writer
Writer<K, V> writer =
new InMemoryWriter<K, V>(mergedMapOutputs.getArrayStream());
LOG.info("Initiating Memory-to-Memory merge with " + noInMemorySegments +
" segments of total-size: " + mergeOutputSize);
// 执行内存中的合并,并将最终结果通过writer(InMemoryWriter)写到mergedMapOutputs(InMemoryMapOutput)
RawKeyValueIterator rIter =
Merger.merge(jobConf, rfs,
(Class<K>)jobConf.getMapOutputKeyClass(),
(Class<V>)jobConf.getMapOutputValueClass(),
inMemorySegments, inMemorySegments.size(),
new Path(reduceId.toString()),
(RawComparator<K>)jobConf.getOutputKeyComparator(),
reporter, null, null, null);
Merger.writeFile(rIter, writer, reporter, jobConf);
writer.close();
LOG.info(reduceId +
" Memory-to-Memory merge of the " + noInMemorySegments +
" files in-memory complete.");
// 将内存到内存合并的输出(输出还在内存中),记录到inMemoryMergedMapOutputs(最终还是会合并到磁盘)
closeInMemoryMergedFile(mergedMapOutputs);
}
}
// 内存到磁盘合并的线程
private class InMemoryMerger extends MergeThread<InMemoryMapOutput<K,V>, K,V> {
// ...
@Override
public void merge(List<InMemoryMapOutput<K,V>> inputs) throws IOException {
if (inputs == null || inputs.size() == 0) {
return;
}
// 将这个输出文件命名为当前inmem文件列表中第一个文件的名称(保证当前磁盘上没有这个名称)。所以我们不会重写prev。创建溢写)。此外,我们现在需要创建输出文件,因为不能保证该文件在调用merge后仍然存在(我们在merge方法中一旦看到空文件,就删除它们)。
// 找出mapId
TaskAttemptID mapId = inputs.get(0).getMapId();
TaskID mapTaskId = mapId.getTaskID();
List<Segment<K, V>> inMemorySegments = new ArrayList<Segment<K, V>>();
// 创建读内存的Segment添加到inMemorySegments中
long mergeOutputSize =
createInMemorySegments(inputs, inMemorySegments,0);
int noInMemorySegments = inMemorySegments.size();
// 合并之后的文件路径
Path outputPath =
mapOutputFile.getInputFileForWrite(mapTaskId,
mergeOutputSize).suffix(
Task.MERGED_OUTPUT_PREFIX);
// ...
try {
LOG.info("Initiating in-memory merge with " + noInMemorySegments +
" segments...");
// 执行内存到磁盘的合并,如果有combine,会先执行combine再合并
rIter = Merger.merge(jobConf, rfs,
(Class<K>)jobConf.getMapOutputKeyClass(),
(Class<V>)jobConf.getMapOutputValueClass(),
inMemorySegments, inMemorySegments.size(),
new Path(reduceId.toString()),
(RawComparator<K>)jobConf.getOutputKeyComparator(),
reporter, spilledRecordsCounter, null, null);
if (null == combinerClass) {
Merger.writeFile(rIter, writer, reporter, jobConf);
} else {
// 设置combine(reduce)执行的结果写入到writer(内存合并到磁盘的输出)
combineCollector.setWriter(writer);
// 执行combine(reduce)执行的结果会写入到writer
combineAndSpill(rIter, reduceCombineInputCounter);
}
writer.close();
compressAwarePath = new CompressAwarePath(outputPath,
writer.getRawLength(), writer.getCompressedLength());
// ...
} catch (IOException e) {
// 确保删除之前调用cloneFileAttributes时创建的磁盘文件
localFS.delete(outputPath, true);
throw e;
}
// 将内存到文件合并的输出(中间合并文件),记录到onDiskMapOutputs,onDiskMapOutputs达到阈值时,会唤醒磁盘到磁盘合并线程执行这些磁盘文件(中间文件或直接拉取的map输出文件)的合并
closeOnDiskFile(compressAwarePath);
}
}
// 磁盘到磁盘合并的线程
private class OnDiskMerger extends MergeThread<CompressAwarePath,K,V> {
// ...
@Override
public void merge(List<CompressAwarePath> inputs) throws IOException {
// 健全性检查
if (inputs == null || inputs.isEmpty()) {
LOG.info("No ondisk files to merge...");
return;
}
long approxOutputSize = 0;
int bytesPerSum =
jobConf.getInt("io.bytes.per.checksum", 512);
LOG.info("OnDiskMerger: We have " + inputs.size() +
" map outputs on disk. Triggering merge...");
// 1. 准备要合并的文件列表
for (CompressAwarePath file : inputs) {
approxOutputSize += localFS.getFileStatus(file).getLen();
}
// 添加校验和长度
approxOutputSize +=
ChecksumFileSystem.getChecksumLength(approxOutputSize, bytesPerSum);
// 2. 启动磁盘上的合并过程
Path outputPath =
localDirAllocator.getLocalPathForWrite(inputs.get(0).toString(),
approxOutputSize, jobConf).suffix(Task.MERGED_OUTPUT_PREFIX); // 合并输出的文件路径
// ...
// 合并产生的中间文件,添加到待合并列表中,并判断待合并列表长度是否超过阈值,是否需要继续合并
closeOnDiskFile(compressAwarePath);
// ...
}
}
// ...
// 最终合并,所有内存文件和磁盘文件全部合并返回一个迭代器(最终合并没有真的合并)。待合并磁盘文件是磁盘文件数量小于阈值(2 * ioSortFactor - 1),合并线程没有执行合并而剩下的磁盘文件。待合并内存文件是内存文件总大小小于阈值(mergeThreshold),合并线程没有执行合并而剩下的内存文件。如果待合并的磁盘文件(onDiskMapOutputs)数量大于等于ioSortFactor,则这里不会执行任何合并,只是将待合并的磁盘文件和内存文件合并返回一个迭代器(不是真的合并)。如果待合并的磁盘文件(onDiskMapOutputs)数量小于ioSortFactor,则先会将尽可能多的待合并内存文件合并成一个磁盘文件(这是真的合并会产生一个合并文件,当使用的内存超过maxInMemReduce限制之后,剩下的内存文件不合并),然后将生成的合并文件也添加到待合并磁盘文件列表中,最后将待合并磁盘文件和剩下的未合并的内存文件全部合并返回一个迭代器(不是真的合并)
private RawKeyValueIterator finalMerge(JobConf job, FileSystem fs,
List<InMemoryMapOutput<K,V>> inMemoryMapOutputs,
List<CompressAwarePath> onDiskMapOutputs
) throws IOException {
// ...
// 腾出内存所需的段
List<Segment<K,V>> memDiskSegments = new ArrayList<Segment<K,V>>(); // 待合并到磁盘的内存Segment列表
long inMemToDiskBytes = 0;
boolean mergePhaseFinished = false;
if (inMemoryMapOutputs.size() > 0) { // 有待合并的内存文件
TaskID mapId = inMemoryMapOutputs.get(0).getMapId().getTaskID();
// 创建内存文件Segment添加到memDiskSegments中;如果处理的内存超过maxInMemReduce这个限制,则不将InMemoryMapOutput转换成Segment,此时inMemoryMapOutputs中还有数据
inMemToDiskBytes = createInMemorySegments(inMemoryMapOutputs,
memDiskSegments,
maxInMemReduce);
final int numMemDiskSegments = memDiskSegments.size();
// 待合并的内存segment数量大于0且合并因子大于待合并的磁盘文件数量
if (numMemDiskSegments > 0 &&
ioSortFactor > onDiskMapOutputs.size()) {
// 如果我们到达这里,这意味着我们有少于io.sort.factor(合并因子)磁盘段,并且这将增加1(内存段合并的结果)。由于这个总数仍然是<=io.sort.factor(合并因子),我们将不再进行任何中间合并,所有这些磁盘段的合并将直接提供给reduce方法
mergePhaseFinished = true;
// 必须溢写到磁盘,但不能为中间合并保留内存
final Path outputPath = // 本次内存合并到磁盘的输出文件路径
mapOutputFile.getInputFileForWrite(mapId,
inMemToDiskBytes).suffix(
Task.MERGED_OUTPUT_PREFIX);
// ...
try {
Merger.writeFile(rIter, writer, reporter, job); // 合并数据写到磁盘
writer.close();
// 合并结束之后,将合并产生的中间磁盘文件添加到待合并磁盘文件列表中
onDiskMapOutputs.add(new CompressAwarePath(outputPath,
writer.getRawLength(), writer.getCompressedLength()));
writer = null;
// add to list of final disk outputs.
} // ...
LOG.info("Merged " + numMemDiskSegments + " segments, " +
inMemToDiskBytes + " bytes to disk to satisfy " +
"reduce memory limit");
inMemToDiskBytes = 0;
memDiskSegments.clear();
} else if (inMemToDiskBytes != 0) {
// ...
}
}
// 待合并的磁盘Segment
List<Segment<K,V>> diskSegments = new ArrayList<Segment<K,V>>();
// ...
for (CompressAwarePath file : onDisk) {
// ...
diskSegments.add(new Segment<K, V>(job, fs, file, codec, keepInputs,
(file.toString().endsWith(
Task.MERGED_OUTPUT_PREFIX) ?
null : mergedMapOutputsCounter), file.getRawDataLength()
));
}
// ...
// 构建最终的待合并的磁盘段和内存段列表
List<Segment<K,V>> finalSegments = new ArrayList<Segment<K,V>>();
// 本方法中上面调用createInMemorySegments时,如果处理的内存超过maxInMemReduce这个限制,此时inMemoryMapOutputs中还有数据在这里处理,创建内存文件Segment添加到finalSegments中
long inMemBytes = createInMemorySegments(inMemoryMapOutputs,
finalSegments, 0);
LOG.info("Merging " + finalSegments.size() + " segments, " +
inMemBytes + " bytes from memory into reduce");
if (0 != onDiskBytes) { // 有待合并的磁盘文件
// 待合并的磁盘文件数量大于等于合并因子时,待合并的内存Segment(memDiskSegments)在上面没有执行合并
final int numInMemSegments = memDiskSegments.size();
// 将上面没有合并的内存Segment(memDiskSegments),全部添加到磁盘Segment列表中
diskSegments.addAll(0, memDiskSegments);
memDiskSegments.clear();
// 只有当存在中间合并时,才传递mergePhase。参见mergePhaseFinished被设置的注释
Progress thisPhase = (mergePhaseFinished) ? null : mergePhase;
// 合并diskSegments(磁盘Segment列表,可能也包含内存Segment),这里没有执行真的合并,只是返回了迭代器
RawKeyValueIterator diskMerge = Merger.merge(
job, fs, keyClass, valueClass, codec, diskSegments,
ioSortFactor, numInMemSegments, tmpDir, comparator,
reporter, false, spilledRecordsCounter, null, thisPhase);
diskSegments.clear();
if (0 == finalSegments.size()) {
return diskMerge;
}
// 将上面合并diskSegments生成的迭代器(不是真的合并),包装成一个Segment添加到finalSegments中
finalSegments.add(new Segment<K,V>(
new RawKVIteratorReader(diskMerge, onDiskBytes), true, rawBytes));
}
// 合并finalSegments(磁盘Segment列表,可能也包含内存Segment),这里没有执行真的合并,只是返回了迭代器
return Merger.merge(job, fs, keyClass, valueClass,
finalSegments, finalSegments.size(), tmpDir,
comparator, reporter, spilledRecordsCounter, null,
null);
}
// ...
}
abstract class MergeThread<T,K,V> extends Thread {
// ...
private AtomicInteger numPending = new AtomicInteger(0); // 待合并次数,正常情况下与pendingToBeMerged对应,有异常时会被设置为0,让对当前线程中方法的调用全部结束掉(当前线程也会结束)
private LinkedList<List<T>> pendingToBeMerged; // 待合并的列表(每个元素也是列表,表示执行一次合并的列表)
protected final MergeManagerImpl<K,V> manager;
// ...
private final int mergeFactor; // 合并因子,每次执行合并的数量不能大于该值
// ...
// 拉取数据结束时,调用这个close方法,等待该合并线程执行结束,并打断该线程使该线程执行结束
public synchronized void close() throws InterruptedException {
closed = true;
waitForMerge(); // 等待合并结束
interrupt(); // 打断线程
}
// 待合并数据达到阈值时,会调用这个方法,让对应的合并线程开始合并数据
public void startMerge(Set<T> inputs) {
if (!closed) {
numPending.incrementAndGet();
List<T> toMergeInputs = new ArrayList<T>();
Iterator<T> iter=inputs.iterator();
for (int ctr = 0; iter.hasNext() && ctr < mergeFactor; ++ctr) { // 一次合并的数量不大于合并因子
toMergeInputs.add(iter.next());
iter.remove();
}
LOG.info(getName() + ": Starting merge with " + toMergeInputs.size() +
" segments, while ignoring " + inputs.size() + " segments");
synchronized(pendingToBeMerged) {
pendingToBeMerged.addLast(toMergeInputs); // 添加到待合并列表中
pendingToBeMerged.notifyAll(); // 唤醒线程,执行合并
}
}
}
// 等待当前线程合并结束
public synchronized void waitForMerge() throws InterruptedException {
// 合并结束时,numPending为0;发生异常时numPending会被设置为0(合并会因为异常而结束)
while (numPending.get() > 0) {
wait();
}
}
public void run() {
while (true) {
List<T> inputs = null;
try {
// 等待通知以启动合并...
synchronized (pendingToBeMerged) {
while(pendingToBeMerged.size() <= 0) {
pendingToBeMerged.wait();
}
// Pickup the inputs to merge.
inputs = pendingToBeMerged.removeFirst();
}
// 执行合并
merge(inputs);
} catch (InterruptedException ie) {
numPending.set(0);
return;
} catch(Throwable t) {
numPending.set(0);
reporter.reportException(t);
return;
} finally {
synchronized (this) {
numPending.decrementAndGet();
notifyAll(); // 唤醒waitForMerge方法
}
}
}
}
// 合并,有三个实现分别是:内存合并到内存、内存合并到磁盘、磁盘合并到磁盘
// 这里所有的合并逻辑与map阶段溢写文件的合并是一样的;在内存合并到内存时如果有combine则先执行combine然后再合并,其它的合并都不会先执行combine,因为内存合并到内存时执行combine代价很低(combine是否执行不影响结果,但是如果执行combine代价很低,则会加快reduce阶段的执行)
public abstract void merge(List<T> inputs) throws IOException;
}
EventFetcher
class EventFetcher<K,V> extends Thread { 用于获取已完成的map列表
// ...
@Override
public void run() {
int failures = 0;
LOG.info(reduce + " Thread started: " + getName());
try {
while (!stopped && !Thread.currentThread().isInterrupted()) {
try {
int numNewMaps = getMapCompletionEvents(); // 获取已经完成的map数量
failures = 0;
if (numNewMaps > 0) {
LOG.info(reduce + ": " + "Got " + numNewMaps + " new map-outputs");
}
LOG.debug("GetMapEventsThread about to sleep for " + SLEEP_TIME);
if (!Thread.currentThread().isInterrupted()) {
Thread.sleep(SLEEP_TIME);
}
} // ...
}
} catch (InterruptedException e) {
return;
} catch (Throwable t) {
exceptionReporter.reportException(t);
return;
}
}
// ...
// 在TaskTracker中查询给定事件ID中的一组map完成事件
protected int getMapCompletionEvents()
throws IOException, InterruptedException {
int numNewMaps = 0;
TaskCompletionEvent events[] = null;
do {
MapTaskCompletionEventsUpdate update =
umbilical.getMapCompletionEvents(
(org.apache.hadoop.mapred.JobID)reduce.getJobID(),// reduce id对应的就是分区号
fromEventIdx,
maxEventsToFetch,
(org.apache.hadoop.mapred.TaskAttemptID)reduce);
events = update.getMapTaskCompletionEvents(); // 获取已经完成的map事件列表
// ...
// 更新上次看到的事件ID
fromEventIdx += events.length;
// Process the TaskCompletionEvents:
// 1. 将SUCCEEDED map保存在knownOutputs中以获取输出
// 2. 将OBSOLETE/FAILED/KILLED map保存在obsoleteOutputs中,以停止从这些map中拉取
// 3. 从neededOutputs中删除TIPFAILED map,因为我们根本不需要它们的输出
for (TaskCompletionEvent event : events) {
// 拿到已完成的map事件,调用ShuffleSchedulerImpl.resolve处理这个事件,会唤醒Fetcher线程去拉取已经完成的map输出
scheduler.resolve(event);
if (TaskCompletionEvent.Status.SUCCEEDED == event.getTaskStatus()) {
++numNewMaps;
}
}
} while (events.length == maxEventsToFetch);
return numNewMaps;
}
}
Fetcher
class Fetcher<K,V> extends Thread {
// ...
public void run() {
try {
while (!stopped && !Thread.currentThread().isInterrupted()) {
MapHost host = null;
try {
// If merge is on, block
merger.waitForResource(); // 等待内存合并完成,释放内存
// Get a host to shuffle from
host = scheduler.getHost(); // 获取PENDING状态的的MapHost
metrics.threadBusy();
// Shuffle
copyFromHost(host); // 拉取数据到本地,可能会失败,但是当前线程会继续拉取的
} finally {
if (host != null) {
scheduler.freeHost(host);
metrics.threadFree();
}
}
}
} catch (InterruptedException ie) {
return;
} catch (Throwable t) {
exceptionReporter.reportException(t);
}
}
// ...
// 拉取map输出的关键方法
// host–MapHost,我们需要从中shuffle可用的map输出
@VisibleForTesting
protected void copyFromHost(MapHost host) throws IOException {
// reset retryStartTime for a new host
retryStartTime = 0;
// Get completed maps on 'host'
// 获取host上已完成的map,会做一些校验,并限制返回已完成的map数量(即一次最多处理多少个已完成的map)
List<TaskAttemptID> maps = scheduler.getMapsForHost(host);
// 完整性检查以捕获只有'OBSOLETE' map的主机,特别是在大型作业的尾部
if (maps.size() == 0) {
return;
}
// ...
// 待拉取的map列表
Set<TaskAttemptID> remaining = new HashSet<TaskAttemptID>(maps);
// 构造url,拼接maps中的所有mapId,拉取多个map指定分区的输出
URL url = getMapOutputURL(host, maps);
DataInputStream input = null;
try {
input = openShuffleUrl(host, remaining, url); // 打开拉取map输出的流
if (input == null) {
return;
}
// 循环遍历可用的map输出并拉取它们(url已经指定了分区号,只会拉取指定分区的数据)
// 在出现任何错误时,faildTasks都不为空,我们将剩余的map放回yet_to_be_fetched列表并标记失败的任务后退出
TaskAttemptID[] failedTasks = null;
while (!remaining.isEmpty() && failedTasks == null) {
try {
// copyMapOutput方法执行一次只会拷贝一个map的输出(已经是指定分区的数据),并删除remaining中指定的mapId。然后继续循环,直到remaining中的map全部处理完毕。copyMapOutput返回值不为null时表示出现了异常,会跳出循环(下次循环条件不再满足)
failedTasks = copyMapOutput(host, input, remaining, fetchRetryEnabled);
} catch (IOException e) { // 远程连接异常,会进行重试
IOUtils.cleanupWithLogger(LOG, input);
// 如果被NM断开连接,请重新建立连接
connection.disconnect();
// 仅从剩余任务获取map输出(已成功拉取的map数据不再拉取)
url = getMapOutputURL(host, remaining);
input = openShuffleUrl(host, remaining, url);
if (input == null) {
return;
}
}
}
// ...
} finally {
// ...
// 将没有成功拉取的map输出,再放回MapHost待拉取的map列表中,当前Fetcher线程会再次拉取的
for (TaskAttemptID left : remaining) {
scheduler.putBackKnownMapOutput(host, left);
}
}
}
// 拉取数据到本地(内存或磁盘),并通知shuffle调度器。该方法返回null表示执行成功,返回值不为null时表示出现了异常
private TaskAttemptID[] copyMapOutput(MapHost host,
DataInputStream input,
Set<TaskAttemptID> remaining,
boolean canRetry) throws IOException {
// ...
try {
// ...
//Read the shuffle header
try {
ShuffleHeader header = new ShuffleHeader();
header.readFields(input);
mapId = TaskAttemptID.forName(header.mapId); // 数据对应的mapId
compressedLength = header.compressedLength; // 压缩的数据大小
decompressedLength = header.uncompressedLength; // 解压缩的数据大小
forReduce = header.forReduce; // 拉取的数据对应的reduce即分区
} catch (IllegalArgumentException e) {
badIdErrs.increment(1);
LOG.warn("Invalid map id ", e);
// 不知道哪一个是坏的,所以认为它们都是坏的
return remaining.toArray(new TaskAttemptID[remaining.size()]);
}
// ...
// 做一些基本的完整性验证,数据长度是否大于0、数据是否是指定的分区、数据对应的mapId是否正确
if (!verifySanity(compressedLength, decompressedLength, forReduce,
remaining, mapId)) {
return new TaskAttemptID[] {mapId};
}
// ...
// 获取map输出的位置-在内存或磁盘上
try {
mapOutput = merger.reserve(mapId, decompressedLength, id);
} catch (IOException ioe) { // IO异常说明本地不能创建拉取数据到本地的输出文件,这个异常很严重
// 终止此reduce
ioErrs.increment(1);
scheduler.reportLocalError(ioe);
return EMPTY_ATTEMPT_ID_ARRAY;
}
// 检查一下我们现在是否可以shuffle
if (mapOutput == null) { // mapOutput为null,表示内存使用已经达到阈值,返回空列表,等待内存到磁盘合并完成并回收内存,然后再拉取数据
LOG.info("fetcher#" + id + " - MergeManager returned status WAIT ...");
// 不是错误,而是等待处理数据。
return EMPTY_ATTEMPT_ID_ARRAY;
}
// lz0,lz4,snappy,bz2等编解码器,抛出关于解压缩失败的异常java.lang.InternalError。捕获并作为IOException重新抛出,以允许处理拉取失败逻辑
try {
// ...
// 拉取数据,从输入流中读取数据,写入到本地(内存或磁盘中)
mapOutput.shuffle(host, is, compressedLength, decompressedLength,
metrics, reporter);
} catch (java.lang.InternalError | Exception e) {
LOG.warn("Failed to shuffle for fetcher#"+id, e);
throw new IOException(e);
}
long endTime = Time.monotonicNow();
// 如果之前重试过,则在map任务取得进展时重置retryStartTime
retryStartTime = 0;
// 将数据拷贝到本地(内存或磁盘)成功,则通知shuffle调度器将本地(内存或磁盘)文件,添加到待合并列表中,并更新状态
scheduler.copySucceeded(mapId, host, compressedLength,
startTime, endTime, mapOutput);
// Note successful shuffle
remaining.remove(mapId);
metrics.successFetch();
return null;
} catch (IOException ioe) {
// ...
// 通知shuffle调度器拉取失败
metrics.failedFetch();
return new TaskAttemptID[] {mapId};
}
}
// ...
}
LocalFetcher
// LocalJobRunner使用LocalFetcher来执行本地文件系统提取
class LocalFetcher<K,V> extends Fetcher<K, V> {
// ...
public void run() {
// Create a worklist of task attempts to work over.
Set<TaskAttemptID> maps = new HashSet<TaskAttemptID>();
for (TaskAttemptID map : localMapFiles.keySet()) {
maps.add(map);
}
while (maps.size() > 0) {
try {
// If merge is on, block
merger.waitForResource(); // 等待内存合并完成,释放内存
metrics.threadBusy();
// 尽可能多地复制,直到使用内存达到阈值限制,如果maps没有拷贝完,则继续循环(再次循环时会等待内存合并完成以释放内存)
doCopy(maps);
metrics.threadFree();
} catch (InterruptedException ie) {
} catch (Throwable t) {
exceptionReporter.reportException(t);
}
}
}
// 关键方法
private void doCopy(Set<TaskAttemptID> maps) throws IOException {
Iterator<TaskAttemptID> iter = maps.iterator();
while (iter.hasNext()) {
TaskAttemptID map = iter.next();
LOG.debug("LocalFetcher " + id + " going to fetch: " + map);
if (copyMapOutput(map)) {
// Successful copy. Remove this from our worklist.
iter.remove();
} else {
// 我们得到了WAIT命令;回到外部循环并阻塞(等待)InMemoryMerge
break;
}
}
}
// 检索单个map任务的map输出,并将其发送到merger
private boolean copyMapOutput(TaskAttemptID mapTaskId) throws IOException {
// 找出map任务存储其输出的位置
Path mapOutputFileName = localMapFiles.get(mapTaskId).getOutputFile(); // map输出文件位置
Path indexFileName = mapOutputFileName.suffix(".index"); // map输出索引文件位置
// 读取它的索引来确定分割的位置和大小。
SpillRecord sr = new SpillRecord(indexFileName, job);
IndexRecord ir = sr.getIndex(reduce); // 获取分区索引,reduce对应一个分区
// ...
// 获取map输出的位置—在内存中或在磁盘上,还没有执行shuffle
MapOutput<K, V> mapOutput = merger.reserve(mapTaskId, decompressedLength,
id);
// 检查一下我们现在是否可以shuffle
if (mapOutput == null) { // 使用内存超过阈值限制,mapOutput会是null
LOG.info("fetcher#" + id + " - MergeManager returned Status.WAIT ...");
return false;
}
// ...
// 现在读取文件,查找适当的部分,并发送它
FileSystem localFs = FileSystem.getLocal(job).getRaw();
FSDataInputStream inStream = localFs.open(mapOutputFileName);
try {
inStream.seek(ir.startOffset);
inStream =
IntermediateEncryptedStream.wrapIfNecessary(job, inStream,
mapOutputFileName);
// 拉取数据,从输入流中读取数据,写入到本地(内存或磁盘中)
mapOutput.shuffle(LOCALHOST, inStream, compressedLength,
decompressedLength, metrics, reporter);
} finally {
IOUtils.cleanupWithLogger(LOG, inStream);
}
// 将数据拷贝到本地(内存或磁盘)成功,则通知shuffle调度器将本地(内存或磁盘)文件,添加到待合并列表中,并更新状态
scheduler.copySucceeded(mapTaskId, LOCALHOST, compressedLength, 0, 0,
mapOutput);
return true; // 拉取数据成功
}
}
ReduceContextImpl
@InterfaceAudience.Private
@InterfaceStability.Unstable
public class ReduceContextImpl<KEYIN,VALUEIN,KEYOUT,VALUEOUT>
extends TaskInputOutputContextImpl<KEYIN,VALUEIN,KEYOUT,VALUEOUT>
implements ReduceContext<KEYIN, VALUEIN, KEYOUT, VALUEOUT> {
private RawKeyValueIterator input; // 输入迭代器
// ...
// 分组比较器,用于对value按照key进行分组,相等的key为一组,通过comparator判断key是否相等。迭代器中的数据来自map输出,是按照key排序的,因此map输出的key排序器和这里的分组比较器必须是相关的(分组比较器判断的相同的key在map输出的key排序器中判断也必须是相同的),否则结果是不可控的。相等key的一组value调一次reduce方法
private RawComparator<KEYIN> comparator;
private KEYIN key; // 当前key
private VALUEIN value; // 当前 value
private boolean firstValue = false; // 是否是key分组的第一个value
private boolean nextKeyIsSame = false; // 下一个key是否相同,即是否属于相同的分组
private boolean hasMore; // 输入迭代器是否还有数据,即是否还有待处理的数据
protected Progressable reporter;
// key/value反序列化器用于将key/value字节数据转换成代码中使用的数据格式,字节数据会先写到buffer字段中,反序列化器再从buffer字段中拿字节数据
private Deserializer<KEYIN> keyDeserializer;
private Deserializer<VALUEIN> valueDeserializer;
private DataInputBuffer buffer = new DataInputBuffer();
private BytesWritable currentRawKey = new BytesWritable(); // 读取的当前key的原始数据
// 调用reduce方法时,相等key的一组value对应的迭代器(ValueIterator),是对输入迭代器input的封装,可以拿到相等key的所有value
private ValueIterable iterable = new ValueIterable();
// 下面两个字段,用于支持迭代器的标记重置功能
private boolean isMarked = false;
private BackupStore<KEYIN,VALUEIN> backupStore;
// ...
public ReduceContextImpl(Configuration conf, TaskAttemptID taskid,
RawKeyValueIterator input,
Counter inputKeyCounter,
Counter inputValueCounter,
RecordWriter<KEYOUT,VALUEOUT> output,
OutputCommitter committer,
StatusReporter reporter,
RawComparator<KEYIN> comparator,
Class<KEYIN> keyClass,
Class<VALUEIN> valueClass
) throws InterruptedException, IOException{
// ...
hasMore = input.next(); // 创建该类的实例时就判断是否有待处理的数据
// ...
}
// 开始处理下一个唯一key,Reducer.run方法中调用该方法循环判断是否还有下一个唯一key
public boolean nextKey() throws IOException,InterruptedException {
// 如果有更多的数据且下一个key相同,直接读取下一个key/value,在不使用backupStore重置迭代器时,这里的循环条件不会为真
while (hasMore && nextKeyIsSame) {
nextKeyValue();
}
// 在不使用backupStore时:对当前方法nextKey的调用,没有数据时返回false,有数据时调用nextKeyValue方法读取key/value并返回true;有数据时读取到的key一定是所有相同key的第一个,然后调用自定义的reduce方法,在自定义的reduce方法中遍历迭代器iterable,会迭代读取所有相同key的value数据,遍历iterable结束的标志是读取到的key发生了变化(没有数据遍历自然会结束),接着Reducer.run方法会继续调用nextKey方法(相比较上次调用key已经发生了变化);这样在有数据时nextKey方法读取到的key一定是所有相同key的第一个,iterable只能迭代出等于指定key的所有数据,保证了自定义reduce方法中可以处理相同key的所有value
if (hasMore) {
if (inputKeyCounter != null) {
inputKeyCounter.increment(1);
}
return nextKeyValue(); // 读取下一个key/value,代码走到这里一定可以读取到数据并返回true
} else {
return false;
}
}
// 读取下一个key/value,有数据返回true,没有数据返回false;读取每条记录时都会多读取一个key来更新nextKeyIsSame
// 反序列化key/value时,会调用WritableSerialization.WritableDeserializer.deserialize方法,该方法只会创建一个key/value对象,不管调用多少次都会复用第一次创建的对象然后调用Writable.readFields重新设置值,这样可以避免创建大量的对象,减少GC
@Override
public boolean nextKeyValue() throws IOException, InterruptedException {
if (!hasMore) {
key = null;
value = null;
return false;
}
firstValue = !nextKeyIsSame; // 是否是相同key的一组value中的第一个value
// 拿到key
DataInputBuffer nextKey = input.getKey();
currentRawKey.set(nextKey.getData(), nextKey.getPosition(),
nextKey.getLength() - nextKey.getPosition());
buffer.reset(currentRawKey.getBytes(), 0, currentRawKey.getLength());
key = keyDeserializer.deserialize(key);
// 拿到value
DataInputBuffer nextVal = input.getValue();
buffer.reset(nextVal.getData(), nextVal.getPosition(), nextVal.getLength()
- nextVal.getPosition());
value = valueDeserializer.deserialize(value);
// ...
hasMore = input.next();
if (hasMore) {
// 再读取一个key,判断下一个key是否相同
nextKey = input.getKey();
nextKeyIsSame = comparator.compare(currentRawKey.getBytes(), 0,
currentRawKey.getLength(),
nextKey.getData(),
nextKey.getPosition(),
nextKey.getLength() - nextKey.getPosition()
) == 0;
} else {
nextKeyIsSame = false;
}
inputValueCounter.increment(1);
return true;
}
// ...
// iterable使用的迭代器
protected class ValueIterator implements ReduceContext.ValueIterator<VALUEIN> {
// ...
// 是否有值
@Override
public boolean hasNext() {
// 关于迭代器重置的,暂不考虑
// ...
// 是一组数据中的第一个或下一个key是相同的,都表示有值
return firstValue || nextKeyIsSame;
}
@Override
public VALUEIN next() {
// 关于迭代器重置的,暂不考虑
// ...
// 如果这是第一个记录,不需要再读取下一个。第一个记录已经读过了(读取每条记录时都会多读取一个key来更新nextKeyIsSame)
if (firstValue) {
firstValue = false;
return value;
}
// 如果这不是第一个记录,下一个键是不同的,这里就不能继续了(一定有问题,可能是输入迭代器中的数据被恶意篡改了)
if (!nextKeyIsSame) {
throw new NoSuchElementException("iterate past last value");
}
// 否则,读取下一个键值对
try {
nextKeyValue();
return value;
} catch (IOException ie) {
throw new RuntimeException("next value iterator failed", ie);
} catch (InterruptedException ie) {
// this is bad, but we can't modify the exception list of java.util
throw new RuntimeException("next value iterator interrupted", ie);
}
}
// ...
}
// ...
}