源码分析
分布式计算追求:
- 计算向数据移动
- 并行度、分而治之
- 数据本地化读取
Client
没有计算发生。
支撑了计算向数据移动,和计算的并行度。
做的最主要的是:存储与计算解耦,就是对所要进行切片的数据进行split切片,(split == map并行度)。
Split默认是与Block块数量一致,目的是为了计算向数据移动,几个Block块分布在几个地方,就起几个Map,这样就不需要让大量数据进行移动,而是只需要将Jar包分发到各个Block所在的结点进行执行即可。
用户也可以对Split进行自定义:
当Split大于Block时,将会有部分数据向计算移动,但从原来的单机Map,会将所有数据拉取到一个结点上,考虑来说,这其实也是计算向数据移动。
当Split小于Block时,一般用于CPU密集型计算,也会有部分数据向计算移动。
MapTask
如何解决HDFS,Block刚好把一个单词切割开的问题?
即蓝色部分为
Block01内容,红色部分为Block02内容。Hello World!!!
Hello World!!!
Hello World!!!
其中第二行中的World单词被切割了,但是在进行MapReduce-WordCount计算时,Hadoop框架仍然能返回一个正确的值给我们,Hadoop框架是如何实现的呢?
阅读原码可以发现。MapReduce在使用LineRecordReader时,会判断,如果当前开始的文件偏移量不为0时(即不是第一个切片文件时),会读一行空行,即将偏移量移至Block的第二行开头。即示例中,偏移到全文的第三行开头处,然后在Map执行时,框架会将第二个Block中没有被处理的数据移动向第一个Block所处的位置进行计算,这样就可以解决单词被Block切分开的问题了。
Input -> map -> output
-
Input(split+format):来自于我们的输入格式化类给我们实际返回的记录读取器对象。TextInputFormat -> LineRecordReaderSplit:file、offset、length
init():
in = fs.open(file).seek(offset)- 除了第一个Map之外,之后的Map都会让出第一行,从Split的第二行读起。换言之,前一个Map会多读取一行,来弥补HDFS把数据切割问题。
nextKeyValue():
- 读取数据中的一条
Key,Value数据 - 返回Boolean值
getCurrentKey()
getCurrentValue()
-
output:-
判断是否需要Reduce?
- 需要,则再次判断Reduce需要几个分区(Partition)?若为1个,则分区器都指定为0,若分区数大于1个,则默认使用Hash分区器进行分区,也可以通过自定义分区器进行。
- 不需要,则直接写出。
-
写出是通过writer写出(K, V, P)的格式到缓存区(
MapOutPutBuffer)中。 -
MapOutPutBuffer:-
*:
-
map()方法执行完毕之后,会进行计算对应的partion值,再通过writer通过三元组(K、V、P)的方式写到缓冲区(可以理解为二位数组)中。
-
缓冲区:
-
缓冲区存储的数据为对应的K、V数据,及其元数据(索引,大小恒定为16byte,存储的是P值、KeyStart、ValueStart、ValueLength),这样我们就可以通过整齐排序的索引数据,随机取出对应数据的KEY。
-
环形缓冲区,其实是逻辑上为环形,其实际内存结构仍为线性。
原理:
通过在缓冲区中划分一个界线,在界线的两边分别存储,KV数据与索引数据,当缓冲区占用率达到80%时,锁住已经有数据的缓冲区,进行快速排序并且溢写;与此同时,在没有数据的那一边的环形缓冲区,再次划分一个界线,再次在界线两边存储数据。若在缓冲区还没有充满的同时,溢写数据结束了,那则可以不影响到写的数据的线程进行。
优点:更好地利用了内存空间,提高了效率。
-
溢写数据前先进行快速排序,先按分区值进行排序,再按Key值进行排序;排序进行的交换数据,是交换索引数据而不是KV数据,原因是:KV数据大小参差不齐,大小一旦不一致,就不能进行数据交换,而KV数据是恒定16byte的,可以进行交换。写出时候,根据索引取出对应的元数据,再通过指向的内存位置,卸下对应的KV数据即可完成排序后溢写。
-
Combiner优化(减少IO),Combiner相当于在Map中提前进行合并,发生在排序之后,在溢写之前。在多个小文件通过归并排序合并成一个大文件的时候,若小文件的个数大于等于minSpillsForCombine(默认 = 3)值时,也会进行一次合并。
(Tips:为什么每一个Map的数据需要由多个小文件合并成一个大文件?磁盘IO是MapReduce的性能瓶颈,合并文件可以避免磁盘的随机读取,加快IO速度。)
-
Combine须注意:合并必须遵循幂等性,即合并数据的过程不应该使最终结果发生错误。
-
-
-
Init() :
spiller: 0.8 缓冲区溢写百分比,默认为80%
sortmb:100 缓冲区大小
sorter:QuickSort 排序算法
comparator:job.getOutputKeyComparator()
- 优先取用户自定义比较器
- 默认取这个Key类型自身的比较器
-
-
Combiner: 用于提前合并相同的Key的K-V数据。
minSpillsForCombine = 3
-
SpillThread
sortAndSpill() 主要用于做排序并溢血。
并判断是否需要合并,若需要合并,则进行。
-
ReduceTask
input -> reduce -> output
rIter - input : 会到所有的Map端产生的文件,拉取对应的数据到Reduce端,并经过sort归并排序成一个大文件,并将其封装成一个迭代器。
而在reduce()方法运行时,也是通过传递一个“假”迭代器values,来将同一个key的数据加载进行的。
values迭代器通过hasNext()方法判断是否还有相同Key的数据,其实就是通过nextKeyIsSame值(下一个值的Key是否与当前Key相等)来判断,当前同一个Key为一组的数据是否还有下一条数据。
而通过next()方法则可以间接调用rIter的nextKeyValue()拉取出一条数据,通过再多读一条数据,判断下一个数据是否是与当前数据的Key相同,更新nextKeyIsSame值。
分组排序器:将大文件中K-V数据,分成一组进行Reduce操作,若用户没有指定,默认使用Key分组进行。
为什么使用迭代器?
避免OOM:大数据文件很大,不能全部加载到内存中。
减少IO成本:同一组中可能会有多个不同的Key,这些不同的Key需要进入不同的reduce()方法中,只需要真迭代器从头开始读,而每次当需要处理新的一组Key数据时,只需要通过假迭代器配合真迭代器去读出数据即可完成ReduceTask操作,整个ReduceTask只需要真迭代器进行的一次I/O,假迭代器进行间接调用。