Hadoop采用的是分布式并行计算的模式来处理大数据,在处理时必然要对数据进行分片,将数据由大化小,将一个大的任务化为几个小的任务,这就是hadoop处理大数据的核心思想。
这里要讨论的是hadoop对数据进行分片的方案,这里的分片是逻辑上的,不同于Hdfs对数据的分块,分片并没有改变数据的存储位置。分片在hadoop中由InputFormat这个类体系来完成。
先来看看InputFormat类的完整体系结构。
很容易看出来这个类体系的祖宗是一个抽象类:InputFormat,这个抽象类定义了两个抽象方法getSplits和createRecordReader,如下:
public abstract List<InputSplit> getSplits(JobContext context
) throws IOException, InterruptedException;
/**
* Create a record reader for a given split. The framework will call
* {@link RecordReader#initialize(InputSplit, TaskAttemptContext)} before
* the split is used.
* @param split the split to be read
* @param context the information about the task
* @return a new record reader
* @throws IOException
* @throws InterruptedException
*/
public abstract RecordReader<K,V> createRecordReader(InputSplit split,
TaskAttemptContext context
) throws IOException,
InterruptedException;</span>
第一个函数getSplits就起将数据分片的作用,第二个函数读取分片的数据,组织成键值对的格式,Map函数处理的键值对就是由这个函数返回的RecordReader对象读取的。
下面我们从源代码中看看Hadoop是如何对数据进行分片的,从上面的类体系继承图可以看到,FileInputFormat类直接继承了InputFormat类,该类实现了getSplits函数,但没有实现createRecordReader函数,所以它还是一个抽象类。那么也就是说,FileInputFormat类实现了对数据分片的功能。
打蛇打七寸,直奔FileInputFormat源码中的getSplits函数:
/**
* Generate the list of files and make them into FileSplits.
*/
public List<InputSplit> getSplits(JobContext job
) throws IOException {
<span style="color:#ff0000;">long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));</span>
<span style="color:#cc0000;">long maxSize = getMaxSplitSize(job);</span>
// generate splits
List<InputSplit> splits = new ArrayList<InputSplit>();
List<FileStatus>files = listStatus(job);
for (FileStatus file: files) {
Path path = file.getPath();
FileSystem fs = path.getFileSystem(job.getConfiguration());
long length = file.getLen();
BlockLocation[] blkLocations = fs.getFileBlockLocations(file, 0, length);
<span style="background-color: rgb(255, 255, 102);">if ((length != 0) && isSplitable(job, path)) {
<span style="color:#cc0000;"> long blockSize = file.getBlockSize();</span>
<span style="color:#ff0000;">long splitSize = computeSplitSize(blockSize, minSize, maxSize);</span>
long bytesRemaining = length;
while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {
int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);
splits.add(new FileSplit(path, length-bytesRemaining, splitSize,
blkLocations[blkIndex].getHosts()));
bytesRemaining -= splitSize;
}
if (bytesRemaining != 0) {
splits.add(new FileSplit(path, length-bytesRemaining, bytesRemaining,
blkLocations[blkLocations.length-1].getHosts()));
}
} else if (length != 0) {
splits.add(new FileSplit(path, 0, length, blkLocations[0].getHosts()));
} else {
//Create empty hosts array for zero length files
splits.add(new FileSplit(path, 0, length, new String[0]));
}
}</span>
// Save the number of input files in the job-conf
job.getConfiguration().setLong(NUM_INPUT_FILES, files.size());
LOG.debug("Total # of splits: " + splits.size());
return splits;
}</span>
好戏开场了,前戏。
第一步,确定分片大小。
诸君注意上面的四行红色代码:
第一行红色代码找出分片的最小长度,它调用了两个函数getFormatMinSplitSize(), getMinSplitSize(job)。getFormatMinSplitSize返回值是1,getMinSplitSize从配置文件中获取我们配置的分片最小长度,获取的是配置文件中mapred.min.split.size的属性值。二者取其大作为分片的最小长度。
第二行红色代码从配置文件中读取我们配置的分片最大长度,获取的是配置文件中的“mapred.max.split.size”属性值,如果该值没有配置,返回Long.Max_value。
第三行红色代码获取文件该文件文件块的大小。
第四行红色代码,调用了一个函数computeSplitSize。有必要看看它的源码:
<span style="font-size:18px;"> protected long computeSplitSize(long blockSize, long minSize,
long maxSize) {
return Math.max(minSize, Math.min(maxSize, blockSize));
}
</span>
会发现FileInputFormat分片的大小不能超过文件单个分块的长度,分片可以小于等于数据块的长度,但不能大于。默认情况下如果我们不配置配置文件中的“mapred.max.split.size”属性值的话,分片大小为块的大小,默认块大小64M。
第二步,高潮来了,开始分片了。
为了方便查看,我们把上面黄色区域的代码搬到下面来看。
<span style="color:#33ff33;">if ((length != 0) && isSplitable(job, path))</span> {//如果文件长度不为0并且文件允许分割
long blockSize = file.getBlockSize();
long splitSize = computeSplitSize(blockSize, minSize, maxSize);
long bytesRemaining = length;
<span style="background-color: rgb(51, 51, 255);"><span style="color:#ffffff;"> while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {
int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);
splits.add(new FileSplit(path, length-bytesRemaining, splitSize,blkLocations[blkIndex].getHosts()));
bytesRemaining -= splitSize;
}</span></span>
if (bytesRemaining != 0) {
splits.add(new FileSplit(path, length-bytesRemaining, bytesRemaining,
blkLocations[blkLocations.length-1].getHosts()));
}
}<span style="color:#66ff99;"> else if (length != 0) </span>{//长度不等于0但不许对文件分割,即不能对文件分片,整个文件作为一个分片。
splits.add(new FileSplit(path, 0, length, blkLocations[0].getHosts()));
} <span style="color:#33ff33;">else</span> {//文件长度为0
//Create empty hosts array for zero length files
splits.add(new FileSplit(path, 0, length, new String[0]));
}
先看上面三行绿色的代码,从总体上了解一下分片的策略:
第一行绿色代码表示如果文件中有数据并且允许分割,则按照之前的分片长度和下面要详解的分片算法进行分片。
第二行绿色代码表示如果文件中有数据但不允许对文件进行分割就将整个文件作为一个分片。
第三行绿色代码表示如果文件长度为0,表示文件没有数据,返回一个空的分片。
好,高潮开始吧。做好准备啦,搞懂会爽死的。
蓝底白字的部分是核心。
首先问自己一个问题,while的循环的条件表示什么意思呢?这个式子本身很好懂,bytesRemaining表示每次分片之前文件的长度,逐次递减, bytesRemaining/splitSize为了判读文件剩余的部分还够不够按照splitSize表示的长度分片,当bytesRemaining/splitSize不大于1.1时不再循环分片。
善于思考的人,要再问一个问题,为什么要是大于1.1呢,这是为了保证某个分片不致过短。比如,要分片的文件的长度只比分片长度大一点点,就应该将整个文件作为一个分片,而不是将其分割为两个分片,因为这样会导致第二个分片的长度太短。
解释的太细了,会降低读者的思考能力吧。
满足分片条件的话,开始对文件进行分片。文件在物理上是被分块存储在各个节点上,分片对象FileSplit应该包含该分片的起始位置所在的块。那么源码是找到分片是从哪个块开始的呢?哈哈,请看下回分解。
首先,看贴出来的第一大段源码中白纸黑字的那一行,这行将指定文件对应的所有块都查找出来。
接下来,就要按照splitSize代表的长度循环从文件的开头所在的块截取分片,并确定每个分片的起始位置所在的块。看什么蓝底红字的那一行,调用了一个函数getBlockIndex(blkLocations, length-bytesRemaining);该函数第一个参数是要分片的这个文件所有的分块组成的集合,第二个参数表示每次分片之后,文件的偏移量,这个偏移量很简单啦,比如一个128M的文件,读取64M之后,那此时这个偏移量就是64,表示文件未被分片的起始位置。把这个函数的源码贴出:
protected int getBlockIndex(BlockLocation[] blkLocations,
long offset) {
for (int i = 0 ; i < blkLocations.length; i++) {
if ((blkLocations[i].getOffset() <= offset) &&(offset < blkLocations[i].getOffset() + blkLocations[i].getLength())){
return i;
}
}
BlockLocation last = blkLocations[blkLocations.length -1];
long fileLength = last.getOffset() + last.getLength() -1;
throw new IllegalArgumentException("Offset " + offset +
" is outside of file (0.." +
fileLength + ")");
}
for循环表示对文件对应的块进行遍历,满足if条件的下标i对应的那个块就是该分片的起始位置所在的块,将这个索引值返回。看完这句话,如果这个函数的解释就到此结束,那你肯定云里雾里。我靠,这是神马逻辑,绝对骂娘。现在国人写的很多技术书有很多这种内容,个人比较喜欢外文的参考书。这里的关键是要搞懂if条件代表的是什么。这个算法设计的真他妈巧妙,所以要成为编程高手,源码绝对要看。
下面要举例详细讲解。
必须先搞定几个表达式的内涵先:blkLocations[i].getOffset()表示每个块在这个文件的偏移量,偏移量不用解释了吧,就是块的起始数据在整个文件中的位置。如果你还不懂,恭喜你,你有可能先飞,因为你笨。blkLocations[i].getLength()表示块的长度。offSize=length-bytesRemaining,bytesRemaining -= splitSize(bytesRemaining 的初始值是文件的长度的length)
假设一个文件有128M,对于hadoop这个巨人来说,这个是不是太小了,举个例子而已,不必当真。
假设分块的时候安装40M的块大小进行分块,会分为四个块,第四块只有8M数据。
假设分片长度是30M。操,一堆假设。
我们来列个表:共有四个块,所以有四行。
blkLocations[i].getOffset() 表blkLocations[i].getOffset() + blkLocations[i].getLength()
block[0] 0 40
block[1] 40 80
block[2] 80 120
block[3] 120 160
当第一次调用这个函数时offSize=0,0<=0<40,第一个块符合条件,所以第一个分片的开始位置在第一个分块。
第二次调用时offsize=30,会发现还是符合第一个分块的添加,0<=30<40,所以第二个分片的起始位置也在第一个分块。
第三次调用时offsize=60,满足第二个块的条件,40<60<80,第三个分片的起始位置位于第二个分块。
第四次调用时offsize=90,满足第三个块的添加 ,80<90<120,第四个分片的起始位置在第三个分块。
第五次调用时offsize=120 ,满足第四个块的添加,120<=120<128,第五个分片的起始位置位于第四个块。
然后再FileSplit中记下每个分片的起始位置所在的块,在根据偏移量从块中读取数据。
讲完了,懂了没,反正我是懂了。