Shuffer reduce端源码分析

  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的一个重要特性!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值