shuffle概述
shuffle是mapreduce任务中耗时比较大的一个过程,面试中也经常问。简单来说shuffle就是map之后,reduce之前的所有操作的过程,包含map task端对数据的分区、排序,溢写磁盘和合并操作,以及reduce task端从网络拉取数据、对数据排序合并等一系列操作:
map task
一个mapreduce任务中,map task的数据量是split数量决定的,不开启小文件合并的话,每个文件至少会启动一个map task。图中显示有4个map task。
split
-
图上我标的split大小为128M其实是不准确的,首先split大小是由minSplitSize,maxSplitSize,blockSize计算中间值得出的:
// 代码为org.apache.hadoop.mapreduce.lib.input.FileInputFormat.computeSplitSize() protected long computeSplitSize(long blockSize, long minSize, long maxSize) { return Math.max(minSize, Math.min(maxSize, blockSize)); }
-
在默认配置中minSplitSize为1,maxSplitSize为LONG.MAX_VALUE,所以默认情况下split大小为blockSize大小,blocSize在hadoop1.x及之前默认为64M,hadoop2.x默认为128M,hadoop3.x默认为256M。
-
但也要注意,这不意味这每个split的大小就是对应的blockSize,除了文件最后一个split数据量不够一个blockSize的情况,还会有最后一个split数据量大于blockSize的情况,原因是在切分split的时候,源码中定义了个final常量
SPLIT_SLOP=1.1
,当剩余数据量/splitSize > SPLIT_SLOP时才会进行下一数据片的切分:// 代码为org.apache.hadoop.mapreduce.lib.input.FileInputFormat.getSplits()中部分代码 private static final double SPLIT_SLOP = 1.1; // 10% slop long bytesRemaining = length; while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) { int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining); splits.add(makeSplit(path, length-bytesRemaining, splitSize, blkLocations[blkIndex].getHosts(), blkLocations[blkIndex].getCachedHosts())); bytesRemaining -= splitSize; } if (bytesRemaining != 0) { int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining); splits.add(makeSplit(path, length-bytesRemaining, bytesRemaining, blkLocations[blkIndex].getHosts(), blkLocations[blkIndex].getCachedHosts())); }
-
此外split的数据是以迭代器的方式一条条进入到map方法的,即数据是一条条进入内存的;
-
这里还有一个细节是:除了第一个split,其他所有split的map task都会丢弃第一条数据,因为上一个split的map task中next方法总是会多读一行记录,以解决第一条记录不完整的问题:
// 代码为org.apache.hadoop.mapred.LineRecordReader.next() // If this is not the first split, we always throw away first record // because we always (except the last split) read one extra line in // next() method. /** Read a line. */ public synchronized boolean next(LongWritable key, Text value) throws IOException { // We always read one extra line, which lies outside the upper // split limit i.e. (end - 1) while (getFilePosition() <= end || in.needAdditionalRecordAfterSplit()) { key.set(pos); int newSize = 0; if (pos == 0) { // 读取第一行后丢弃,没有赋值给pos newSize = skipUtfByteOrderMark(value); } else { newSize = in.readLine(value, maxLineLength, maxBytesToConsume(pos)); pos += newSize; } if (newSize == 0) { return false; } if (newSize < maxLineLength) { return true; }
buffer
- 如图中所示,map计算后的结果首先会存入到内存中的一个扇形缓存区
MapOutputBuffer
,默认大小为100M,当缓存大于80%的时候开始异步(SpillThread)进行溢写。具体可查看org.apache.hadoop.mapred.MapTask#MapOutputBuffer.init()
的源码; - 溢写的时候要对数据进行分区和排序,分区默认哈希取模的方式,排序默认为快排保证分区内有序,可通过参数
map.sort.class
设置为其他排序类, - 溢写出的多份小文件最终会merge成大文件交由reduce task处理。
reduce task
- 一个需要清楚的概念是reduce task和map task是线性的关系,只有所有map task都执行完了,reduce task才能开始;
- reduce task在 mapreduce中默认为1,可通过
mapred.reduce.tasks
指定,上图中有3个reduce task; - 在hive中默认是通过 每个reduce处理的数据量,每个任务最大的reduce任务数,总的数据量等计算出来的,也可以通过参数设置。
fetch
- map端的数据并不是发送到reduce端的,而是reduce主动去map端拉取对应分区的数据,所以才叫fetch;
- 从不同的map端拉取的数据要要进行合并,合并的时候通过归并排序的方式保证数据在分区、分组内有序。
reduce
- 在一个mapreduce中map方法执行的次数是由数据记录数决定的,即map方法的作用颗粒度是一条记录,但是reduce方法的作用颗粒度是一组记录;
- 和map一样,redcue也是采用迭代器的方式读取数据,所以数据也是一条条进入内存的,只不过reduce中使用了两个迭代器非常巧妙的辨别数据是否为同组数据的问题。后续再通过源码解析的方式解释这个细节。
总结
从拆分的各个步骤可以看出,shuffle中做了大量的排序,数据溢写、合并和网络传输的工作,故而shuffle是个非常耗时的过程。