Hadoop会在适当的时候启动ReduceTask:条件待定??
一、从map端拉取数据
1.1、Shuffle类
ReduceTask.run()会先用反射构造ShuffleConsumerPlugin类(接口类)的实例,ShuffleConsumerPlugin的实现类默认为Shuffle类:
Class<? extends ShuffleConsumerPlugin> clazz = job.getClass("mapreduce.job.reduce.shuffle.consumer.plugin.class", Shuffle.class, ShuffleConsumerPlugin.class);
shuffleConsumerPlugin = (ShuffleConsumerPlugin)ReflectionUtils.newInstance(clazz, job);
Shuffle类初始化时会创建MergeManager<K,V>(接口)用于merge数据。MergeManagerImpl<K, V>是其实现类。MergeManagerImpl<K, V>的构造函数会start两个线程:
1、inMemoryMerger:数据从内存到磁盘的merge
2、:数据从磁盘到磁盘的merge
还有一个默认不开启的线程:memToMemMerger:数据从内存到内存的merge(一般不会使用,因为一般情况下数据量会很大)。
三个线程都extends 抽象类MergeThread(以实现run方法),共用一个run方法逻辑。
数据merge的具体内容我们下面再说。
通过ShuffleConsumerPlugin.run()从map端拉取和整合数据,获取key/value:
//用于迭代key/value的类
RawKeyValueIterator rIter = null;
//从map端拉取数据,合并数据。
rIter = shuffleConsumerPlugin.run();
public RawKeyValueIterator run() throws IOException, InterruptedException {
...
//EventFetcher线程用来获取已完成的map的事件:host等信息
EventFetcher<K, V> eventFetcher = new EventFetcher(this.reduceId, this.umbilical, this.scheduler, this, maxEventsToFetch);
eventFetcher.start();
boolean isLocal = this.localMapFiles != null;
//Fetcher线程组的个数,默认5个(可),单机版1个。可通过"mapreduce.reduce.shuffle.parallelcopies"设置
int numFetchers = isLocal ? 1 : this.jobConf.getInt("mapreduce.reduce.shuffle.parallelcopies", 5);
//Fetcher线程根据EventFetcher获取的host从map端拉取数据到内存缓冲区。
//shuffle类会创建一个Fetcher组。
Fetcher<K, V>[] fetchers = new Fetcher[numFetchers];
if (isLocal) {
fetchers[0] = new LocalFetcher(this.jobConf, this.reduceId, this.scheduler, this.merger, this.reporter, this.metrics, this, this.reduceTask.getShuffleSecret(), this.localMapFiles);
fetchers[0].start();
} else {
for(int i = 0; i < numFetchers; ++i) {
fetchers[i] = new Fetcher(this.jobConf, this.reduceId, this.scheduler, this.merger, this.reporter, this.metrics, this, this.reduceTask.getShuffleSecret());
fetchers[i].start();
}
}
...
RawKeyValueIterator kvIter;
try {
kvIter = this.merger.close();
} catch (Throwable var13) {
throw new Shuffle.ShuffleError("Error while doing final merge ", var13);
}
...
//返回key/value迭代器
return kvIter;
...
}
Shuffle类会创建并Start一个EventFetcher线程,用来获取map完成的事件(host等信息)。还会创建并Start默认数量为5(可通过mapreduce.reduce.shuffle.parallelcopies设置)的Fetcher[] 线程组,用来根据EvenFetcher的信息拉取对应map端的数据。
新建Fetcher时,会把MergeManagerImpl<K, V>作为参数传进去。
1.2、Fetcher线程
Fetcher.run()方法有三个主要步骤:
1、等待merge完成:this.merger.waitForResource()
调用的是MergeManagerImpl的waitForResource()方法,waitForResource()调用的是inMemoryMerger.waitForMerge(),即父类MergeThread(抽象类)的waitForMerge():
public synchronized void waitForMerge() throws InterruptedException {
while(this.numPending.get() > 0) {
this.wait();
}
}
numPending是一个原子操作类AtomicInteger,保证线程安全。当内存缓冲区使用超过阈值,Fetcher会让numPending+1,那么所有其他Fetcher线程开始新的数据拉取时,会调用被synchronized修饰的waitForMerge方法,使所有Fetcher线程失去"this"即inMemoryMerger对象的锁,进入阻塞,停止拉取数据。
在内存到磁盘的merge完成后,inMemoryMerger线程会将numPending-1,然后notifyAll(inMemoryMerger对象)。那么Fetcher线程就会继续运行,不再阻塞。
2、获取host
host = this.scheduler.getHost();
EventFetcher线程会将已完成的map端的host等信息存放在scheduler里的set < MapHost > 集合里。
Fetcher线程会从scheduler获取将要读取的Map端的连接信息。
3、拉取数据
Fetcher会调用自身的方法copyFromHost(host);
protected void copyFromHost(MapHost host) throws IOException {
...
//获取改host的map任务Id,因为一个map节点,可能开启了数个map任务,即有数个map数据要拉取。
List<TaskAttemptID> maps = this.scheduler.getMapsForHost(host);
if (maps.size() != 0) {
//将map任务Id转换为set集合。
Set<TaskAttemptID> remaining = new HashSet(maps);
//获取url
URL url = this.getMapOutputURL(host, maps);
//建立连接,获取数据流
DataInputStream input = this.openShuffleUrl(host, remaining, url);
...
//循环复制该节点所有已完成map任务数据
while(!remaining.isEmpty() && failedTasks == null) {
//从数据流复制数据
failedTasks = this.copyMapOutput(host, input, remaining, this.fetchRetryEnabled);
}
...
//下面还有一些处理复制失败的容错代码,就不仔细看了。
}
}
我们看看主要的 copyMapOutput(host, input, remaining, this.fetchRetryEnabled) 方法:
private TaskAttemptID[] copyMapOutput(MapHost host, DataInputStream input, Set<TaskAttemptID> remaining, boolean canRetry) throws IOException {
//存放数据的对象,MapOutput是抽象类,InMemoryMapOutput和OnDiskMapOutput是它的实现类
MapOutput<K, V> mapOutput = null;
...
//返回MapOutput对象
//如果数据量很少,会返回OnDiskMapOutput对象,即直接拷贝到磁盘
//一般返回InMemoryMapOutput对象,即拷贝到内存缓冲区。
mapOutput = this.merger.reserve(mapId, decompressedLength, this.id);
...
//我们只分析将InMemoryMapOutput的情况:将input数据流的数据复制到InMemoryMapOutput里的byte[] memory
mapOutput.shuffle(host, is, compressedLength, decompressedLength, this.metrics, this.reporter);
...
//复制完成,将mapOutput添加到内存缓冲区里,并判断是否达到阈值,启动merge
this.scheduler.copySucceeded(mapId, host, compressedLength, startTime, endTime, mapOutput);
}
这里会根据数据量创建MapOutput实例,一般创建的是InMemoryMapOutput,即将数据存到内存了(当数据量很少的时候会直接拷贝到磁盘,即创建OnDiskMapOutput)。
然后调用InMemoryMapOutput.shuffle()将数据复制到InMemoryMapOutput里,InMemoryMapOutput有一个Byte[] 用来存放数据:
public void shuffle(MapHost host, InputStream input, long compressedLength, long decompressedLength, ShuffleClientMetrics metrics, Reporter reporter) throws IOException {
...
//将数据流input的数据全部放到byte[] memory里。
IOUtils.readFully((InputStream)input, this.memory, 0, this.memory.length);
...
}
scheduler.copySucceeded()会调用output.commit() => InMemoryMapOutput.commit() => MergeManagerImpl.closeInMemoryFile(InMemoryMapOutput)。
让我们看看closeInMemoryFile(InMemoryMapOutput)方法:
Set<InMemoryMapOutput<K, V>> inMemoryMapOutputs = new TreeSet(new MapOutputComparator());
public synchronized void closeInMemoryFile(InMemoryMapOutput<K, V> mapOutput) {
//相当于内存缓冲区,fetcher线程拉取的map数据都会放到这个set集合里。
this.inMemoryMapOutputs.add(mapOutput);
//记录已存放的所有数据的总长度
this.commitMemory += mapOutput.getSize();
//将已存放数据的长度和阈值比较,如果大于阈值,开始内存到磁盘的merge
if (this.commitMemory >= this.mergeThreshold) {
LOG.info("Starting inMemoryMerger's merge since commitMemory=" + this.commitMemory + " > mergeThreshold=" + this.mergeThreshold + ". Current usedMemory=" + this.usedMemory);
//将内存到内存merge完成的数据加进来,因为内存到内存的merge默认关闭,所以inMemoryMergedMapOutputs为空
this.inMemoryMapOutputs.addAll(this.inMemoryMergedMapOutputs);
this.inMemoryMergedMapOutputs.clear();
//开始内存到磁盘的merge
this.inMemoryMerger.startMerge(this.inMemoryMapOutputs);
//重置commitMemory为0
this.commitMemory = 0L;
}
//如果内存到内存的merge线程启动(默认关闭)了,那么当剩下的map数据数量>=默认值100个时,开始内存到内存的merge
if (this.memToMemMerger != null && this.inMemoryMapOutputs.size() >= this.memToMemMergeOutputsThreshold) {
this.memToMemMerger.startMerge(this.inMemoryMapOutputs);
}
}
所有Fetcher线程拉取的数据都会放到一个集合里Set<InMemoryMapOutput<K, V>> ,会有一个变量commitMemory记录所有数据的长度。因为这个方法是synchronized修饰的,所以是线程安全的。commitMemory会和阈值mergeThreshold比较,大于阈值,就会开始内存到磁盘的merge:inMemoryMerger.startMerge。
阈值=
mapreduce.reduce.memory.totalbytes 默认:Runtime.getRuntime().maxMemory()
*
mapreduce.reduce.shuffle.input.buffer.percent 默认:0.7
*
mapreduce.reduce.shuffle.merge.percent 默认0.9
Runtime.getRuntime().maxMemory() :java虚拟机的最大堆内存:Xmx设置。
因为所有map数据copy完之前,reduce是不会执行的,所已虚拟机的内存大部分可以用于缓存区。
二、内存到磁盘的merge
2.1、inMemoryMerger线程
内存到磁盘的merge是由inMemoryMerger线程处理的,inMemoryMerger线程比Fetcher线程创建的还要早。
inMemoryMerger继承MergeThread抽象类,调用的时父类MergeThread的run方法:
LinkedList<List<T>> pendingToBeMerged = new LinkedList();
public void run() {
while(true) {
List inputs = null;
...
//synchronized块,当pendingToBeMerged里的元素个数少于等于0时,inMemoryMerger线程将pendingToBeMerged对象锁释放并等待阻塞。
synchronized(this.pendingToBeMerged) {
while(this.pendingToBeMerged.size() <= 0) {
this.pendingToBeMerged.wait();
}
//Fetcher线程调用.startMerge()时会将数据加到pendingToBeMerged里,并唤醒(notifyAll)所有pendingToBeMerged对象锁的wait()。inMemoryMerger线程将获取pendingToBeMerged对象锁,继续执行
//获取pendingToBeMerged的第一个元素,并从pendingToBeMerged中删除。该元素就是一批map数据
inputs = (List)this.pendingToBeMerged.removeFirst();
}
//将数据merge到磁盘
this.merge(inputs);
...
synchronized(this) {
//将numPending重置为0,并唤醒(notifyAll)所有持有inMemoryMerger对象在wait()的Fetcher线程。
this.numPending.decrementAndGet();
this.notifyAll();
}
...
}
}
inMemoryMerger线程在内存缓存区没有到阈值之前,会在pendingToBeMerged对象锁上阻塞。如上面所说,当数据量大于阈值时,Fetcher线程会调用inMemoryMerger.startMerge(),唤醒inMemoryMerger线程:
public void startMerge(Set<T> inputs) {//
if (!this.closed) {
//用于阻塞Fetcher线程,在merge结束之前停止copy数据到内存。
this.numPending.incrementAndGet();
//将内存上的所有map数据从set<InMemoryMapOutput>转换成List<InMemoryMapOutput>
List<T> toMergeInputs = new ArrayList();
Iterator<T> iter = inputs.iterator();
for(int ctr = 0; iter.hasNext() && ctr < this.mergeFactor; ++ctr) {
toMergeInputs.add(iter.next());
iter.remove();
}
LOG.info(this.getName() + ": Starting merge with " + toMergeInputs.size() + " segments, while ignoring " + inputs.size() + " segments");
//Fetcher线程获取pendingToBeMerged对象锁,将一批map数据存放到pendingToBeMerged里,然后唤醒inMemoryMerger线程在pendingToBeMerged对象锁的wait()。
synchronized(this.pendingToBeMerged) {
this.pendingToBeMerged.addLast(toMergeInputs);
this.pendingToBeMerged.notifyAll();
}
}
}
唤醒inMemoryMerger线程后,inMemoryMerger.run()继续执行,调用merge(List< InMemoryMapOutput >)方法将数据merge到磁盘。
暂时看不懂:
inMemoryMerger.merge:内存到内存的merge
onDiskMerge.merge:磁盘到磁盘的merge
MergeManageImp.finalMerge:最终输出到reduce的merge
combiner只会发生在内存到内存的merge时
它们都共用一套逻辑:Merge.MergeQueue.merge()(看不懂)
排序的具体逻辑:
reduce端的排序主要在三个步骤中:
1、内存到磁盘的merge时
2、磁盘到磁盘的merge时
3、finalMerge
4、reduce读取key/value时
它们都共用一套逻辑:
数据存放在List< Segment >中,每个Segment里有一个byte[]存放大小各异的key/value对。
List< Segment >是在一个优先级队列MergeQueue里,保证List< Segment >是根据Segment第一个key大小排序的,排序准则是用户设置的排序器。
当合并数据或者读取数据时,会先拿第一个Segment的第一个key/value,因为Segment里的key/value是有序的(map端保证),而Segment也是有序的,所以第一个Segment的第一个key/value肯定是最小的。拿走第一个key/value后,会调整优先级队列MergeQueue,使List< Segment >的第一个Segment的第一个key/value是最小的。不断重复,就可以实现对多个Segment里的key/value的归并排序。
三、reduce读取key/value
reduceTask先获取实例为MergeQueue(实现了RawKeyValueIterator)的RawKeyValueIterator对象。
RawKeyValueIterator是用于获取key/value的接口。
RawKeyValueIterator rIter = null;
rIter = shuffleConsumerPlugin.run();
获取MapOutputKeyClass、MapOutputValueClass和分组比较器:
Class keyClass = job.getMapOutputKeyClass();
Class valueClass = job.getMapOutputValueClass();
RawComparator comparator = job.getOutputValueGroupingComparator();
调用runNewReducer():
runNewReducer(job, umbilical, reporter, rIter, comparator, keyClass, valueClass);
private <INKEY, INVALUE, OUTKEY, OUTVALUE> void runNewReducer(JobConf job, TaskUmbilicalProtocol umbilical, final TaskReporter reporter, final RawKeyValueIterator rIter, RawComparator<INKEY> comparator, Class<INKEY> keyClass, Class<INVALUE> valueClass) throws IOException, InterruptedException, ClassNotFoundException {
//重新现实RawKeyValueIterator接口,让reducerContext只能使用key/value的获取方法(MergeQueue有许多其他方法:如merge)。
rIter = new RawKeyValueIterator() {
public void close() throws IOException {
rIter.close();
}
public DataInputBuffer getKey() throws IOException {
return rIter.getKey();
}
public Progress getProgress() {
return rIter.getProgress();
}
public DataInputBuffer getValue() throws IOException {
return rIter.getValue();
}
public boolean next() throws IOException {
boolean ret = rIter.next();
reporter.setProgress(rIter.getProgress().getProgress());
return ret;
}
};
TaskAttemptContext taskContext = new TaskAttemptContextImpl(job, this.getTaskID(), reporter);
//获取用户编写的reduce class
org.apache.hadoop.mapreduce.Reducer<INKEY, INVALUE, OUTKEY, OUTVALUE> reducer = (org.apache.hadoop.mapreduce.Reducer)ReflectionUtils.newInstance(taskContext.getReducerClass(), job);
//获取Redecue输出key/value的类 默认是FileOutputFormat:输出到文件
org.apache.hadoop.mapreduce.RecordWriter<OUTKEY, OUTVALUE> trackedRW = new ReduceTask.NewTrackingRecordWriter(this, taskContext);
job.setBoolean("mapred.skip.on", this.isSkipping());
job.setBoolean("mapreduce.job.skiprecords", this.isSkipping());
//创建实例为ReduceContextImpl的ReduceContext对象。将其包裹在WrappedReducer里。
org.apache.hadoop.mapreduce.Reducer.Context reducerContext = createReduceContext(reducer, job, this.getTaskID(), rIter, this.reduceInputKeyCounter, this.reduceInputValueCounter, trackedRW, this.committer, reporter, comparator, keyClass, valueClass);
try {
//调用reducer.run方法,传参是上面创建的WrappedReducer。
reducer.run(reducerContext);
} finally {
trackedRW.close(reducerContext);
}
}
重点来了:
reducer.run():
public void run(Reducer<KEYIN, VALUEIN, KEYOUT, VALUEOUT>.Context context) throws IOException, InterruptedException {
this.setup(context);
try {
while(context.nextKey()) {
this.reduce(context.getCurrentKey(), context.getValues(), context);
Iterator<VALUEIN> iter = context.getValues().iterator();
if (iter instanceof ValueIterator) {
((ValueIterator)iter).resetBackupStore();
}
}
} finally {
this.cleanup(context);
}
}
context.nextKey() => ReduceContextImp.nextKey():
public boolean nextKey() throws IOException, InterruptedException {
while(this.hasMore && this.nextKeyIsSame) {
this.nextKeyValue();
}
if (this.hasMore) {
if (this.inputKeyCounter != null) {
this.inputKeyCounter.increment(1L);
}
return this.nextKeyValue();
} else {
return false;
}
}
我们看看nextKey都做了些什么。
首先它ReduceContextImp有 boolean 两个变量:hasMore 和 nextKeyIsSame.。
hasMore的初始值是RawKeyValueIterator.next()。实际调用的是MergeQueue.next():
public boolean next() throws IOException {
//Segment的数量为0时,返回 false,结束Reduce的循环。
if (this.size() == 0) {
this.resetKeyValue();
return false;
} else {
//minSegment是指向List<Segment>里第一个元素的临时变量。
if (this.minSegment != null) {
//如果minSegment不为空,就重新调整队列,将第一个key最小的Segment放在第一。
this.adjustPriorityQueue(this.minSegment);
if (this.size() == 0) {
this.minSegment = null;
this.resetKeyValue();
return false;
}
}
//获取List<Segment>第一个元素。
this.minSegment = (Merger.Segment)this.top();
long startPos = this.minSegment.getReader().bytesRead;
//获取第一个key
this.key = this.minSegment.getKey();
//获取第一个value
if (!this.minSegment.inMemory()) {
this.minSegment.getValue(this.diskIFileValue);
this.value.reset(this.diskIFileValue.getData(), this.diskIFileValue.getLength());
} else {
this.minSegment.getValue(this.value);
}
long endPos = this.minSegment.getReader().bytesRead;
this.totalBytesProcessed += endPos - startPos;
this.mergeProgress.set((float)this.totalBytesProcessed * this.progPerByte);
return true;
}
}
所以hasMore=input.next()是判断还有没有下一个key/value,并将下一个key/value获取到MergeQueue里的key/value变量里。
nextKeyIsSame是判断下一个key是否和当前key相等,依据用户设定的分组比较器进行比较。
nextKeyValue()方法是将MergeQueue的key/value的值获取并反序列化到ReduceContextImp自己的key/value变量,key/value的类型是用户设置好的reduce输入的key/value类型。
context.getCurrentKey()返回ReduceContextImp的key对象,即分组后的第一个key值。
context.getValues()返回的是一个ValueIterable对象的values,当我们调用values.iterator().next()时,调用的时ValueIterable.next()。ValueIterable.next()会判断nextKeyIsSame是否为true,即下一个key是否合当前key相等:
- 如果相等,调用nextKeyValue(),获取下一个key/value到ReduceContextImp自己的key/value变量,返回value值。因为reduce引用的是ReduceContextImp的key对象,而且key不是基本类型,所以reduce的key也会随之改变。
- 如果不相等:先判断它是否是分组的第一个key,如果是第一个key,直接返回value,因为第一个value值已经在nextKey()里的nextKeyValue()获取了。如果不是第一个key,它又和下一个key不相等,就报错:iterate past last value(超过最后的值了)。这种情况就是用户编写reduce逻辑时没有用iterator.hasNext()判断分组还有没有下一个value值(注:reduce的一次调用不是看values迭代完结束的,而是用户写的逻辑结束而结束的。所以用户不判断hasNext(),自然会报超出迭代的最后值)。
reduce一次调用,Iterable values不是一次性获取的,而是Iterable.next()一次就获取一次key/value,key也会随之变化。这是reduce的一个重要特性!